Haskell

父さんPythonやめてHaskellをやっていこうと思うんだ

Pythonはやめません

関数型を学ぶなら関数型言語でしょ

ということで、巷では数学の圏論を理解しないと無理だとか言われているHaskellに手を出すことにした。

たぶんね、最終的には圏論も理解しなきゃいけないと思うんだけど、最初に最低限使うだけなら圏論とか理解してなくてもいける気がするんだよね。

オブジェクト指向理解しなくてもJavaでもRubyでもHello Worldできるのと一緒で(それよりは敷居が高いだろうけど)。

大抵は実際に使ってみて「なにが嬉しいか」的な体験をもとに理論を学ぶほうが効率がいい気がする。

言語の学習って畢竟なにが便利なのかってモチベーションが大事ですよね、しらんけど。

すごいHaskellたのしく学ぼう!

巷ではすごいH本とか誤解を招く呼び方をされている、Haskell入門バイブル的なやつを買ったので、消化できた内容をアウトプットしようと思う。

img

ぶっちゃけ表紙もファンシーだしなめてたのは否めない。 ガーッと読んで「関数型完全に理解した」くらいで終わりにしようと思ってたけど、わりと整理しながらじゃないと後半きびしい感じ。

ということで復習がてら書いていくが、どうも長くなりそうなので、この記事は畳み込みの前あたりでいったん区切る。

環境構築

とりあえずstackを入れる。コンパイル言語だけどインタラクティブシェルもある。GHCiとかいうらしい。

シェルでは、stack ghciで呼べて、Prelude>のあとに式を入力していく。

$ stack ghci
...
Prelude>

関数呼び出し

基本的に()は、必要な場合以外は使わない。Haskellではすべての関数はデフォルトでカリー化されている。

min 9 10

if

Haskellにおいてif文ではなくて式。つまり値を持つために網羅的である必要性がありelse節は必須。

doubleSmallNumber x = if x > 100
                        then x
                        else x*2

リスト

記法はいたって普通。

[1,2,3,4,5]

本では明記されていないが、Haskellでいうところのリストは配列でなく単方向連結リストっぽい。

というのも [1,2,3,4] ++ [5]よりも1:[2,3,4,5]のほうが高速だと書かれているから。

裏取ってないけどリスト末尾へのpushにO(N)O(N)、先頭へのappendがO(1)O(1)ということから間違いないと思われる。

こういうとき、データ構造と計算量が理解できてるかって大きな違いですよね。競プロ万歳。

先頭へのappendをおこなう:はcons演算子とも呼ぶとか。インデックスでアクセスする場合は、[5,6,7,8] !! 2みたいに書く。

文字列は[Char]型で実装されているらしく、"oden tabe tai" !! 3とかすると'n'が返る。

リスト全体を対象にとった大小比較も可。

中置

div 4 2はバッククォートを使って4 `div` 2とも書ける。

range

[1,2,3,4,5][1..5]と書ける。[5,4,3,2,1][5,4..1]と書ける。

これは逆順に限らずstepを指定する書き方で[1,3,5,7,9]なら[1,3..9]と書ける。

無限リスト

Haskellは遅延評価なので[1..]のように無限リストを簡単に作れる。

先頭からチョッキンしてくるならtake 5 [1..]のようにすればオッケー。

その他の関数

Prelude> take 3 $ cycle [1,2]
[1,2,1]

Prelude> take 3 $ repeat 5
[5,5,5]

Prelude> replicate 4 5
[5,5,5,5]

リスト内包表記

例を見たほうがはやい。,以降は述語と呼ばれており条件節。,を重ねていくつも書ける。

Prelude> [x*5 | x <- [1..5]]
[5,10,15,20,25]

Prelude> [x*5 | x <- [1..5], x*2 > 5]
[15,20,25]

Prelude> [x+y | x <- [1..3], y <- [2..4]]
[3,5,7]

Prelude> [5 | _ <- [1..5]]
[5,5,5,5,5]

タプル

Prelude> fst (3, 12)
3

Prelude> snd (3, 12)
12

型のチェック

型は::で表現する。

Prelude> :t 'd'
'd' :: Char

Prelude> :t "hoge"
"hoge" :: [Char]

Prelude> :t ("fuga", True, 235)
("fuga", True, 235) :: Num c => ([Char], Bool, c)

Integer

HaskellではIntは普通のintで、Integerはbigint的な実装らしい。

型変数

いわゆるジェネリクス的なやつを扱うためのTみたいなやアレ。Haskellでは小文字のa,b,cとかを使うらしい。

Prelude> :t head
head :: [a] -> a

型クラス

Rustでいうところのトレイト的なやつだと思う。=>で表現されるっぽい。

あと記号オンリーの演算子はデフォルトで中置関数になる。受け渡しをしたい場合は()でくくる必要がある。

Prelude> :t (==)
(==) :: Eq a => a -> a -> Bool

パターンマッチ

このあたりから関数型チックになってくる。関数型は基本的にを使わずをベースにしてコードを紡いでいく。

パターンマッチはifを使わない条件分岐の世界観になる。これは再帰表現と相性が良い。

fact :: Int -> Int
fact 0 = 1
fact n = n * fact (n - 1)

パターンマッチの応用

タプルとパターンマッチの組み合わせは強力になる。またリストに対するx:xsといった表現は再帰とさらに相性がよい。

first :: (a, b, c) -> a
first (x, _, _) = x

-- asパターンを使用するとパターンに名付けができる
firstLetter :: String -> String
firstLetter "" = "DO NOT ENTER EMPTY STRING!"
firstLetter all@(x:xs) = [x] ++ ": " ++ all

ガード

パターンマッチは構造によって場合分けをするが、値によって場合分けをしたい場合はガードを使う。

isYoung :: Int -> String
isYoung age
    | age < 20 = "You're young!"
    | age > 70 = "You're almost dead!"
    | otherwise = "You're old!"

where / let

さらにwhereletを使用すると変数定義も可能。

以下はDoubleで受け取ると小数誤差が出て嫌なので100倍の値で判断している。

isPriceLow :: Int -> String
isPriceLow price
    | taxed < 220 * 100 = "price is low."
    | taxed > 550 * 100 = "price is high."
    | otherwise = "price is not low."
    where taxed = price * 110

whereは文だが、letは式として使える。

let bindings in expressionの構文で使用する。さらに;で区切ることができて、

let a = 10; let b = 40; let c = 60 in a*b*c

のような書き方をする。リスト内包表記の中でも通用する。

case式

Haskellでは例に漏れずcaseも式である。シンタックスは以下にならう。

わりとコード中のどんなところでも使えるっぽい。

case expression of pattern -> result
                pattern -> result
                pattern -> result

再帰

これまで命令的なコードの書き方に慣れ親しんできたが、ここでどれだけ宣言的なお作法を学べるかが鍵な気がしている。

再帰による自己定義的な書き方は驚くほどコードをシンプルにしてくれる。

max' :: (Ord a) => [a] -> a
max' [] = error "empty list is not allowed."
max' [x] = x
max' (x:xs) = max x (max' xs)

本にはいくつもの関数を再帰で実装した例が紹介されている。

カリー化関数

複数の引数を受け取る関数に一部の変数だけ渡すと残りの変数を引数にとる関数を返す形にするのがカリー化。

他の言語だと明示的にカリー化させる必要があるが、Haskellでは逆にデフォルトがカリー化の挙動を示す。

中置関数の部分適用

中置でも片方にだけ値を置けば部分適用になる。100 / 20(/20) 100と書けるということ。

ラムダ式

バックスラッシュに続けてラムダ式を記述できる。

(\xs -> length xs >15)

(/3)(\x -> x / 3)と書くようなマヌケな真似をしないようにする。

つづく

次から畳み込みが畳み掛けてくる。