さて、前回で Haskell の開発環境を構築できたので本編に入っていきたいと思います。
プログラミング言語の入門記事としては、基本データ型などから入るのがセオリーですが、そうした話題は大切ではあるものの同時に退屈でもあります。プログラマとしてある程度経験を積んでいれば、そうしたものはコードを読めば大体判断できますし、必要になってからあらためて理解すれば十分でしょう。
そのようなわけで、あえて関数から説明に入ってみたいと思います。
関数の基本
Swift で足し算をする関数のコードは次のように書けます。
func add(x: Int, y: Int) -> Int { return x + y } // 呼び出し add(x: 1, y: 2) // => 3
一方、Haskell では次のように書きます。
add :: Int -> Int -> Int add x y = x + y -- 呼び出し add 1 2 -- => 3
見た目がかなり異なりますが、比較すると次のことが読み取れるでしょう。
- 引数と戻り値の型は
->
で連結して記述する。 - 引数ラベルは存在しない。
- 呼び出すときは
スペース
で区切る。
少し詳しく見ていきましょう。
関数シグネチャの宣言
先ほどの Haskell のコードで1行目は関数シグネチャ、すなわち関数がどういう型を持つのかを定義しています。
add :: Int -> Int -> Int
これは「add
は Int
と Int
を受け取り Int
を返す関数である」と読むことができます。
引数と戻り値の区別がされていないことに違和感を持つかもしれませんが、これは Haskell の関数がデフォルトでカリー化されており「どこまでが引数であるか?」を考えることにあまり意味がないからです。その点については後述します。
なお、Swift では関数シグネチャの型を省略することができませんが、実は Haskell では省略可能となっており、関数の定義から型推論が可能であればこの行は無くてもコンパイルできます。
とはいえ、人間が読む際は型シグネチャがあったほうが分かりやすいので、一般的には記述するのが基本になっていますし、この連載記事でも記述するようにしていきます。
関数適用
ここまで「関数の呼び出し」と表現してきましたが、関数型言語では関数を適用するという表現がよく使用されます。
例えば f(x)
というコードについて、関数 f を x に適用すると表現したりします。動作的にはどちらも変わらないのですが、Haskell の入門書などでも適用という言葉が使われていますし、ここからはこちらの言葉を使いたいと思います。
さて、関数適用は add 1 2
というとてもシンプルな記述で行われています。
純粋型関数型言語として関数が主役な Haskell ではスペースが関数適用(関数呼び出し)を表すようになっています。Swift のように引数ラベルといったものも使用できないため、慣れないうちは非常に読みづらいかもしれません。
ここで察しの良い方は、引数の間も スペース
で区切られているのは構文としておかしいのではないか、と疑問を持たれるかもしれません。これについては本記事の後半で触れていきます。
where / let - in
Swift では大きめの関数を定義する際、計算の途中結果を一時的に格納するローカル変数などを用いたりします。
func calc(_ x: Int, y: Int) -> Int { let a = x * 100 let b = y * 10 return a + b } calc(1, 2) // => 120
Haskell では where
を使って、一時的な変数や関数を定義できます。
calc :: Int -> Int -> Int calc x y = a + b where a = x * 100 b = y * 10
あるいは let-in
式を使った書き方も利用できます。
calc :: Int -> Int -> Int calc x y = let a = x * 100 b = y * 10 in a + b
where vs let-in
where
は必要な計算を後でで記述するスタイル、let-in
式は事前に記述するスタイル、とも表現できるかもしれません。
それぞれ声に出して読んでみると分かりやすいかもしれません。where
の定義では・・・
calc x y = a + b where a = x * 100 b = y * 10
「calc x y
は a + b
である。なお、a
は x * 100
、b
は y * 10
である。」と読むことが出来ます。
一方、let-in
では・・・
calc x y = let a = x * 100 b = y * 10 in a + b
「calc x y
は、a
が x * 100
で b
は y * 10
であり、結果は a + b
である。」と読むことが出来ます。
命令形言語に慣れていると let-in
式のほうが読みやすく感じるかもしれませんが、主要な計算式(ここでは a + b
)が最初に現れるため、Haskell では where
を使った記述のほうが読みやすいという人が多いようです。
ここまで書くと where
のみを使えば十分に聞こえますが、where
は関数のスコープに限定されるという制限があります。それに対して let-in
式は、ただの式なのでどこでも記述できます。
とはいえ、最初は where
だけを覚えておけば十分かもしれません。
レイアウトルール
どちらもインデントを合わせることが重要であることを覚えておきましょう。例えば、以下のコードはコンパイルエラーとなります。
calc :: Int -> Int -> Int calc x y = a + b where a = x * 100 b = y * 10 -- compile error (parse error on input ‘b’)
Haskell ではレイアウトルールと呼ばれるものによってコードの書き方が規定されているため、インデントに誤りがあるとコンパイルエラーとなります(この点は同じくインデントベースである Python に似ているかもしれません)。
{}
と ;
を使った書き方もありますが、あまり一般的に使用されないためここでは割愛します。
ローカル関数
前述のコードではどちらも計算結果の変数のみを定義していましたが、ローカル関数を定義することも可能です。
where
の例のみ記載します。
add :: Int -> Int -> Int add x y = f x y where f a b = a * 100 + b * 10
必要であれば型注釈をつけることもできます。
add :: Int -> Int -> Int add x y = f x y where f :: Int -> Int -> Int -- 型宣言を追加 f a b = a * 100 + b * 10
型注釈
さて、ここまでで勘の良い方は気づかれたかもしれませんが、Swift で型注釈をする際に : 型
と記述するのに対して、Haskell では :: 型
と記述します。
// Swift での型注釈 let message: String = "Hello" let add: (Int -> Int) -> Int = { $0 + $1 }
-- Haskell での型注釈 message :: String message = "Hello" add :: Int -> Int -> Int add x y = x + y
:
の数や、Swift では殆どの場所で書けるのに対して Haskell では記述する場所に制限があるなどの違いはありますが、どちらもコンパイラによる型推論のヒントまたは可読性のために記述する点は一緒です。
if 式
ここまで見てくると、次に if-else
の書き方を知りたくなってくるのがプログラマというものでしょう。引数に渡された数値について、正の数だった場合に True
、負の数だった場合に False
を返すコードを見比べてみます。
Swift ではこうですね。
func isPositive(_ x: Int) -> Bool { if x >= 0 { return true } else { return false } }
Haskell では次のようになります。
isPositive :: Int -> Bool isPositive x = if x >= 0 then True else False
一行にまとめて書いても大丈夫です。
isPositive x = if x >= 0 then True else False
いくつか違いが見えてくるでしょう。
- Haskell における
Bool
は大文字始まりのTrue
とFalse
である。 - Haskell では
if-then-else
という構文で記述する。 - Haskell では式となっており結果を返している。
Swift における if
は文であるため結果を返すことはできません。一方、Haskell では if
は式であるため値を返します。そのため Haskell では else
を省略する、すなわち値を返さないコードは許可されていません。
勘の良い方はお気づきかと思いますが、Haskell での if-then-else は Swift での三項演算子に当たると考えられます。どちらも一行で記述して比較してみると分かりやすいでしょう。
// Swift x >= 0 ? true : false
-- Haskell if x >= 0 then True else False
ガード
条件が複数あって場合分けをするような場合、すなわち if-elseif に相当するコードはどのようになるのでしょうか。得点に応じて評価の文字列を返す関数を定義してみます。
Swift では次のようになるでしょう。
func judege(_ x: Int) -> String { if x == 100 { return "Perfect" } else if x > 90 { return "Great" } else if x > 50 { return "Good" } else if x > 30 { return "Bad" } else { return "Poor" } }
どうやら著者は音ゲーマーであると推論できそうですが、それはさておき Haskell ではこのような場合にガードという記法が用意されています。
judge :: Int -> String judge x | x == 100 = "Perfect" | x > 90 = "Great" | x > 50 = "Good" | x > 30 = "Bad" | otherwise = "Poor"
どちらも上から順番に評価され、条件に合致したものが評価されます。otherwise
は Swift での else
に相当するものです。
なお、ここでは =
の位置を揃えていますが、これはインデントではないため揃えなくてもコンパイルエラーになりません。しかし、Haskell ではこうした記号の位置を揃えることが多いため、この連載記事でも基本的に揃える形式で記載していきます。
カリー化・部分適用
単純な構文の比較もそろそろ飽きてきた頃でしょう。そろそろ関数型プログラミングらしいトピックをこのあたりで入れて読者のモチベーションを保つのに努めたいものです。
さて、カリー化という言葉を聞いたことがあるでしょうか?Swift が発表された当時から学んでいる方や、関数型に興味を持たれている方はご存知かもしれません。
カリー化とは複数の引数を受け取る関数を、1つずつ引数を受け取れる関数にすることです。といっても文章にしても何やら分からないと思うので Swift のコード例を見ていきたいと思います。
以下は何度も登場している add
関数ですが、この関数は引数を2つを同時に渡さないと機能しません。
func add(_ x: Int, _ y: Int) -> Int { return x + y } add(1) // compile error
それは当たり前だと感じるかもしれませんが、Swift ではクロージャを利用することで引数を順番に適用できるようになります。
func add(_ x: Int) -> ((Int) -> Int) { return { y in x + y } } let f = add(1) // 新たな関数が返る f(2) // => 3
このようにクロージャを利用して関数の結果として新たな関数を返すようにすることで、引数を1つずつ適用できる関数を作れます。このように変更することをカリー化と呼び、新しいバージョンの関数 add
はカリー化された関数などと表現されます。
さて、このコードでは最初に add(1)
を呼び出し、返却された関数を変数 f
に代入しています。このように引数の一部を埋めることを部分適用と言ったりします。引数全体を適用していないから部分適用なのですね。
この例ではシンプルすぎて無駄にコード量を増やしてトリッキーなことをしているだけに感じるかもしれませんが、カリー化や部分適用は非常に有用なテクニックで、それを活用した Swift のライブラリなども存在します。Argo などが有名どころでしょうか?
さて、Haskell の場合はデフォルトで全ての関数がカリー化されています。add
関数を再掲します。
add :: Int -> Int -> Int add x y = f x y
()
で優先順位を明確にすると、以下のようになっています(Swift のコードと対比させると分かりやすいですね)。
add :: Int -> (Int -> Int) add x y = f x y
部分適用について、Swift コードに対応させて書くと次のようになります。
calc :: Int calc = f 2 -- => 3 where f = add 1
ここまでくると、関数適用がスペースであるにも関わらず add 1 2
といったように引数もスペースで区切れている理由がわかるかと思います。
()
で優先順位を明示すると ((add 1) 2)
という計算が行われているのです。Swift では add(1)(2)
という書き方に対応することになります。
さて、関数定義の説明のところで、引数と返り値の区別があって無いようなものだと記載しましたが、カリー化と部分適用に触れることで、その理由が少し分かってきたのではないでしょうか?
add 1
は Int -> Int
な関数を返し、add 1 2
は Int
を返しますし、単に add
としたときは Int -> Int -> Int
な関数(つまり add
そのもの)を返す、関数であると考えることができます。すなわち、いくつの引数を部分適用するかによって返り値は変わってくるとも言えますし、引数と返り値の区別は無いものだと考えることも出来ます。
まとめ
といったところで今回のまとめです。
- 関数シグネチャは
add :: Int -> Int -> Int
のように記述する。 - 本体は
add x y = x + y
のように記述する。 - 関数適用は
add 1 2
のように記述する。 where
でローカルな変数や関数を記述できる。let-in
式でも同様のことができる。:: 型
で型注釈を付けられる。if-then-else
は式で、Swift の三項演算子にあたる。- 条件が複数ある場合にはガード構文を利用できる。
- カリー化は複数の引数を受け取る関数を、1つずつ引数を受け取れるようにすること。
- カリー化された関数に一部の引数だけを与えることを部分適用という。
- Haskell では全ての関数はカリー化されている。
- そのため関数の引数と返り値の違いは曖昧になっている。
次回はデータ型の宣言やパターンマッチなどに触れていく予定です。
あとがき
1,500円でプロギアの嵐が無限クレジットで遊べる Capcom Arcade Stadium は最高ですね。
Written by tobi462.