fugafuga.write

日々のログ

すごいH本 part78

newtype と 遅延評価

undefined という値がある。これは、ぶっ壊れた計算を表す。 この値を評価すると Haskell はすごく怒る。

*Main> undefined
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:14:1 in interactive:Ghci3

すなわち例外を投げる。

しかし、undefined を含むリストを作っても、undefined の要素を要求しなければ、 例外発生せずに処理される。

*Main> head [3,4,5,undefined,2,undefined]
3

Haskell は先頭要素以外を評価しないためである。

ここで以下のような型を作る

data CoolBool = CoolBool { getCoolBool :: Bool }

中身の Bool が True か False かどうかによらず、'hello' を返す関数を書く。

helloMe :: CoolBool -> String
helloMe (CoolBool _) = "hello"

この関数を、CoolBool 値ではなく undefined に適用してみる。

*Main> helloMe undefined
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:21:9 in interactive:Ghci1

data キーワードで定義された方には複数の値コンストラクタがあるかもしれないので、 関数の引数が (CoolBool _) に合致するかどうかを確認するためには、 どのコンストラクタが使われたのか分かるところまで引数の評価を進める必要がある。

そして、undefined を評価しようとして例外が発生している。

今度は、CoolBool を newtype で定義する。

newtype CoolBool = CoolBool { getCoolBool :: Bool }

同じように関数を適用してみる

*Main> helloMe undefined
"hello"

例外が発生しない。

newtype キーワードはコンストラクタを1つしか作れないことを Haskell は知っているため、 引数が (CoolBool _) パターンに合致すると判定できる。

  • data はオリジナルな型を無から作り出すもの
  • newtype は既存の型をもとにはっきり区別される新しい型を作るもの
  • data に対するパターンマッチは箱から中身を取り出す操作
  • newtype に対するパターンマッチはある型を別の型に直接変換する操作

type vs. newtype vs. data

type

型シノニムを作るためのもの

type IntList = [Int]

型注釈にどちらの名前を使うかは自由

*Main> ([1,2,3] :: IntList) ++ ([1,2,3] :: [Int])
[1,2,3,1,2,3]

型シノニムは型シグネチャを整理してわかりやすくしたい時に使う。 [(String, String)]PhoneBook という名前で扱えるようにするなど。

newtype

既存の型を包んで新しい型を作るためのもの

newtype CharList = CharList { getCharList :: [Char] }

この場合、CharList[Char]++ で連結することはできない。 ++ はリスト限定の演算子なので無理。

newtype 宣言でレコード構文を使うと、newtype の値コンストラクタと、 フィールド内の値を取り出す関数が作られる。

新しい型は、元の型の所属していた型クラスを引き継がないので、deriving で導出するか、 インスタンス宣言を手書きするか、GeneralizedNewtypeDeriving を使う必要がある。

data

data は自作の新しいデータ型を作るためのもの。

3つの使い分け方

  • 型宣言を整理したい、型名が体を表すようにしたいだけなら type
  • 既存の型をある型クラスのインスタンスにしたくて、新しい型にくるむなら newtype
  • まったく新しいものを作るなら data

所感

3つのデータ型宣言の違いがなんとなくわかった。

モノイドはいまだ登場せず。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part77

モノイド

Monoid は、値を2項演算子で結合できるような型を表現する。

newtype キーワード

newtype キーワードを使うと、既存の型から新たな型を作ることができる。

リストをアプリカティブファンクターにする方法は複数ある。

左辺のリストの関数と右辺のリストの値のあらゆる組み合わせのリストを作る方法。

*Main Control.Applicative> [(+1),(*100),(*5)] <*> [1,2,3]
[2,3,4,100,200,300,5,10,15]

2つのリストを綴じ合わせるように新たなリストを作る方法。 ZipListを使う。

*Main Control.Applicative> getZipList $ ZipList [(+1),(*100),(*5)] <*> ZipList [1,2,3]
[2,200,15]

ZipList データ型宣言をどう書くか考える

data ZipList a = ZipList [a]

レコード構文を使うとこうなる

data ZipList a = ZipList { getZipList :: [a] }

newtype を使うとこうなる

newtype ZipList a = ZipList { getZipList :: [a] }

ZipList は実際にこのようにnewtypeで定義されている。 data で定義されない理由は、コンストラクタに包んだりほどいたりするような処理のオーバーヘッドがかかるため。

newtype は既存の型を何かでくるんで新しい型を作るために使う。

いくつか制約もある。

  • 値コンストラクタは1種類しか作れない
  • 値コンストラクタに持てるフィールドも1種類だけ

data の場合、上記のような制約は無い。

data Profession = Fighter | Archer | Accountant
data Race = Human | Elf | Orc | Goblin
data PlayerCharacter = PlayerCharacter Race Profession

newtype で作った型に対しても data と同じように deriving で型クラスのインスタンスを導出できる。

*Main Lib> newtype CharList = CharList { getCharList :: [Char] } deriving (Eq, Show)
*Main Lib> CharList "this will be shown!"
CharList {getCharList = "this will be shown!"}
*Main Lib> CharList "benney" == CharList "benny"
False
*Main Lib> CharList "benney" == CharList "benney"
True

[Char] をとり、CharList を返す値コンストラクタとなっている。

一方、getCharList の型宣言

*Main Lib> :t getCharList
getCharList :: CharList -> [Char]

CharList をとり、[Char] を返す関数となっている。

これは、2種類の型の間の変換だと考えることができる。

newtype を使って型クラスのインスタンスを作る

タプルをFunctorのインスタンスにしたいと考える。

fmap をタプルに作用させて第一要素を変更するようにしたい。 fmap (+3) (1,1) と書いたら (4,1) になるように。

Maybe の場合、型引数が1つなので Functor のインスタンスにするのはすんなりいけるが、 (a,b) の場合、fmap が作用するのは型 a の部分だけだよーと指定するのはできない。

この制限を回避するために、タプルを newtype して2つの型引数の順番を入れ替える。

newtype Pair b a = Pair { getPair :: (a, b) }

これをインスタンスにする

instance Functor (Pair c) where
    fmap f (Pair (x, y)) = Pair (f x, y)

newtype で作った型にはパターンマッチが使える。

実行

*Main> getPair $ fmap (*100) (Pair (2, 3))
(200,3)
*Main> getPair $ fmap reverse (Pair ("london calling", 3))
("gnillac nodnol",3)

所感

モノイドの章なのにモノイドという単語が出てこなかった

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part76

アプリカティブの便利な関数

Control.ApplicativeliftA2という関数がある。

liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

1つの関数を2つのアプリカティブ値に適用する関数。 これを、

liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)

のように解釈すると、

通常の2引数関数を、2つのアプリカティブ値を引数に取る関数に昇格させる関数

とみなすことができる。

2つのアプリカティブ値からそれらの返り値をリストとして内包する1つのアプリカティブ値を組み立てることができる。

Just 3Just 4 を使って実践する。

まず、Just 4Just [4] にする。

*Main Control.Applicative> fmap (\x -> [x]) (Just 4)
Just [4]

Just [3,4] を作る

*Main Control.Applicative> liftA2 (:) (Just 3) (Just [4])
Just [3,4]

これと同じ

*Main Control.Applicative> (:) <$> (Just 3) <*> (Just [4])
Just [3,4]

アプリカティブ値のリストをとって、 リストを返り値として持つ1つのアプリカティブ値を返す関数を作る

sequenceA' :: (Applicative f) => [f a] -> f [a]
sequenceA' [] = pure []
sequenceA' (x:xs) = (:) <$> x <*> sequenceA' xs

実行

*Main Control.Applicative> sequenceA' [Just 1, Just 2]
Just [1,2]

こう書くのと同値

*Main Control.Applicative> (:) <$> Just 1 <*> sequenceA' [Just 2]
Just [1,2]

さらに評価を進めるとこう

*Main Control.Applicative> (:) <$> Just 1 <*> ((:) <$> Just 2 <*> sequenceA' [])
Just [1,2]

sequenceA' []Just [] になる

*Main Control.Applicative> (:) <$> Just 1 <*> ((:) <$> Just 2 <*> Just [])
Just [1,2]

最終的にこう

*Main Control.Applicative> (:) <$> Just 1 <*> Just [2]
Just [1,2]

sequenceA' は畳み込みを使っても実装できる

sequenceA'' :: (Applicative f) => [f a] -> f [a]
sequenceA'' = foldr (liftA2 (:)) (pure [])

実行

*Main Control.Applicative> sequenceA'' [Just 3, Just 2, Just 1]
Just [3,2,1]
*Main Control.Applicative> sequenceA'' [Just 3, Nothing, Just 1]
Nothing
*Main Control.Applicative> sequenceA'' [(+3),(+2),(+1)] 3
[6,5,4]
*Main Control.Applicative> sequenceA'' [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
*Main Control.Applicative> sequenceA'' [[1,2,3],[4,5,6],[3,4,4],[]]
[]

Maybe 値のリストに対して使うと、すべての返り値をリストとして含んだ単一のMaybe値ができる。 ただし、元のリストの中のMaybe値のいずれかがNothingだった場合、結果もNothingになる。

これを使う場面として、 Maybe値のリストがあり、要素のすべてがNothingでない場合に限ってなにかしたい場合に使う。

*Main Control.Applicative> map (\f -> f 7) [(>4),(<10),odd]
[True,True,True]
*Main Control.Applicative> and $ map (\f -> f 7) [(>4),(<10),odd]
True

sequenceA'' を使う

*Main Control.Applicative> sequenceA'' [(>4),(<10),odd] 7
[True,True,True]
*Main Control.Applicative> and $ sequenceA'' [(>4),(<10),odd] 7
True

sequenceA を [] に対して使うと、リストのリストをとり、リストのリストを返す関数になる。

*Main Control.Applicative> sequenceA [[1,2,3],[4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]
*Main Control.Applicative> [[x,y] | x <- [1,2,3], y <- [4,5,6]]
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]

I/O アクションに対して使う

*Main Control.Applicative> sequenceA [getLine, getLine, getLine]
hey
foo
hoo
["hey","foo","hoo"]

所感

最後の方が難しい。理解しきれてない部分がある。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part75

Zipリスト

ZipList という型があり、これは Applicative のインスタンスである。

instance Applicative ZipList where
    pure x = ZipList (repeat x)
    ZipList fs <*> ZipList xs = ZipList (zipWith (\f x -> f x) fs xs)

<*> は1つ目の関数を1つ目の値に、2つめの関数を2つ目の値に、... と適用する。 zipWith の仕様上短い方のリストに合わせた結果となる。

pure は、引数をとってそれを無限に繰り返すリストを返す。 この定義なら pure f <*> xsfmap f xs が等しい。という法則を満たす。

仮に pure x = ZipList [x] だった場合、 pure (*2) <*> ZipList [1,5,10] の結果は ZipList [2] になってしまう。

2つの有限リストを zip した結果の長さは短い方の長さになるため。 一方、無限リストと有限リストを zip したときの結果のリストの長さは、 有限リストの長さとなる。

ZipList a 型は Show インスタンスをサポートしていないので、 getZipList を使って生リストを取り出す必要がある。

*Main Control.Applicative> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100,100]
[101,102,103]
*Main Control.Applicative> getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [100,100..]
[101,102,103]
*Main Control.Applicative> getZipList $ max <$> ZipList [1,2,3,4,5,3] <*> ZipList [5,3,1,2]
[5,3,3,4]
*Main Control.Applicative> getZipList $ (,,) <$> ZipList "dog" <*> ZipList "cat" <*> ZipList "rat"
[('d','c','r'),('o','a','a'),('g','t','t')]

(,,)\x y z -> (x,y,z) と同じ。

zipWith 以外にも zipWith3zipWith7 までの関数がある。

所感

pure の定義の発想が自分では到底思いつかないようなものだった。自分で書いていけるか不安になった。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る

すごいH本 part74

IOはアプリカティブファンクター

IO は Applicative

instance Applicative IO where
    pure  = return
    a <*> b = do
        f <- a
        x <- b
        return (f x)

return は何もしない I/Oアクションを返す。

IOに関する <*> 演算子は、 2つのI/O アクションを1つに糊付けするにあたって逐次実行という意味が加わっている。

myAction :: IO String
myAction = do
    a <- getLine
    b <- getLine
    return $ a ++ b

これをアプリカティブスタイルで書ける。

myAction :: IO String
myAction = (++) <$> getLine <*> getLine

getLine は実世界から文字列をとってきてくれる小さな箱。

(++) <$> getLine <*> getLine は、 小さな箱を2つ実世界に送り出して端末から入力行を取得し、 結合して返してくれる大きな箱が作られる。

つまり、式 (++) <$> getLine <*> getLine は I/O アクションである。

よって、このように書くことができる。

main = do
    a <- (++) <$> getLine <*> getLine
    putStrLn $ "The two lines concatenated turn out to be: " ++ a

関数もアプリカティブファンクター

関数 (->) r

instance Applicative ((->) r) where
    pure x = (\_ -> x)
    f <*> g = \x -> f x (g x)

pure は、引数を無視して常にその値を返す関数 を作る。

*Main> :t (+) <$> (+3) <*> (*100)
(+) <$> (+3) <*> (*100) :: Num b => b -> b
*Main> (+) <$> (+3) <*> (*100) $ 5
508

引数を (+3)(*100) に渡し、+ を使う関数ができる。

所感

関数のアプリカティブファンクターの処理順が、<*><$> の順番になるのは何故なんだろうか。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

  • 作者: MiranLipovaca
  • 出版社/メーカー: オーム社
  • 発売日: 2017/07/14
  • メディア: Kindle版
  • 購入: 4人 クリック: 9回
  • この商品を含むブログを見る