読者です 読者をやめる 読者になる 読者になる

チューリング不完全

What are you afraid of? All you have to do is try.

プログラミングHaskell 第7章 高階関数

前章に比べるとレベル上がった印象。


import Data.Char(chr, ord, isLower, isUpper)

-- 7.1 基本概念
twice :: (a -> a) -> a -> a
twice f x = f (f x)

-- 7.2 リスト処理
map :: (a -> b) -> [a] -> [b]
map f xs = [f x | x <- xs]

map f [] = []
map f (x:xs) = f x : map f xs

filter :: (a -> Bool) -> [a] -> [a]
filter p xs = [x | x <- xs, p x]
filter p [] = []
filter p (x:xs) | p x       = x : filter p xs
                | otherwise = filter p xs

-- 整数のリストから偶数を取り出し、二乗した値の合計をとる
sumsqreven :: [Int] -> Int
sumsqreven ns = sum (map (^2) (filter even ns))

-- 7.3 畳込 foldr

sum = foldr (+) 0
product = foldr (*) 1
or = foldr (||) False
and = foldr (&&) True

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f v [] = v
foldr f v (x:xs) = f x (foldr f v xs)

length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs

length = foldr (\_ n -> 1+n) 0

reverse :: [a] -> [a]
reverse [] = []
reverse (x:xs) = reverse xs ++ [x]

-- 新しい要素をリストの末尾に追加する
snoc :: a -> [a] -> [a]
snoc x xs = xs ++ [x]

reverse [] = []
reverse (x:xs) = snoc x (reverse xs)

reverse foldr snoc []

-- error
-- (++ys) = foldr (:) ys 

-- 7.4 畳込関数 foldl
sum = foldl (+) 0
product = foldl (*) 1
or = foldl (||) False
and = foldl (&&) True

length = foldl (\n _ -> n+1) 0
reverse = foldl (\xs x -> x:xs) []
-- error
-- (xs++) = foldl (\ys y -> ys ++ [y]) xs

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v [] = v
foldl f v (x:xs) = foldl f (f v x) xs


-- 7.5 関数合成演算子
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f.g = \x -> f (g x)

odd n = not (even n)
odd = not.even

twice f x = f (f x)
twice f = f.f

sumsqreven ns = sum (map (^2) (filter even ns))
sumsqreven = sum.map (^2).filter even

id :: a -> a
id = \x -> x

compose :: [a -> a] -> (a -> a)
compose = foldr (.) id


-- 7.6 文字列の変換器
type Bit = Int

bin2int :: [Bit] -> Int
-- bin2int bits = sum [w*b | (w,b) <- zip weights bits]
--                where weights = iterate (*2) 1
bin2int = foldr (\x y -> x+2*y) 0

int2bin :: Int -> [Bit]
int2bin 0 = []
int2bin n = n `mod` 2 : int2bin (n `div` 2)

-- 二進表記を8ビットにする
make8 :: [Bit] -> [Bit]
make8 bits = take 8 (bits ++ repeat 0)

encode :: String -> [Bit]
encode = concat.map (make8.int2bin.ord)

-- 8ビットの二進表記に分割する
chop8 :: [Bit] -> [[Bit]]
chop8 [] = []
chop8 bits = take 8 bits : chop8 (drop 8 bits)

decode :: [Bit] -> String
decode = map (chr.bin2int).chop8

transmit :: String -> String
transmit = decode.channel.encode

channel :: [Bit] -> [Bit]
channel = id

exercise.

1.

リスト内包表記 [ f x | x <- xs, p x ] を
高階関数 map と filter を使って書き直すとどうなるか?

map f ( filter p xs )

2.

標準ライブラリでの定義を見ないで、
高階関数all, any, takeWhile, およびdropWhileを定義せよ。

myAll :: (a -> Bool) -> [a] -> Bool
myAll p [] = True
myAll p (x:xs) = (p x) && myAll p xs

myAny :: (a -> Bool) -> [a] -> Bool
myAny p [] = False
myAny p (x:xs) = (p x) || myAny p xs
  
myTakeWhile :: (a -> Bool) -> [a] -> [a]
myTakeWhile p [] = []
myTakeWhile p (x:xs) | p x       = x : myTakeWhile p xs
                     | otherwise = [] 

myDropWhile :: (a -> Bool) -> [a] -> [a]
myDropWhile p [] = []
myDropWhile p (x:xs) | p x       = myDropWhile p xs
                     | otherwise = x:xs


3.

関数foldrを用いて、関数map f と filter p を定義せよ。

ここでちょっと詰まりました。
map fは2つ定義が書いてあります。
まずはラムダ式の方を思いついたのですが、うーん?というのをTwitterでつぶやいたところ@osanai_先生に2つめのを教えてもらいました。
でもそれをfilterの方には全然いかせてません。

map :: (a -> b) -> [a] -> [b]
map f = foldr (\x y -> f x : y) [] 
map f = foldr ((:).f) []

filter :: (a -> Bool) -> [a] -> [a]
filter f = foldr (\x y -> if f x then x:y else y) []

4.

foldlを用いて、十進表記を整数に変換する関数
dec2int :: [Int] -> Int
を定義せよ。

dec2int :: [Int] -> Int
dec2int = foldr (\x y -> x + 10*y) 0


5.

以下の定義が誤りである理由を説明せよ。
sumsqreven = compose [sum, map (^2), filter even]

compose :: [a -> a] -> (a -> a)
map(^2), filter evenは同じ型だが、sumのみ異なるため。


6.

標準ライブラリの定義を見ないで、以下の2つの高階関数を定義せよ。
・「引数に組をとる関数」を「カリー化された関数」へ変換する関数 curry
・「引数が2つのカリー化された関数」を「引数に組をとる関数」へ変換する関数 uncurry
ヒント:まず、関数の型を書きなさい。

ここでどはまりしました。
引数を2つとる関数を定義する場合、まだどうしても左辺にf x y = みたいに書いてしまいます。

mycurry :: ((a, b) -> c) -> (a -> b -> c)
mycurry = (\a b c -> a (b,c))

myuncurry :: (a -> b -> c) -> ((a, b) -> c)
myuncurry = (\a (b,c) -> a b c)


7.

高階関数 unfold は、リストを生成する簡単な再帰の様式を閉じ込める。
この関数は、以下のように定義できる。


unfold p h t x | p x = []
| otherwise = h x : unfold p h t (t x)


すなわち、関数 unfold p h t は、
述語pを引数に適用した結果Trueとなれば、空リストを返す。
そうでなければ、関数hを引数へ適用することで先頭の要素を作り、
残りのリストは自分自身を呼び出すことで生成して、全体として空でないリストを作る。
再帰する際には、関数tを引数に適用して、新たな引数を作る。
たとえば関数unfoldを用いると、関数int2binはもっと簡潔に書き直せる。

int2bin = unfold (==0) (`mod`2) (`div`2)

関数 unfold を用いて関数 chop8, map f, iterate fを再定義せよ。
|

不完全です。というのもmapで適用できる型にEqクラスであるという制限がついてしまっています。
(==[])のせいなんですが、どうしたもんでしょうか・・・

unfold :: (a -> Bool) -> (a -> b) -> (a -> a) -> a -> [b]
unfold p h t x | p x = []
               | otherwise = h x : unfold p h t (t x)

chop8_unfold :: [Bit] -> [[Bit]]
chop8_unfold = unfold (==[]) (take 8) (drop 8)

map_unfold :: Eq a => (a -> b) -> [a] -> [b]
map_unfold f = unfold (==[]) (f.head) (tail)

8.

パリティービットを用いることで、
文字列を通信するプログラムを軽度の通信エラーを検出できるように変更せよ。
すなわち、8ビットの二進表記は、符号化の際にパリティービットを付加される。
パリティービットは、1の個数が奇数であれば1, そうでなければ0となる。
逆に複号の際は、9ビットの二進表記のパリティービットが正しいか検査する。
正しければパリティービットを捨て誤りであればパリティーエラーを報告する。
ヒント: ライブラリ関数 error :: String -> a は、
評価を強制終了し、与えられる文字列をエラーメッセージとして表示する。

check_parity中でerrorを使ってるんですが、
これcheck_parityの型を完全に無視してる気がするんだけど大丈夫なの?(´・ω・`)

それから、decode_parityの定義がたぶんなんかおかしいです。
ほんとはmap(chr.bin2int.check_parity).chop9と書きたかったのですが、
bin2int.check_parityのとこで怒られてしまいました。
(.)演算子がちゃんとわかってない。

-- パリティを先頭に付加
make9_parity :: [Bit] -> [Bit]
make9_parity bits = length (filter odd bits) `mod` 2 : make8 bits

encode_parity :: String -> [Bit]
encode_parity = concat.map(make9_parity.int2bin.ord)

-- まじめにやるならchop nを作ってchop8, chop9はそれを使う方がよい気がする
chop9 :: [Bit] -> [[Bit]]
chop9 [] = []
chop9 bits = take 9 bits : chop9 (drop 9 bits)

check_parity :: [Bit] -> [Bit]
check_parity (x:xs) | x == checked_parity = xs
                    | otherwise           = error "parity error"
                        where checked_parity = length (filter odd xs) `mod` 2

decode_parity :: [Bit] -> String
decode_parity = map(chr.bin2int).map(check_parity).chop9

9.

通信エラーの生じる通信路を用いて、
練習問題8で定義した文字列を通信するプログラムを試せ。
この通信路は最初のビットを落とすとする。
これは、関数tailをビットのリストに適用することで実現できる。

transmit_normal :: String -> String
transmit_normal = decode_parity.channel.encode_parity

transmit_broken :: String -> String
transmit_broken = decode_parity.channel_broken.encode_parity

channel_broken :: [Bit] -> [Bit]
channel_broken = tail