『入門Haskell』練習問題例解(15)
『入門Haskell』最後の問題です。
Base64(エンコード)
p.144の(1)です。
(1) このBase64エンコードは「76文字以内に改行を入れる」ということが行われていません。これに対する対処を、以下の2つのアプローチで実装しなさい。
1. base64Encode関数でevalStateした結果に文字列処理を行う
2. Stateモナドが保持する状態に、「現在何文字目を処理しているか」を持っておく
p.143のbase64.hsを元にして改行処理を追加します。ところでbase64.hsのcalcSuffix関数の定義の中で
calcSuffix (Second n) = table !! shiftR n 4 : "==\n" calcSuffix (Third n) = table !! shiftR n 2 : "=\n"
となっているのは
calcSuffix (Second n) = table !! shiftL n 4 : "==\n" calcSuffix (Third n) = table !! shiftL n 2 : "=\n"
の誤りです。shiftRではなくshiftLです。それを踏まえた上で…
(1) 1.
76文字ごとに改行を入れていく関数insertLineFeedsを定義して使います。
base64Encode' = insertLineFeeds . base64Encode where insertLineFeeds s | length s <= 76 = s | otherwise = (take 76 s) ++ "\n" ++ insertLineFeeds (drop 76 s)
このbase64Encode'関数を使えば、正しく改行が挿入されます。
(1) 2.
B64EncStateの定義を書き換えます。
data B64EncState = First Int | Second Int Int | Third Int Int
1つ目のIntが「現在何文字目を処理しているか」という状態です。先ほどはエンコードした後の文字列に対して76文字ごとに改行を入れました。今度はエンコード中に改行を入れていきます。ということは、76 / 4 * 3 = 57文字処理するごとに改行を入れることになります。よってコード全体は
import Control.Monad.State import Data.Char import Data.Bits data B64EncState = First Int | Second Int Int | Third Int Int table = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+/" procChar :: Int -> State B64EncState String procChar c = get >>= transit where transit (First m) = do put $ Second (m+1) (c .&. 3) return [table !! shiftR c 2] transit (Second m n) = do put $ Third (m+1) (c .&. 15) return [table !! (shiftL n 4 .|. shiftR c 4)] transit (Third m n) = do put $ First (m+1) return $ [ table !! (shiftL n 2 .|. shiftR c 6) , table !! (c .&. 63)] ++ if m `mod` 57 == 0 then "\n" else "" calcSuffix :: B64EncState -> String calcSuffix (First m) = "\n" calcSuffix (Second m n) = table !! shiftL n 4 : "==\n" calcSuffix (Third m n) = table !! shiftL n 2 : "=\n" b64Enc :: String -> State B64EncState String b64Enc s = do strs <- mapM (procChar.ord) s suffix <- gets calcSuffix return (concat strs ++ suffix) base64Encode s = evalState (b64Enc s) (First 1)
となります。
Base64(デコード)
p.144の(2)です。
(2) Base64文字列から元のバイト列を復元するbase64Decode関数を定義しなさい。
エンコードの時は「元の文字列を6ビット単位で切り分ける -> テーブルから文字を探して置き換えていく」ということをやったので、デコードの場合はまず逆引きをしなくてはいけません。逆引きテーブルは連想リストの形で
reverseTable = zip (['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+/") [0..]
となります。文字'Z'を逆引きしたいときは fromJust $ lookup 'Z' reverseTable とすれば25が得られます。状態は
data B64DecState = First | Second Int | Third Int | Fourth Int
で表します。これを使って
1文字目…空文字列を値として返す。「今見ている数字の下位6ビット」を次に渡す。
2文字目…「1文字目の時に残した6ビットを左に2桁シフトしたもの」と「今見ている文字を右に4桁シフトしたもの」との論理和を取って値として返す。「今見ている数字の下位4ビット」を次に渡す。
3文字目…「2文字目の時に残した4ビットを左に4桁シフトしたもの」と「今見ている文字を右に2桁シフトしたもの」との論理和を取って値として返す。「今見ている数字の下位2ビット」を次に渡す。
4文字目…「3文字目の時に残した2ビットを左に6桁シフトしたもの」と「今見ている文字」との論理和を取って値として返す。
という処理を行います。後は改行文字'\n'とイコール'='ですが、単純に無視すればいいみたいです(状態遷移も行いません)。
以上をまとめてコードの全体を示します。
import Control.Monad.State import Data.Char import Data.Bits import Data.Maybe data B64DecState = First | Second Int | Third Int | Fourth Int reverseTable = zip (['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+/") [0..] decChar :: Int -> State B64DecState String decChar 61 = return "" -- '=' は無視 decChar 10 = return "" -- '\n' は無視 decChar c = get >>= transit where d = fromJust $ lookup (chr c) reverseTable transit First = do put $ Second (d .&. 63) return "" transit (Second n) = do put $ Third (d .&. 15) return [chr (shiftL n 2 .|. shiftR d 4)] transit (Third n) = do put $ Fourth (d .&. 3) return [chr (shiftL n 4 .|. shiftR d 2)] transit (Fourth n) = do put $ First return [chr (shiftL n 6 .|. d)] b64Dec :: String -> State B64DecState String b64Dec s = mapM (decChar.ord) s >>= return . concat base64Decode :: String -> String base64Decode s = evalState (b64Dec s) First
試してみます。
*Main> base64Encode "Gakuen Utopia Manabi Straight!" "R2FrdWVuIFV0b3BpYSBNYW5hYmkgU3RyYWlnaHQh\n" *Main> base64Decode "R2FrdWVuIFV0b3BpYSBNYW5hYmkgU3RyYWlnaHQh\n" "Gakuen Utopia Manabi Straight!"
ちゃんと元の文字列が復元できました。これで『入門Haskell』の練習問題は全て終わりました。お疲れ様でした。
『入門Haskell』練習問題例解(14)
p.128。モナド関連の関数です。
1. sequence_ を foldr を使って定義しなさい。
2. mapM と mapM_ を定義しなさい(sequenceなどを使ってもよい)。
3. foldM と foldM_ を定義しなさい。
sequence_
mySequence_ :: Monad m => [m a] -> m () mySequence_ = foldr (>>) (return ())
こうなります。ちなみに、sequence を foldr を使って定義すると
mySequence :: (Monad m) => [m a] -> m [a] mySequence = foldr ((flip (.) (((flip (.) ((return .) . (:))) . (>>=)))) . (>>=)) (return [])
となります。もはや何がなんだか…
mapM, mapM_
mapM f = (\x -> sequence (map f x)) => (\x -> sequence ((map f) x)) => (sequence . (map f)) => (\x -> sequence . (map x)) f => ((sequence .) . map) f
なので、最終的には
myMapM :: Monad m => (a -> m b) -> [a] -> m [b] myMapM = (sequence .) . map myMapM_ :: Monad m => (a -> m b) -> [a] -> m () myMapM_ = (sequence_ .) . map
となります。
foldM, foldM_
foldM f a [x1, x2, ... ,xn] は
f a x1 >>= (\y -> f y x2) >>= ... >>= (\y -> f y xn) >>= (\y -> f y [])
と同じことです。flip関数を使ってyが消去できるので、答えは
myFoldM :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a myFoldM f a [] = return a myFoldM f a (x:xs) = (f a x) >>= (flip (myFoldM f) xs) myFoldM_ :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m () myFoldM_ f a [] = return () myFoldM_ f a (x:xs) = (f a x) >>= (flip (myFoldM_ f) xs)
となります。
次回、最終回!
『入門Haskell』練習問題例解(13)
p.113です。
(1)木の最大深さを計算する関数depthを書きなさい。
(2)上で示したtreeToList(p.112)は、リストの順序が滅茶苦茶になっています。これを、キーの昇順に並べるように実装しなおしなさい。
昇順に並んだリストを返す関数はtreeToSortedListとします。(1)(2)をいっぺんに解くと
import List data Tree a = Leaf | Node a (Tree a) (Tree a) deriving Show depth :: Tree a -> Int depth Leaf = 0 depth (Node _ l r) = 1 + max (depth l) (depth r) lengthTree :: Tree a -> Int lengthTree Leaf = 0 lengthTree (Node _ l r) = 1 + lengthTree l + lengthTree r lookupTree :: (Ord a) => a -> Tree (a, b) -> Maybe b lookupTree x Leaf = Nothing lookupTree x (Node (k, v) l r) | x < k = lookupTree x l | x > k = lookupTree x r | otherwise = Just v insertTree :: (Ord a) => (a, b) -> Tree (a, b) -> Tree (a, b) insertTree (k', v') Leaf = Node (k', v') Leaf Leaf insertTree (k', v') (Node (k, v) l r) | k' < k = Node (k, v) (insertTree (k', v') l) r | k' > k = Node (k, v) l (insertTree (k', v') r) | otherwise = Node (k', v') l r emptyTree = Leaf listToTree :: (Ord a) => [(a, b)] -> Tree (a, b) listToTree = foldl (flip insertTree) emptyTree treeToList :: Tree a -> [a] treeToList Leaf = [] treeToList (Node x l r) = x : treeToList l ++ treeToList r treeToSortedList :: (Ord a) => Tree a -> [a] treeToSortedList = sort . treeToList
こうなります。
『入門Haskell』練習問題例解(12)
p.106です。
(1) MaybeはOrd a => Ord (Maybe a)でもあります。このインスタンス宣言を行いなさい。またShow a => Show (Maybe a)のインスタンス宣言も行いなさい。
(2) 2章で見たように、Ordの宣言の概略は次のようになっています。(中略)これにデフォルトの定義を加えることで、必要最低限の関数だけ用意すれば動作するようにしなさい。
Maybe
MyMaybe型を作ります。derivingを使ってよいなら
data MyMaybe = data MyMaybe a = MyNothing | MyJust a deriving (Eq, Ord, Show)
で一発です。使わなければ
data MyMaybe a = MyNothing | MyJust a instance (Eq a) => Eq (MyMaybe a) where MyNothing == MyNothing = True MyJust x == MyJust y = x == y _ == _ = False instance (Ord a) => Ord (MyMaybe a) where compare MyNothing MyNothing = EQ compare MyNothing _ = LT compare _ MyNothing = GT compare (MyJust x) (MyJust y) = compare x y instance (Show a) => Show (MyMaybe a) where show MyNothing = "MyNothing" show (MyJust x) = "MyJust" ++ show x
こんな感じになると思います。ただしこの定義だと show (MyJust (MyJust 1)) は "MyJust (MyJust 1)" ではなく "MyJust MyJust 1" となってしまいます。何とかしたいのですが…。
Ord
MyOrd型クラスを作ります。関数は myCompare, (@>), (@<), (@>=), (@<=), myMax, myMin とします。
はじめ、スーパークラスにはEq型クラスをそのまま使おうと思ったのですが、(==) と (/=) だけ@が頭についてないのは何となく気持ち悪いなあと思ったのでMyEq型クラスを用意することにしました。ついでにOrdering型に代えてMyOrdering型を使うことにしました。
class MyEq a where (@==), (@/=) :: a -> a -> Bool x @== y = not (x @/= y) x @/= y = not (x @== y) data MyOrdering = MyLT | MyEQ | MyGT instance MyEq MyOrdering where MyLT @== MyLT = True MyEQ @== MyEQ = True MyGT @== MyGT = True _ @== _ = False class MyEq a => MyOrd a where myCompare :: a -> a -> MyOrdering (@>), (@<), (@>=), (@<=) :: a -> a -> Bool myMax, myMin :: a -> a -> a x @> y = myCompare x y @== MyGT x @< y = myCompare x y @== MyLT x @>= y = myCompare x y @/= MyLT x @<= y = myCompare x y @/= MyGT myMax x y | x @>= y = x | otherwise = y myMin x y | x @<= y = x | otherwise = y
ためしに data Alpha = A | B | C | D という型に順序を入れてみます。
data Alpha = A | B | C | D deriving Show instance MyEq Alpha where A @== A = True B @== B = True C @== C = True D @== D = True _ @== _ = False instance MyOrd Alpha where myCompare A A = MyEQ myCompare A _ = MyLT myCompare _ A = MyGT myCompare B B = MyEQ myCompare B _ = MyLT myCompare _ B = MyGT myCompare C C = MyEQ myCompare C D = MyLT myCompare D C = MyGT myCompare D D = MyEQ
GHCiで結果が確認できるようにShow型クラスを継承させました。
*Main> myMax C B C *Main> myMin A A A
こうなります。
『計算論』メモ 問1.3.15
『計算論 計算可能性とラムダ計算 (コンピュータサイエンス大学講座)』p.22の問1.3.15。
- 代入命令 (0を代入)
- 代入命令 (次の自然数を代入)
- 判定命令 (xとyが等しいか判定し分岐する)
- 入力命令(入力値は保存)と出力命令
からなるNプログラムを正規形のNプログラムと言うが、任意のNプログラムPに対して、Pと同じ関数を計算するNプログラムがあることを示せ、という問題。
Nプログラムとは
- 代入命令
- 判定命令
- 入力命令(入力値は保存)と出力命令
で構成される。ここでf, gは変数と定数0, 1, 2, ...と四則演算 からなる任意の算術式である。
代入命令 が正規形Nプログラムで計算できるならば、判定命令 も正規形Nプログラムで計算できる。なぜなら
(1) input() (2) f() = g() ?
は
(1) input() (2) s := f() (3) t := g() (4) s = t ?
に変形できるからである。そこで代入命令 が正規形Nプログラムで計算できることを示せばよいが、 は仮定により算術式であり、算術式は原始的関数であるから、原始的関数の定義と定理1.3.14(「原始的関数はNプログラムで計算することができる」)により、零関数 、後者関数 、射影関数 が正規形Nプログラムで計算できることを示せば十分である。
零関数。
(1) input() (2) s := 0 (3) output(s)
後者関数。矢印で行き先が明示されていない場合次の行へ進むものとする。
(1) input(x) (2) s := 0 (3) s = x ? true -> (5) false -> (4) (4) s := s + 1 -> (3) (5) s := s + 1 (6) output(s)
射影関数。
(1) input() (2) output()
証明終。