とある京大生の作業ログと日々の雑記

コンピュータサイエンスについて学んだことを可視化したり日々の雑記をまとめてます。

圏論がベースのプログラミング言語Haskellから様々なことを学んだ話

こんにちは!コミさん(@komi3__ )です!


前回はDeep Learningを用いて株価予測をしよう!みたいなことをやって、その導入としてQuandlを用いてのデータの入手方法を前編としてやったんですよね。

で、それで入手方法をやってあとはそれを用いて実装をしようというのが後編に回す予定だったんですが...


まあ、率直に言えば、めちゃくちゃ課題に追われててやる暇がなかったです。はい。

課題とか授業内容をほったらかしてAI関係の趣味に走りまくってたおかげで課題が溜まってまして...

この土日に株価予測モデルの実装を行う予定だったのですが、ちょっとまだ追いついてません。あしからず。


んで、まあちょこちょこ調べ物はしてて色々ブログのネタになりそうなものはあって、とりあえず1回更新しとくか〜みたいな感じで、今回はHaskellについて書こうかな、と。

ちょっとAI関係のことから逸れるかなぁって感じなんですけど、今後色んなアルゴリズムを実装してく中で今回Haskellをやってちょっと学ぶべきことが多かったので、シェアします。


なんでいきなりHaskell?みたいに思う人もいると思うんですけど、背景として、なんとなく単位稼ぎのためにノリで履修したHaskellの演習授業にかなり苦戦してた、って感じです。


では行きましょう。


てかぼくさっきからHaskellって普通に言ってますけど、皆さんHaskellを知ってますでしょうか...?

一般に、プログラムをやるってなるとオブジェクト指向型言語を扱うことがほとんどなんですけど、そのオブジェクト指向型とは大きく違って、関数型言語とかがあります。

関数型言語としてはHaskellとかLispあたりが有名どころですかね?


HaskellってPython, C, Fortranとかの有名言語と比べると割と特殊な言語なんですよね(その特殊性のおかげで今回の課題にはかなり苦戦しました)


特殊なポイントは何個かあるんですけど、その最たるところが

・変数がない

・for文がない

ってところなんですよね....


なんかHaskellが数学の圏論をベースにして作られた言語らしくて、だからHaskell的思想だとfor文とか変数あたりの考えってけしからんって感じらしいです(ちょっとなに言ってるんだろう)


とにかく抽象化して考えろっていうのがHaskellの設計思想らしいのです。


課題では「素数発見のプログラムを書け」とか「リスト操作の関数を定義せよ」みたいな、普段Pythonとかに慣れ親しんでる人からすれば30秒もあれば書けるようなものばっかりなんですけど、Haskellだとそれがだいぶ厄介になっちゃうんですよね〜


for文も変数もないから、関数の定義とかどうするかっていうと、基本的には再帰的な感じで片付けます。


とりあえず具体例をあげましょう。

例えば、n番目の素数を出すプログラムで、Pythonだと


find nth prime with Python

みたいな感じで、for文を活用しまくって書くじゃないですか。

それがHaskellだと


find nth prime with Haskell

みたいな感じで、常に再帰的な関数定義を行なっていきます。


最初はわけわからなさすぎて大苦戦してたんですけど、それがしばらくしてからだんだんとパズルを解く感覚になって楽しくなってきました。
現在は初心に戻ってやっぱりHaskellってクソ言語やなとか思ってたり


Haskellやってて、ホントに頭の柔らかさを求められてるなぁってのが率直な感想です。


これからHaskellやって今後苦戦しうる人のため、現在苦戦している人のために、パイソニスタ視点でのHaskell攻略のポイントをちょこっと話していこうと思います。

複雑な関数定義はうまく小問題に切り崩して再帰の考えに帰着させるべし

上に述べた素数発見のプログラムとかは、

「変数iで割り切れなかったら次はi+2で割ってみる」(try関数の話)

とか、

「isPrime関数でnがFalseだったら次はn+2を処理する」(findPrime関数の話)

みたいな感じで、とにかく大きな問題を小さな問題へと切り崩してくのがとにかく重要なんですよね。


これってプログラマーとしての底力というか、プログラマーとしての実力がまさに反映される感じですよね。


そういう意味で、Haskellでの学んだことはぼく自身の地力の向上につながったような気がします。

標準実装されている関数について知るべし

Haskellって抽象化しすぎて全体としては不便なプログラミング言語なんですけど、その中でも割と便利な関数があります。

ぼくが今回色んな問題を解いていく中で、特に有用だと感じたのが

"map", "foldr", "foldl"

の3つです。


map関数は

map (2+) [1,2,3,4]

みたいな感じで、リスト全体に同じ関数を作用させるというものです。

これがなかなか便利なんですよ。


次にfoldrとfoldlですね。

どういう関数かっていうと、畳み込みを行う関数です。

foldr ¥ [x1, x2, x3 ... xn]

ちなみに¥には畳み込みを行なった際の空集合に入るものを指定します。(yとか)

こんな感じでやると

x1(x2(...(xn, y))...)

みたいな感じの畳み込みを行います。


foldlは逆の方からの畳み込みを行います。


これが割と有能なんですよね。


どう使えるかを説明するために具体的な問題をあげましょう。


カタラン数ってのがあるんですけど


c_n = c_0 c_{n-1} + c_1 c_{n-2} + ... + c_{n-2} c_1 + c_{n-1} c_0

c_0 = 1


こんな感じの定義です。


これって見方を変えれば「(c_0, c_1 ... c_{n-1})をベクトルとして見たときの内積」ですよね!
(割とぼく面白い見方してますよね、という自画自賛)

これをHaskellで定義すると以下の通りです。

atalan :: Int -> [Int]
catalan 0 = [1]
catalan 1 = catalan 0 ++ [1]
catalan n = catalan (n-1) ++ [foldr (+) 0 $ zipWith (*) (catalan (n-1)) (reverse (catalan (n-1)))]

main = print $ catalan 10


(++)はappendの意味で、リストへの追加を表します。

このプログラムの[foldr ...]のところが内積の定義です。

引数としてはn-1番目までのカタラン数のリストと、それを逆順番にしたリストの2つを引数として取っています。


こう考えたらなるほどなぁって感じしませんか?(多分腑に落ちない人が多数だと思いますが)




まあこんな感じで、Haskell再帰的な考え方が割といい頭の体操になるんですよね〜


プログラムやってて腕に覚えあり、という方は是非ともHaskellを触ってみたらいいと思います!
(環境構築がやや大変なんですけど)


まとめ

Haskellは頭の体操。

プログラマーとしての実力が試される。

Haskellをやることで謎の充実感が得られる。




ではでは、今回はここらへんで〜


追伸

割と忙しくて株価予測モデルとかPyTorchの話にもう少し時間がかかりそうです。

先延ばしにして申し訳ないです (>_<)

いつになるかわかりませんが、お楽しみに!