2020/04/27

そうだ、この機会にHaskellやってみよう

Haskellとは

Haskellは非正格な純粋関数型言語だそうです。非正格というのは関数の引数は関数本体が実際に使われるまで評価されないというもので、遅延評価とも言うようです。OCamlとは違った(OCamlはデフォルトでは正格)関数型言語ということになります。OCamlでもLazyを使って遅延評価にすることは可能です。そして純粋関数型言語というのは「参照透過性」が常に保たれる。「参照透過性」が常に保たれるということは、関数への入力が同じであれば常に同じ結果を得られるということで、思わぬ不具合が起きにくいということでもあります。例えば、親子丼を注文したら、必ずいつもの鳥と卵を使った親子丼が出てくるということです。
逆に「参照透過性」が怪しい場合は、味はほぼ同じなのに、実はカエルの肉と卵の親子丼でした、といったように内部で何かある感じの状態。何となくモヤっとした感じになりませんか?素人の私はこのようなざっくりイメージを持ちました。カエルの肉もおいしいらしいが、できればいつもどおりの親子丼にしたいものです。また常に同じ結果を得ることができるということは、テストもしやすいということです。
諸々のことからHaskellを勉強すると、「プログラミングのお行儀がよくなる」らしいです。プログラミングがカオスな私には必要なのかもしれません。

さっそく開始

私はManjaro Linuxを使っていますが、pamacでstackをインストールしてHaskellを使えるようにしました。
まずはghciで色々試してみます。ghchiはHaskellのREPLで、対話的に色々試すことができます。

$ ghci
GHCi, version 8.8.3: https://www.haskell.org/ghc/  :? for help
Prelude>
まずREPLを立ち上げてすぐに注目する箇所はPrelude>という部分です。「Preludeってなに?」
簡単に言うと、いつでもどこでも使えるものです。つまり以下のようなことがインポートなしでできます。

Prelude> -- One line Comment
Prelude> {- Multiline Comments -}
Prelude> putStrLn $ show "Hello, world!!"
"Hello, world!!"
Prelude> print "Hello, world!!"
"Hello, world!!"
Prelude> print "hello" >>= \_ -> print "world"
"hello"
"world"
Prelude> 1 + 2 - 3 * 4 / 5
0.6000000000000001
Prelude> list = [1,2,3,4,5]
Prelude> list
[1,2,3,4,5]
Prelude> newList = 0 : list
Prelude> newList
[0,1,2,3,4,5]
Prelude> newNewList = newList ++ [6]
Prelude> newNewList
[0,1,2,3,4,5,6]
Prelude> newNewList !! 0
0
Prelude> head newNewList
0
Prelude> last newNewList
6
Prelude> init newNewList
[0,1,2,3,4,5]
Prelude> tail newNewList
[1,2,3,4,5,6]
Prelude> tuple = (1, 2)
Prelude> tuple
(1,2)
Prelude> fst tuple
1
Prelude> snd tuple
2
Prelude> :q
Leaving GHCi.

printputStrLn $ showは同じらしい。リストではコンス演算子:やその他諸々があり、操作は便利そうな予感。ただインデックスに!!というのはまだ違和感を感じますが、慣れの問題だろう。

ファイル&ghci

今度はプログラムをファイルに書いてやってみようかと思います。とりあえずファイル名(名前は大文字で始める)はMain.hsとしておきます。

module Main(main) where

main = do
    print "Hello, world!!"
    putStrLn $ show "Hello, world!!"
最初の行のmodule Main(main) whereは、Main.hsにおいては暗黙的に宣言されるので省略できるそうですが、勉強のため省略せずに書きました。上記でも書いたように、printputStrLn $ showは同じですが、ここではdoの存在が重要で、もしdoがなければこのプログラムはエラーになり実行できません。doは何やらアクションというものが絡む処理だそうで、アクションをつなげる役割があるようです。doなしで書くと

main = do
    print "Hello, world!!" >>= \_ -> print "Bonjour"
アクションについては今後しっかり勉強しようと思います。ちなみに>>=はbindを意味し、\_(バックスラッシュ+アンダースコア)は引数なしのラムダ式となっています。
ghciで実行する場合は、以下のようにします。

Prelude>:l Main
[1 of 1] Compiling Main             ( Main.hs, interpreted )
Ok, one module loaded.
*Main> main
"Hello"
"Bonjour"

:lでファイルをロードできます。

Fizzbuzz

今度はFizzbuzzにチャレンジ。ファイル名はFizzbuzz.hsとします。

module Fizzbuzz(main) where

fizzbuzz :: Int -> String
fizzbuzz n
    | fizz && buzz = "Fizzbuzz"
    | fizz         = "Fizz"
    | buzz         = "Buzz"
    | otherwise    = show n
    where
        fizz = mod n 3 == 0
        buzz = mod n 5 == 0

main :: IO ()
main = do
    print [fizzbuzz x | x <- [1..30]]
where以下でローカル変数を定義する。letを使うこともできるようです。ちなみにotherwiseはキーワードです。[fizzbuzz x | x <- [1..30]]はリスト内包表記で、試しにちょっと使ってみた感じです。リスト内包表記はPythonでも使われているので、Pythonで書くと以下と同じになるかと思います。

def fizzbuzz(n):
    fizz = n % 3 == 0
    buzz = n % 5 == 0

    if fizz and buzz:
        return "Fizzbuzz"
    elif fizz:
        return "Fizz"
    elif buzz:
        return "Buzz"
    else:
        return str(n)

print([fizzbuzz(x) for x in range(0, 31)])
ちなみにHaskellにはfor文がないという噂を耳にしました。このあたりも要勉強。

Prelude> :l Fizzbuzz
[1 of 1] Compiling Fizzbuzz         ( Fizzbuzz.hs, interpreted )
Ok, one module loaded.
*Fizzbuzz> main
["1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz",
"13","14","Fizzbuzz","16","17","Fizz","19","Buzz","Fizz","22","23",
"Fizz","Buzz","26","Fizz","28","29","Fizzbuzz"]

とりあえずうまくいきました。

まとめ

まだまだHaskellの構文に馴染めないレベルです。数学的な要素が強めで、体育会系の私には特に敷居が高い。どうやら圏論というものがすごく関係しているようで、私の場合、数学から勉強が必要な部類の言語かもしれません。なんとなくOCamlやRustに似たようなスメルを感じているのですが、どうなのだろう?おすすめの入門書やサイトなどがあればぜひ教えてください。

追記(2020/4/30)

Fizzbuzzのコードの一部が勝手に変な変換になっていましたので修正しました。申し訳ございません。

ついでにL99問題の2までやりましたので、コードを載せておきます。


-- L99-01
lst :: [a] -> Maybe a
lst [] = Nothing
lst [x] = Just x
lst (_:xs) = lst xs

-- L99-02
lstTwo :: [a] -> Maybe (a, a)
lstTwo [] = Nothing
lstTwo [x] = Nothing
lstTwo [x, y] = Just (x, y)
lstTwo (_:xs) = lstTwo xs

HaskellではRustのOption的なものとしてMaybeというものがあるようです。戻り値はJust aNothingとなるようです。パターンマッチングは非常に個性的に感じましたが、これも慣れの問題だろうかと思います。