Haskell
AdventCalendar
関数型プログラミング
HaskellDay 4

Haskellを勉強して感動したこと・難しいと思ってること

Haskell Advent Calendar 4日目の記事です。

Haskellに入門したのは6, 7年前 ゲームが作れる言語(某モナディウス)だよ、と言われホイホイ研究室に入って学んだのが運命の出会いでした。最初は奇妙な言語でやたら頭を使わせるし聞いたことも無いしで、文句を言っていましたが学んでいくうちに次第にプログラムの美しさ合理性に惹かれていきました。残念ながら普段は僕の頭にあまる言語なので使用していませんが、後のプログラミング人生に多くの影響を残して言った感動したこと、そしてなぜ難しいと思っているかをまとめていきたいなと思います。基本的には、書籍「すごいHaskellたのしく学ぼう」の内容の抜粋になります。Haskell未経験者の方に是非、学ぶきっかけになればと思っています。(本当はHaskellのビジュアルプログラミング環境を作りたかったです・・・。時間が無くて断念)

Haskellに入門して感動したこと

Haskellを始める以前は主にC言語やJavaを使っておりvoidだらけの肥大化した関数・メソッドを書きがちでした。Haskellを学んでからは、とにかく意識をして副作用を分離し明示的に値を返すように心がけるようになりました。Haskellではあらゆるものを式にすることで、その手助けをしているのに後々気づいて感動を受けました。新しく学ぶ言語を選ぶ上で分岐、反復が式になっているものを選ぶのは当然となりました。特にリスト内包記法は、C言語で配列を用意して、カウンタ変数を用意して、間違いが起きやすい条件を決めて、と言う作業にうんざりしていた僕には奇跡のような機能に感じました。

-- if-else
> let x = 10 in if x > 5 then "big" else "small"
"big"
-- case
> let x = 2 in case x of 0 -> "zero"; 1 -> "one"; 2 -> "two"; _ -> "other"
"two"
-- リスト内包記法
> [ x*y | x <- [1..9], y <- [1..9]]
 [1,2,3,4,5,6,7,8,9,2,4,6,8,10,12,14,16,18,3,6,9,12,15,18,21,24,27,4,8,12,16,20,24,28,32,36,5,10,15,20,25,30,35,40,45,6,12,18,24,30,36,42,48,54,7,14,21,28,35,42,49,56,63,8,16,24,32,40,48,56,64,72,9,18,27,36,45,54,63,72,81]

パターンマッチ・再帰

学校で再帰を学びましたが、とにかく大嫌いでした。単なる数字を使った再帰は簡単だったのですが、配列などのデータ構造を用いた再帰は長さやindexを用いて非直感的で、どのような分岐を書いていいかサッパリわからなかったためです。リストを使ったパターンマッチは、ものすごく単純明快でした。空の場合([])と先頭の要素と残りの要素(x:xs)に分けた場合を考えるだけです。文法にリストの構造が直感的にマッピングしているので何も迷う必要はありませんでした。分岐のパターンを考えるのは同じではないかと思われるかもしれませんが、型に対するパターンの漏れがあるとコンパイラが叱ってくれます。またif-elseによる分岐ではなく、関数を場合によってぶつ切りにすることで一つずつのケースに集中して考えられるのも魅力でした。lengthでは空だったら0、先頭の要素は1という数字に変換するだけで良いのか!mapでは先頭の要素に関数を適用していくだけではないか!と再帰を考えるのが徐々に好きになり、直感的ではない添字や条件を決めるループは嫌いになっていきました。

-- > fact 5
-- 120
fact :: Int -> Int
fact 1 = 1
fact n = n * fact(n - 1)

-- > length' [1..5]
-- 5
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length xs

-- > map' (* 5) [1..5]
-- [5,10,15,20,25]
map' :: (a -> b) -> [a] -> [b]
map' _ [] = []
map' f (x:xs) = f x : map' f xs

-- 代数的データ型も手軽にパターンマッチ可能な型が手に入る本当にすごい機能です。
data MyNum = One | Two | Three

num :: MyNum -> Int
num One   = 1
num Two   = 2
num Three = 3

高階関数

再帰の書き方を一通り覚えると、プログラムの構造に共通点が見えてきます。処理そのもの、つまり関数を渡したり返してもらえば、本質のみを記述するだけでプログラムが完成します。この高階関数の抽象度の高さには多くの人が感銘を受けたかと思われます。今では多くの言語で採用されモダン言語の必須機能にまで昇華されたと思います。これは単なる便利なライブラリではなく、必ず値を返す式・関数、パターンマッチ・再帰による構造の明示化の積み重ねができてはじめて、多くの汎用性が得られる、というのが大きな魅力だと思います。また、数学由来の関数ばかりなので安心して使えますし、証明という究極の信頼方法を得れるのはHaskellの大きな魅力だと思っています。

> map show [1..10]
  ["1","2","3","4","5","6","7","8","9","10"]
> filter even [1..10]
[2,4,6,8,10]
> foldl (+) 0 [1..10]
55

型クラス

型クラスは本当に奥が深くて、まさに魔法だ・・・と感動した機能です。ほんの少しですが紹介します。魔法の多くは独自のデータ型でも自動的に得ることができます。例に混ぜ込んでおきます。また、自動的でなくても型クラスに定められたいくつかの関数を手で定義するだけで、この魔法は手に入ります。魔法は魔法でなくなり、友人からはまるで魔法使いのように見られるようになります。

Eq

等価性。たかが等価されど等価です。ユニットテストを書くようになってからは、等価の大事さが身にしみてわかるようになりました。Haskellでは非常に多くの値が比較可能です。

> [1,2,3] == [1,2,3]
True
> [1..10] == [1..10]
        True
> (1, 'a', []) == (1, 'a', [])
True
> data RGB = Red | Green | Blue deriving (Eq)
> Red == Red
True
> data Point = Point { x :: Int, y :: Int } deriving (Eq)
> Point { x = 1, y = 2 } == Point { x = 1, y = 2 }
True

Ord

等価の次は比較ですよね。良く考えると当然ですが、比較をするには等価性も同時に保証されている必要があります。

> [1,3] < [1,2]
False
> (1, 'b') < (1,'c')
True
> data RGB = Red | Green | Blue deriving (Eq, Ord)
> Red < Blue
True

Enum

既に何度か見せていますが値の範囲をしていすることで一気に値の列を取得することができます。

> [1..10]
[1,2,3,4,5,6,7,8,9,10]
> ['a'..'z']
"abcdefghijklmnopqrstuvwxyz"
> data Day = Sun | Mon | Tue | Wed | Thu | Fri | Sat deriving (Enum, Show)
> [Mon..Fri]
[Mon,Tue,Wed,Thu,Fri]

遅延評価・無限リスト

遅延評価・・・自体の感動は上手く説明できる気がしないので、遅延評価を利用した無限リストの感動を紹介します。百聞は一見にしかずですが、まさかプログラムで無限を扱えると思いませんでした。今では無限リストも採用している言語少なくはないですよね。

> take 100 ['a'..] 
"abcdefghijklmnopqrstuvwxyz{|}~\DEL\128\129\130\131\132\133\134\135\136\137\138\139\140\141\142\143\144\145\146\147\148\149\150\151\152\153\154\155\156\157\158\159\160\161\162\163\164\165\166\167\168\169\170\171\172\173\174\175\176\177\178\179\180\181\182\183\184\185\186\187\188\189\190\191\192\193\194\195\196"
> take 100 $ filter even [1..]      
[2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200]

無効値・例外を表す型

これも最近の言語でよく目にするようになりましたね。無効な値や例外を型で表す型です。手軽にチェーンでき分岐が発生しないのが感動のポイントでした。Maybe(Option, Optional)は見かけますが、例外を失敗の値として持つEitherの普及がまだまだだと思うので、これから増えていって欲しいですね。

minus5 :: Int -> Maybe Int
minus5 x = if x - 5 < 0 then Nothing else Just $ x - 5

> Just 100 >>= minus5 >>= minus5 >>= minus5
Just 85
> Just 10 >>= minus5 >>= minus5 >>= minus5
Nothing
> Right "John" >>= initJ >>= lastn >>= four
Right "John"
> Right "Joe" >>= initJ >>= lastn >>= four
Left "e last character is not 'n'"
> Right "Mike" >>= initJ >>= lastn >>= four
Left "The first character is not 'J'."
> Right "Johhhhhhn" >>= initJ >>= lastn >>= four
Left "It is not four characters."

他の言語でも変わらないこと

Haskellで学んだ知識は他の言語でも純粋な関数型をサポートしていない場合でも十分に活かせます。ごく簡単なところでは、所謂void型を避ける。void型が起きそうな部分と値を返す関数をしっかり分ける。これだけでテストのしやすさや関数の再利用性、全体の設計が綺麗になりました。またScalaz, Swiftzなど各言語の関数型猛者が一瞬で関数型ワールドを提供するライブラリを作ったりします。Swiftに関してはリリースの当日には、Swiftzが存在してびっくりした記憶があります。多くの言語で再現できる関数型のテクニックですが、やはり一番先頭を走っている言語の一つがHaskellだと思います。有効そうなテクニックは輸入され、純粋に学びたいときに一番ノイズが少なく参考にしやすい言語だと思っています。

ここで紹介したことは、Haskellの魅力のほんの一部、始まりの始まりに過ぎません。しかし、私自身が多くを学べていないことと挙げだすとキリがないので、ここでストップしておきます。

Haskellに入門して難しいなとおもってること

ひたすら褒めたので最後にHaskell難しいなと思っているを書いておきます。冒頭でも述べましたがHaskellを触り始めたのは6.7年も前ですが、入門レベル(H本の途中)で足が止まってしまい残念ながらメインに使用している言語ではありません。普段はElm、Scala、Java、Ruby等を書いています。これはHaskellが悪いところではなく私の頭が追いつけない部分なのですが、感動した箇所として説明した高階関数が難しいと思ってしまうポイントです。簡単な1つの関数を受け取る関数ぐらいで済めば問題ないのですが、受け取る関数が複雑になったり複数になったりすると途端に頭が働かなくなってしまいます。また、モジュール内でaliasが張っている複雑な関数が型に混じってくると脳内で展開が追いつかなくなってしまいます。もう一つは純粋な部分とdo記法を用いるモナドやアクションなどが出現する部分の頭の切り替えが追いつきません。学習段階では完全に純粋な部分(モナドも純粋ではありますが・・・)だけで済みますが実用となるとそうは言ってられません。すると簡単なものでも物凄く思考に時間が掛かってしまい断念してしまうケースが多くなってしまいました。積極的にHaskellのプログラミングを教えてくれる人が側にいれば話は変わったのかもしれません。また、Haskellが難しいのは当たり前とも思っています。それは他の言語より進んだ抽象化を手に入れようと邁進し、より複雑な課題を解決しているからです。Haskell問わずですが、簡単なことを簡単に複雑なことをなるべく簡単に提供する。それは複雑な事柄に対する機能なので理解や習得が難しいのは当たり前だと思います。そしてそれが良い言語の一つのあり方なんだと思っています。普段使っているScalaは手続き的に崩すことも容易なので、それが気に入って使い続け、ElmはHaskellに比べ多く機能を削いではいますが、アーキテクチャやランタイムが難しい部分を引き受けてくれるので楽しく頭をあまり使わない部分だけやっていられるので最近は好んで使用しています。しかし、どちらの言語もHaskellを学んだ背景を武器に戦っていけるので、そう言った意味では未だに心強い味方です。Haskellで思うようにプログラミングができる、というのは大きな目標であり、楽しみでもあります。以上で私のHaskellに入門して感動したこと・難しいと思っていることの、まとめを終わりたいと思います。