IGGG Advent Calender 2015のために書いた記事です。
プログラミング言語Haskellを複数に分けて簡単にまとめてみました。
第2回はHaskellの基本構文についてです。
基本構文なんて、今更自分が話す必要はないのですが、以下が解ればコードを読めるので(出来るかわからないけど)今後Haskellの細かい話をするうえで便利なので、まとめておきます。
Haskellの開発環境にはいくつか種類があります。
恐らく一番簡単なのは公式サイトにいってHaskell Platform をインストールする事でしょう。
Haskell Platform はGHCを利用しています。
他にもHugsやJHCと言うのもあるようです。
また、最近はGHCよりもsltackというのが流行っているようです。
stackはHaskell Platformに比べ、バージョンの競合とかが起きにくい(と言うか起きないようにしてる)感じです。
検索するとやり方が出てくるので、今回は割愛します。
ターミナル上で、ghc <ファイル名>
と打つとコンパイルされて実行ファイルが生成されます。
また、ghci
と打てばHaskellのインタプリタが起動します。
関数を定義して利用する、と言う話であればGHCiの方が楽なので、今回はGHCi主体で説明します。
GHCiにはいくつかの専用コマンドがあります。
例えば
:q
,:quit
: GHCi終了:l <ファイル名>
,:load <ファイル名>
:ソースコードを読み込む:r
,:reload
: 読み込んでいるソースコードの再読み込み:t <式>
,:type <式>
: 式(関数や値)の型を返してくれます(便利)
他にもいろいろあります。
:help
か:h
とやれば確認できます。
GHCiで試してみましょう。
Prelude> 1 + 1 - 2 * 3 / 2
-1.0
Prelude> 4 `div` 2 `mod` 3
2
Prelude> 2 ^ 4
16
Prelude> True && False
False
Prelude> div 4 2
2
Prelude> (+) 1 2
3
Haskellでは+
や^
のような記号で書かれた演算(関数)は中置記法になります。
それ以外の関数は前置記法で、空白区切りで引数を与えます。
また、前置記法の関数もバッククォート(`)で囲ってやると、中置記法になり、中置記法の関数は括弧で囲むと前置記法になります。
--
でそれ以降コメントアウトで、{- -}
で囲むとその範囲をコメントアウトします。
例として、引数の値に10を加える関数を定義します。
add10 :: Int -> Int
add10 n = n + 10
まず、最初の行に関数名 :: 型
を書きます。
ただし、Haskellは強力な型推論を持っているので、関数の定義から型が推論できる場合、書かなくてもコンパイルは通ります(コンパイラが型を推論するので)。
今回はわかりやすく全部書いていきます。
Int -> Int
は"Int型の引数を1つ貰いInt型の値を返す"型と言う意味です。
Haskell(というか全ての関数型)は関数を第一級オブジェクトとして扱う(このような関数を高階関数と言う)ので、Int -> Int
とうのも、Int
やDouble
等と変わらない1つの型です。
2行目から関数の定義を書きます。
add10 n
のn
が引数になり、=
の右辺が返す値(と言うかは式)になります。
ちなみに、引数の部分を書かずに以下のように定義することも可能です。
add10' = (+ 10)
実行してみると
*Main> add10 1
11
*Main> add10 0
10
となります。
Haskellの条件分岐はいくつか書き方があります。
だいたいどの言語にでもあるif-then-else
構文が利用出来ます。
ただし、else
を省略できません。
sign :: Int -> Int
sign n = if n == 0 then 0 else if n > 0 then 1 else -1
case 式 of
で与えた式の場合分けで記述する。
fizz :: Int -> String
fizz n = case n `mod` 3 of
0 -> "Fizz"
_ -> show n
(_
はワイルドーカード)
ガード(|
)に条件を優先度の高い順に記述して分岐する。
fizzBuzz :: Int -> String
fizzBuzz n
| 0 <- n `mod` 15 = "FizzBuzz"
| 0 <- n `mod` 5 = "Buzz"
| 0 <- n `mod` 3 = "Fizz"
| otherwise = show n
ちなみに、ガードもif-then-else
もcase
の構文糖衣でしかないです。
パターン町井と言う機能を利用すると、そもそも構文がいらない。
(というか、ガードとかもパターンマッチは利用してたけど...)
sign :: Int -> Int
sign 0 = 0
sign n = if n > 0 then 1 else -1
ちなみに、分岐を書くなら、このパターンマッチの書き方を極力お勧めします。
読みやすいので。
次に話す、where
をうまく利用すれば、全てパターンマッチで書けます。
余り冗長になるようでしたら他でもいいですが。
Haskellには、for
やwhile
のような繰り返し文はありません。
なので、関数の再帰呼び出しで表現します。
例えば、最大公約数を求める関数gcd
を考えます。
C言語なら(本質的でないのでa > b
を仮定します)
int gcd(int a, int b) {
int c;
do {
q = a % b;
a = b;
b = q;
} while (q > 0)
return b;
}
Haskellなら
gcd :: Int -> Int -> Int
gcd a b
| b > 0 = gcd b (a `mod` b)
| otherwise = a
関数が自分自身を呼び出すことを再帰呼び出しと言います。
値(と言うか式)の局所的な束縛の仕方には2種類あります。
let
式where
節: 正確には束縛ではなく定義
それぞれ以下のように書きます。
hoge :: Int -> Int
hoge a = let b = 100 in
a + b
fuga :: Int -> Int
fuga a = a + b
where
b = 100
もちろん、関数なんかも束縛できます。
大きな違いは、let
式は(その名の通り)式ですが、where
節は(その名の通り)式ではありません。
let
は式なのでどこでも書くことが出来ます。
例えば(let a = 100 in a) + 100
と書くのもありです。
対して、where
は関数定義(とcase式)の構文の一部です。
let
式の方が便利ですが、インデントの関係で見にくいので、where
を極力使うことをお勧めします。
組込みの型は以下のようなのがあります
Int
: 整数型Double
: 実数型Integer
: 長整数(何十桁数が使える)Char
: 文字String
: 文字列Bool
: 真偽値
さらに、特殊な型として
- リスト :
[Int]
みたいに書く、線形リスト型 - タプル :
(Int,Char)
みたいに書く、いわゆる直積型 - Maybe :
Maybe Int
みたいに書く、Just a
かNothing
を返す
リストやタプルは便利なデータ構造です。
特に、リストはHaskellにとって最も基本的な構造なので、リスト操作関数が豊富です。
このページがかなり便利で、Haskellを書くときは常に開いてる気がします(笑)
Maybe型と言うのは、不定値を含む場合に利用する型です。
他の言語では、不定値としてnull
何かを利用しますが、Nothing
を利用すれば型として保証でき、より安全になります。
(Either型と言うのを利用すれば、不定値ではなく、別の型の値を返すことが出来ます。)
Haskellにおいて型はひじょーーーーに奥深いので今回で書きるはずはないのですが、簡単な型の定義方法だけ説明しておきます。
定義の仕方は3つあります(型名の先頭は大文字)。
type
宣言data
宣言:type Tree = Leaf Tree Tree |
newtype
宣言
type
宣言はただのエイリアス(別名のコト)で type Hoge = [Int]
のように使います(右辺は既存の型)。
例えば、String
はCharのリストでできているので、type String = [Char]
と書きます。
対して、全く新しい型を作る場合はdata
宣言を利用します。
例えば、Bool
はdata Bool = True | False
と等価です。
少し詳しく説明するために二分木の型を考えます。
data Tree a = Node a Tree Tree
| Leaf a
(複数の値を定義する場合あh|
で区切っていきます。)
これは、Tree Int
のように利用します(Maybe型と同じです)。
このa
を型変数と言います。
そして、Tree
はInt
をもらってTree Int
を返す初期化関数と考えられるので、型コンストラクタと言います。
型の持ちうる値を定義するためにはNode
やLeaf
のような識別子が必要です。
そして、Leaf
はInt
をもらって、(Tree Int
の値である)Leaf Int
を返す初期化関数と考えられるので、値コンストラクタと言います(紛らわしい)。
ちなみに、値コンストラクタと型コンストラクタは同じでもよく、True
,False
のように値コンストラクタは引数を撮らなくてもいいです(型コンストラクタは引数を撮らない場合、たぶんコンストラクタと呼びません)。
(また、コンストラクタといったら、値の方を指す気がします)。
newtype
宣言は値コンストラクタを一つしか持たないdata
宣言です。
なんでわざわざ分けているのかと言うと、(たしか)型推論周りを効率化するためらしいです。
以上のことがわかれば、とりあえずはコードを読めると思います。
他の関数はHoogleやHayooを利用すると簡単に調べられるんじゃないですかね(別に怪しいサイトじゃないですよ)。