Haskell入門ハンズオン! #3: 当日用資料 1/5
はじめに
この記事は2018年3月11日に開催のHaskell入門ハンズオン #3の当日用の資料だ。
サンプルコード
サンプルコードは以下の短縮URLから入手できる。
内容
Haskellでパーサコンビネータを自作し、四則演算の式を解析して(しつつ)、評価するプログラムを作っていく。コードを書きながら、Haskellの構文や技法を紹介していく。例題のコードは拙書「Haskell - 教養としての関数型プログラミング」の例を使った。
「第20章 演習: 構文解析」の1章ぶんをまるまる解説する。第20章はこの書籍の298から310ページまでの内容だ。書籍では200ページ以上かけて、この例題で必要な要素を説明している。その部分をまるまるとばす。コードを書くなかで、適宜、解説をいれるが、完全に理解できなくていい。雰囲気を感じとってもらいたい。
環境構築
環境の構築はHaskell入門ハンズオン! 事前資料(2/3)を参考にしてもらいたい。
環境の確認
つぎのようにして、環境構築ができていることを確認する。
% stack ghc -- --version
The Glorious Glasgow Haskell Compilation System, version 8.2.2
対話環境
対話環境を試してみよう。ターミナルにstack ghciと打ち込む。
% stack ghci
とりあえず、数値や文字を打ち込む。
Prelude> 123
123
Prelude> 'c'
'c'
足し算や引き算もできる。
Prelude> 3 + 5
8
Prelude> 15 - 9
6
関数を使う
関数を使ってみよう。
Prelude> negate 8
-8
関数を使うには、つぎのようにする。
[関数名] [引数1] [引数2]
+や-などの演算子も、実は関数だ。
Prelude> (+) 3 5
8
Prelude> (-) 15 9
6
関数(+)に引数3と5とを、関数(-)に引数15と9とを、それぞれ、あたえている。
関数を作る
まずは、変数を定義してみる。
Prelude> n = 1234
Prelude> n
1234
Prelude> n + 4321
5555
関数を定義する。半径から円の面積をもとめる関数。
Prelude> area r = r ^ 2 * pi
Prelude> area 3
28.274333882308138
半径3cmの円の面積は、だいたい28cm^2。変数の定義と関数の定義の構文は、よく似ている。Haskellでは変数piは円周率として定義ずみだ。
定義ファイル
対話環境を終了する。
Prelude> :quit
対話環境で定義した関数は、対話環境の終了とともに消えてしまう。定義した関数を、もっと長く使いたいときには、関数を定義したファイルを対話環境に読み込んで使う。好きなエディタでファイルcircle.hsを作る。僕の場合はVimを使う。
area r = r ^ 2 * pi
これを対話環境に読み込む。
% stack ghci
Prelude> :load circle.hs
試してみよう。
*Main> area 3
28.274333882308138
読み込めたようだ。ここで、対話環境は立ち上げたままにして、ファイルcircle.hsに円周をもとめる関数を追加する。
circumf r = r * 2 * pi
対話環境に、コマンド:reloadを打ち込む。
*Main> :reload
*Main> circumf 3
18.84955592153876
たいていにおいて、僕の開発スタイルはターミナルを、ふたつに分割して、Vimと対話環境を開きっぱなしにして、ファイルを変更するたびに:reloadしながら。というもの。
型
値には型がある。対話環境で型をみてみよう。
*Main> :type 'c'
'c' :: Char
文字'c'はChar型の値だ。
*Main> :type True
True :: Bool
真(本当)であることをあらわす値TrueはBool型の値だ。
関数の型
関数にも型がある。関数の型は、引数の型と返り値の型とで決まる。例として、文字を大文字にする関数と、文字が大文字であることを確認する関数をみる。まずは、それぞれを使ってみる。
*Main> :module Data.Char
Prelude Data.Char> toUpper 'c'
'C'
Prelude Data.Char> isUpper 'c'
False
関数toUpper, isUpperはモジュールData.Charから公開されている。対話環境では、コマンド:moduleでモジュールを導入する。
関数toUpperは引数も返り値もChar型の値であり、関数isUpperは引数がChar型で、返り値はBool型の値だ。それぞれの型をみてみよう。
Prelude Data.Char> :type toUpper
toUpper :: Char -> Char
Prelude Data.Char> :type isUpper
isUpper :: Char -> Bool
関数の型は、引数の型と返り値の型とを'->'でつなぐ。
型宣言
Haskellでは(たいてい)型は自動的に決まる。しかし、型を明示するのは良い習慣だ。ファイルCircle.hsを編集する。
area :: Double -> Double
area r = r ^ 2 * pi
circumf :: Double -> Double
circumf r = r * 2 * pi
対話環境に読み込んで試す。
Prelude Data.Char> :load circle.hs
*Main> area 3
28.274333882308138
入出力について
Haskellでは「関数は関数」だ。「足し算を評価するついでに、設定ファイルを書きかえておきましたよ」といった(気の利いた|余計な)ことはしてくれない。関数は入出力をしない。
...
「入出力なしで、どうやって生きていけと?!」
Haskellでは関数の評価と入出力とは分離されている。
入出力は、むしろ、ただの値だ。
リストという構造がある。たとえば数のリストの総和をもとめられる。
*Main> sum [3, 4, 5]
12
入出力も値なのでリストの要素にすることができる。
*Main> hello = [putStrLn "hello", putStrLn "world"]
リストに含まれる入出力を組み立てることもできる。
*Main> sequence_ hello
hello
world
逆順にして組み立てることもできる。
*Main> sequence_ (reverse hello)
world
hello
対話環境は評価した結果の値が入出力だったとき、その入出力を実行してくれる。ちなみに、結果の値が入出力でないとき、もしそれが表示可能なら、表示する。
Haskellでは入出力(および状態変化)は、IOモナドという、わくぐみであつかわれる。今回はIOモナドについては、くわしく説明しない。とりあえず覚えておくことは、Haskellでは「入出力は評価のついでに、おこなうものではない」「入出力も、また値である」ということだ。
詳細は拙書を参照のこと。