hnwの日記

Haskellのカッコいいところを紹介してみる

あけましておめでとうございます。本年もよろしくお願いいたします。


唐突ですが、Haskellを少しやってみたので、その感想を書いてみます。


僕は正月休みにHaskellProject Eulerに挑戦していました。これは、数学っぽい問題をプログラムを書いて解いていくサイトです。数学は再帰的な定義が多いため関数型言語が向いているだろうと考え、ほぼ未経験のHaskellを試してみました。


解けそうな問題から50問解いてみましたが、随分スラスラ書けるようになってきた気がします。また、書いていると不思議と気持ちがいい言語だという印象を受けました。気持ちよさは複合的なものだと思いますが、その要因になっていそうな特徴を紹介してみます。

数学や英語の知識で「読める」表現が多い

いきなり印象論なんですが、Haskellのコードは初見でもそこそこ読める気がします。中の人が読みやすさを大事にしているためだろうと想像しています。


例えば、Haskellでは2引数関数をバッククォートで囲むことで、中置することができます。mod関数について言うと、次の2つは同じ結果になります。

x = mod 5 2
-- x == 1
y = 5 `mod` 2
-- y == 1


最初に見たときは「バッククォートは無いわー」と思っていましたが、慣れてくると読みやすく思えてきます。何より、中置した方が読みやすい関数を中置できるのは良い仕組みだと思います。


また、標準モジュールData.Functionに含まれるon関数も僕は好きです。利用例は次のようなものです。

import Data.List
import Data.Function
xs = sortBy (compare `on` snd) [(4,4),(1,2),(3,1)]
-- xs == [(3,1),(1,2),(4,4)]


sndというのはペア(=2-タプル)の後者を取り出す1引数の関数です(secondの略でしょう)。「compare on second」と音読できるのが素晴らしいと思います。


詳しい説明も書いておきます。sortByの第一引数にはソート関数を指定します。compareというのは2引数を受け取って大きいか小さいか等しいかを返す関数です。上の例ではon関数を用い、ペアを2つ受け取ってペアの後者の値同士で比較する関数を作り出し、ソート関数として利用しています。

文字列がリストで実現されている

Haskellでは、文字列は文字のリストとして表現されています。そのため、たとえば文字列を反転させるのにリストの要素を反転させるreverse関数が使えます。

x = reverse [1,2,3]
-- x == [3,2,1]
y = reverse "123"
-- y == "321"


Perlのreverse関数のようにコンテキストによって挙動を変えるのでなく、文字列もリストも同じ処理で反転できるわけです。他にも文字列処理にリスト処理の関数が使えるため、覚えることが他の言語より少ないように感じます。

リスト内包表記が簡潔

リスト内包表記というのは次のような書き方のことです。

xs = [ x^2 | x <- [1..10]]
-- xs == [1,4,9,16,25,36,49,64,81,100]


これだけで1から10までの平方数のリストが作れます。シンプルな割に十分読める表現ですよね。


これは数学で集合を表現する場合と非常に良く似た書き方です。実は、ここでの「<-」は数学記号の「∈」を意味しているそうです。「ちょっと無理があるだろ…」という気もしますが、覚えておくと読み書きしやすいはずです。

ラムダ式が簡潔

ラムダ式(無名関数)が非常に短く書けるのも特徴的だと感じます。map関数を使って、先ほどのリスト内包表記と同じリストを作ってみましょう。

xs = map (\x -> x^2) [1..10]
-- xs == [1,4,9,16,25,36,49,64,81,100]


map関数の第1引数がラムダ式になっています。xを受け取ってx^2を返すものですが、非常にシンプルだと思いませんか?言語によってはlambdaとかfunctionとか書く必要があるわけですが、あれって面倒だと思うんですよね。


ちなみに、なぜラムダ式の先頭がバックスラッシュなのかというと、中の人としてはλのつもりなんだそうです。「いくらなんでもそれは無いだろ…」という気もしますが、心意気だけは受け取っておきましょう。

無限リストを作りやすい

Haskellの処理は基本的に遅延評価されます。つまり、値が必要になってはじめて値の評価が行われます。このため、処理によっては効率を保ちつつ綺麗に書けることがあります。


フィボナッチ数列を例として考えてみましょう。フィボナッチ数列とは次のように定義される数列です。

  • F1 = 1
  • F2 = 1
  • Fn+2 = Fn + Fn+1


Haskellでフィボナッチ数の無限リストを書くと次のようになります。

fibs = 1 : 1 : zipWith (+) fibs (tail fibs)


:はリストの区切りを意味します。fibsは数値のリストであり、fibsの第1要素は1、第2要素は1です。


驚くべきことに、第3要素以降の定義に自分自身が登場しています。zipWithは第2引数と第3引数の対応する要素を第1引数の要素で評価して新しいリストを作る関数です。第3引数の(tail fibs)はfibsの2項目から始まるリストを返します。つまり、fibsの3項目はfibsの1項目と2項目を足したもの、4項目は2項目と3項目…というように、定義通りの書き方になっていることがわかります。


また、この書き方は効率も悪くありません。再帰で素直に書くと呼び出し回数が爆発しがちですが、この書き方ではフィボナッチ数列の各項は一度しか計算しません。

型システムが強力

Haskellは静的型付け言語ですので、おかしな型が与えられた場合には実行前に検出することができます。


その一方で、カジュアルに書く分には自分の書いた関数の型宣言を省略することができます。これは、型を省略した場合には勝手に型を付けてくれる型推論機構のおかげです。静的型付けのメリットと型宣言を省略できる楽チンさが両方手に入るわけです。


関数を定義してエラーが起きた場合も、その関数や途中経過の型推論の結果を見れば間違いがわかったりします。


まれに型推論できないこともありますが、そのときは自分で型を付けてやればOKです。

まとめ

いろいろ書いて来ましたが、Haskellの根源的な楽しさは再帰の楽しさのような気がします。それをサポートする機能が充実していたり、読みやすかったりするから余計に楽しく感じるということかもしれません。


本稿では触れませんでしたが、パターンマッチや関数合成、またmap、filter、foldlなどリスト操作を簡潔に書ける関数の多さも良い特徴だと思います。


初心者がつまづきそうな点として、エラーが出たときに何で怒られてるのかわかりにくい点があると思います。僕の印象では、8割方のエラーが評価順序の勘違いによる型の不整合エラーなんですが、どこでミスったか探すのに少し苦労したりします。本当に初心者の人は、周りに聞く人がいた方がいいかもしれません。


また、変数の値を書き換えられないのが制約として厳しすぎるように感じる人がいるかもしれません。これについては頭を再帰脳に切り替えるしか無いと思います。


練習問題がHaskellにマッチしていた面もあるとは思いますが、Haskellのコードを書いてるとアドレナリンが出るような気がします。この記事からその楽しさが伝わったらいいなあと思っています。