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

純粋関数型言語 Haskell 基礎まとめ

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

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

Haskellを学ぶ上で難しいのは、その概念が高度に抽象化されている点にある*1。そのため、矛盾の無い説明が難しい。自分なりに解釈した上で説明すると、どこかに矛盾が入り込み、説明しづらい。それが読み手にとって理解を難しくしていると思う。それと、何が嬉しいのか、が分からない。これは、様々な型インスタンスを使った事例を読む必要がある。現時点の自分なりの主観と選択が入った説明であれば、以下のようになる。

遅延評価

Haskellでは値が具体的に必要になるまで評価しない。標準関数を使ったxs = repeat xは、全ての要素がxの無限リストを作成して、それをxsに束縛するが、これはエラーにならない。head xsはこの無限リストの先頭の要素を返す。length xsは無限リストの要素数を評価する必要があるので、これは計算が終わらない。このようにHaskellでは、本当に必要となるまで、出来る限りその場では評価せずに遅延させる。*2

カリー化

ある多引数関数の一部の引数のみを部分適用することで、その関数を元にした新たな関数を作ることができる。 仮に、多引数関数が禁止されている世界*3で、多引数関数を実現しようとすると、その世界では引数を一つずつ適用していくしかない。 そうやって、部分的に実引数が渡された新たな関数を作りながら、最終的に全ての引数を適用した結果を取得することができる。 カリー化というのは、多引数関数の引数を先頭のものから一つずつ適用していくことで、部分適用された関数を取得していきながら、最終的に結果を取得することができるような手法を概念として表したようなものと考えることができる。 Haskellの関数の型表記において、'->'が引数と戻り値で区別なく使われている点を見てもそのことが分かる。どのような関数であっても引数の先頭から一つずつ適用するしかないからだ。

Prelude> :t (*)
(*) :: Num a => a -> a -> a
Prelude> :t (* 10)
(* 10) :: Num a => a -> a
Prelude> :{
Prelude| let myfunc :: Int -> Int -> Int -> Int
Prelude|     myfunc x y z = x + y + z
Prelude| :}
Prelude> :t (myfunc)
(myfunc) :: Int -> Int -> Int -> Int
Prelude> :t (myfunc 1)
(myfunc 1) :: Int -> Int -> Int
Prelude> :t ((myfunc 1) 2)
((myfunc 1) 2) :: Int -> Int
Prelude> :t (((myfunc 1) 2) 3)
(((myfunc 1) 2) 3) :: Int

文脈

Maybe型は、計算が失敗するかもしれない、という「文脈」のついた値を示す型である。計算が成功した場合はJustにくるんで、例えばJust 10を返し、計算が失敗した場合はNothingを返せばよい。もしMaybe型を扱うことができる関数があれば、計算が失敗したかどうかを、C言語で返り値がNULLであるかを分岐でチェックするような、手続き的な処理を書く必要がなく、統一的に扱うことができる。

IO型は、外の世界(副作用の存在する世界)という「文脈」のついた値を示す型である。 リスト[]は、非決定性という「文脈」がついた値を示す型、と考えることもできる。複数の値を持っており、そのどれが値なのかが決定的でない、という意味である。

このように、Haskellには様々な「文脈」を表す型が存在し、同時にそれらを統一的に扱う手法も提供されている。

参照透過性

Haskellは純粋関数型言語なので、副作用のある関数は許さない。ある関数にある実引数を適用した場合、その結果は常に同じである、という「参照透過性」があることを前提とする。

しかし、それだとIOが処理できない。実行するたびに関数が返す結果が変わる可能性があるからである。

そのために文脈(型)IOを使う。IO型は、「副作用がある」という文脈を値に与える。とにかくIOを含む処理は、IO型にまかせる必要があり、その値は文脈を考慮したまま計算に使用する必要がある。

ファンクター

ある文脈の付いた値に対して、文脈の付かない値を引数にとる通常の関数が適用できる方がよい。例えばMaybe型の値Just 10に対して(*10)を適用することで、Just 100が手に入る方が、Maybe用の非常に良く似た*maybeみたいな、内部で文脈を解釈する関数を用意する必要がなく、使い勝手が良い。そのためには、Maybe型にfmap関数を定義して、fmap (*10) Just 10とすることで望み通りJust 100が手に入るようにすれば良い。また、これはMaybe型に限った話ではなく、他の文脈を表す型でも同じである。そのために抽象化した上位の型として、ファンクタ型Functorがある。MaybeやIO、リストはFunctorのインスタンスであるためfmapが定義されており、このfmapを使うことによって、文脈の付いた値を文脈を考慮しない関数に適用することができる。

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

今度は(*10)ではなく[(*10)]を適用したいとする。リストが関数を要素として持っており、それをなんらかの文脈の付いた値に適用したい場合である。文脈から関数を取り出して、それをファンクタのように、ある文脈の付いた値に適用し、そして文脈を付けて返す。

そのためにアプリカティブファンクタ型Applicativeは、<*>を定義*4している。この演算子は、例えばリストではfs <*> xs = [ f x | f <- fs, x <- xs ]とリスト内包表記を用いて定義されており、これはリストから関数を取り出して、それぞれの関数をリストの値に適用する。

Prelude> :m Control.Applicative
Prelude Control.Applicative> (myfunc) <$> [1,2,3] <*> [10,20,30] <*> [1,-1,0]
[12,10,11,22,20,21,32,30,31,13,11,12,23,21,22,33,31,32,14,12,13,24,22,23,34,32,33]

なお、(myfunc) <$>fmap myfuncと等価である。良く使う表現であるため、演算子<$>が定義されている。

Prelude Control.Applicative> :t (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
Prelude Control.Applicative> :t (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b

モノイド

モノイドはファンクタやアプリカティブファンクタと異なり、具体型をインスタンスとする型クラスである。

memptyは単位元を示す定数*5である。単位元とは、*であれば1、+であれば0、である。 mappendは同じモノイド同士を'二項演算'して新たなモノイドを返す関数を定義する。かなり抽象的な表現だが、appendとあるにもかかわらず型によっては'結合'とは言えない演算もあるので、このように表現している。 mconcatはデフォルト実装が与えられており、foldr mappend m aである。複数のモノイドから、単一のモノイドを作る。

モナド

モナドは、ある値をある文脈に入れて返す関数f :: a -> m bを相手にする。これが、ファンクタやアプリカティブファンクタと異なる点である。ある文脈の付いた値m aを、その関数に適用することを考える。そのためにモナドMonadは、バインドと呼ばれる>>=を定義している。

Prelude> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m 

また、Monadreturnを定義している。これは、他言語におけるreturnとは全く違い、Haskellreturnは値をモナドに包んで返す関数、である。

Prelude> :t (return)
(return) :: Monad m => a -> m a

Maybeやリスト、IOなどは全てモナド型の型インスタンスである。

モナドが嬉しいのは、ある文脈の付いた値が複数あるときに、それらに対して次々に関数を適用していけるところである。

Prelude> Just 10 >>= \x -> return (x*10) >>= \y -> return (y+10)
Just 110

do記法

モナドを扱うための、より表現力のある記法を構文糖衣として提供する。doブロックでは、演算子<-を使ってモナドから値を取り出して変数に束縛できるので、命令型(手続き型)のスタイルでコーディングができる。doブロックの最後の式の結果が、doブロック全体の結果となる。

do
    x <- Just 10
    y <- return (x*10)
    z <- return (y+10)
    return z
Prelude> do { x <- Just 10; y <- return (x*10); z <- return (y+10); return z }
Just 110

その他の演算子

<*><$>>>=<-->の他に頻出の演算子として$.がある。$は単なる関数適用である。ただし、最低の優先度で、右結合になるので、つまり右辺を先に評価しろ、という指令となり、優先順位を変える()を右側に付けるのを省略することができる。.は関数合成であり、( f . g . h ) a = f ( g ( h a ) )である。map(fmap)などの高階関数に渡す際に便利である。

c.f.

このサイトの絵を見て、それ以上でも以下でもないところに、なるほど、と思えるようになったら、それがモナドを理解した一つの指標になるのではないかな、と思う。

モナド - ウォークスルー Haskell

*1:どのくらい抽象度が高いかというと、ハトとイワシは、二つの目と一つの口を持つ点が共通している、と言ってしまうくらい。魚類と鳥類だから全然違う生き物じゃないか、と思ってしまいそうなものだが。

*2:なおlengthはリストの要素数だけに興味があるので、実際には各要素そのものの評価は行われない。これは、例えばx=1/0であったとしてもエラーにならないことを意味する

*3:レジスタの特別な制限などにより!

*4:言葉の意味が重複しないように説明すると、これは抽象メソッドのようなものであると言える。つまり、インターフェースを与えているのだ。ただ、具体的な実装が与えられる場合もあるし、型インスタンスの方で実装しなければいけない場合もある。なお<*>は、具体的な実装は与えられていない

*5:引数の無い関数は定数と同じように扱える