Posted at

Haskellの関数の型とかカリー化とか #Haskell


Haskellの関数の型とか

初心者への入門記事のような感覚で書きます。

本当は型クラスとかの話まで網羅した初心者向けの型全般の記事にしたかったのですが、カリー化の話だけで長くなってしまいました。


Haskellの型

Haskellのデータ型には以下のものがあります。


標準データ型


基本データ型


  • Bool

  • Maybe a

  • Either a b

  • Ordering

  • Char

  • String

  • Tuples


数値


  • Int

  • Integer

  • Float

  • Double

  • Rational

  • Word

他にもリストやモナドがありますが、話がすこし難しくなってしまうためこの段階ではここまでにしておきます。


関数の型

まずはBoolを例にして話をすすめていきたいと思います。

BoolというのはTrueとFalseの2種類の値を持っている型です。

こういった、全ての値が列挙して定義されているような型を列挙型と言います。

Boolに関係する代表的な関数として、(&&)(||)が存在します。

これは他のプログラミング言語を使ったことがある人ならすぐに理解できると思いますが、論理和と論理積を求める演算子です。

Haskellの大きな特徴として、Haskellの演算子は全て関数であるというものがあります。

つまり、先ほどの説明を言い直すと、

(&&)(||)は論理和と論理積を求める2引数関数です。

となります。

多分、この段階で「は?」となる点が2点あるかと思います。

それは、

「関数ってことはLispみたいに前置で演算子を書かないといけないのかよ?」

という点と、

「さっきから&&を囲んでるその括弧は何だよ、毎回それ書かないといけないのかよ?」

の2点かと思います。

この2点に対して、同時にお答えしたいと思います。

Haskellの関数の中で、関数名が記号のみで定義されている2引数関数は少し特殊な扱いを受けます。

記号のみの関数は、()をつけて定義します。

そしてこのまま、他の関数と同じように

> (&&) True True

True

のように利用する事ができます。

しかし、記号のみで定義された2引数関数は、括弧を外して中置演算子として利用する事ができます。

> True && True

True

(文字を使った関数に括弧を付けて定義しても意味はありません。しかし、文字を使った2引数関数をバッククオートで囲むと中置で使うことができます。)

では次に関数の型に関してお話します。

関数には型があります。この型を理解していないと、Haskellのエラーはまず読めないでしょう。

しかし基本は簡単です。

Haskellの型は以下のように書きます。

変数(関数)名または値そのもの :: 

また、型とは(再帰的な定義になってしまいますが)



引数の型 -> 戻り値の型

とします。

例えば、

True :: Bool

であったり、a = Trueとしていた場合、

a :: Bool

といった感じになります。

関数の型も基本的にはこれと同じです。

例えば2引数関数のfというものがあったとします。

この関数は、Bool型の値2つを取って、Bool型の結果を返すものだとします。

型の構造としては(&&)(||)と同じ気がしますね。

f :: (Bool, Bool) -> Bool

CやJavaと似たような感じですね。戻り値の型が最初に書かれるか最後に書かれるかぐらいの差でしょう。

おいこら待てや、と強いHaskellerの方々からマサカリが飛んで来そうですが、まあまあお待ち下さい。

初心者の方にはちょっと分からないかも知れませんが、こういう型の定義はHaskellではあまり使われません。

そうです。さっき(&&)(||)と同じような型、と言ったのは嘘ですごめんなさい。

話を戻しますが、さっきの(Bool, Bool) -> Boolという型定義は可能です。使うこともあります。しかし、Haskellの良さというのを結構捨ててしまう事になるので、あまり見かけません。

「じゃあどうするんだよ?」という話ですが、結論から言うと

f :: Bool -> Bool -> Bool

とやる方がHaskellらしいです。

これのどこが違うのか、という事なのですが、ここでカリー化という話が出てきます。


カリー化

カリー化とは、


複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(Wikipedia)


です。

つまりどういう事かというと、先ほどのf(元の括弧でくくっていた方です)は Bool型の引数とBool型の引数を取って、Bool型の値を返すというものでした。

これをカリー化するというのは、Bool型の値を取って、「Bool型の値を取って、Bool型の値を返す」関数を返す関数にすると良いです。

つまり、

f :: Bool -> (Bool -> Bool)

とします。

これの何が嬉しいのか、という話なんですが、見ての通り関数が返ってきます。

なので、部分適用が簡単にできるんです。

例えば、Trueとの論理和を返す関数みたいなものが欲しくなることってHaskellを使っていると結構よくあるんですよね。

ラムダ式を使って\x -> True && xとやっても良いんですが、はっきり言ってめんどいです。

(&&)ももちろんカリー化されているので、この場合、(&&) TrueもしくはTrue &&とする事でTrueとの論理和を返す関数として扱う事が出来ます。

「関数が返ってくるって、じゃあ普通に2つ引数に値を渡せないんじゃないのか?」と思った方もいるかも知れませんが、そんなことはありません。

(&&)に第1引数としてTrue、第2引数としてFalseを与える与えるとすると、



  1. (&&) True Falseとして引数を与える(わかりやすさのために前置にします)


  2. ((&&) True) Falseとなり、((&&) True)Trueとの論理和を返す関数となる


  3. Trueとの論理和を返す関数Falseを与えるので、TrueFalseの論理和が求まる

という流れとなります。

3引数関数だろうがそれ以上の関数だろうがBool -> (Bool -> (Bool ->Bool))のようにカリー化する事ができます。

ここまで来てやっと最初に提示したf :: Bool -> Bool -> Boolまで戻るのですが、最初の引数以外をくくる括弧はいりません。

つまり、3引数関数だと単純にBool -> Bool -> Bool -> Boolですね。

(ちなみに、先ほど提示した括弧で引数をくくった定義ですが、あれは2引数関数ではなく、1つのタプルを取る1引数関数ですので、2引数関数というのも嘘でした)