技術的なやつ

技術的なやつ

6.1 すごいHaskellたのしく学ぼう! 第1章

カラオケ採点機制作の息抜きとして、「すごいH本」ことすごいHaskellたのしく学ぼう!を読み始めた。

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

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

Haskellとは

Haskellとは、純粋関数型言語である。CとかJavaとか普段我々がよく使う言語は手続き型言語と呼ばれる。

関数型言語では、「ある関数は必ず決まったある値を返す」ことを原則とする。そもそも、数学における関数の定義は「ある引数を与えるとある決まった値を返す」である。そのため、y=x^2において「yはxに関する関数」であるが、「xはyに関する関数」ではない*1。グラフを考えればわかるが、あるy(>0)を与えてもxの値は一意に定まらないのである。「ある引数を与えると必ず決まったある値を返す」関数のことを副作用のない関数と呼ぶ。Haskellは副作用のない関数しか扱えない。

手続き型言語では、関数は必ずしも値を返すわけではない(Cとか考えてもらうとわかる)。関数内にはそのメソッドが実行する手続きが書かれているのである。
ちなみに、Cだと簡単に副作用のある関数を書くことができる。たとえば、以下のような関数である。

int a;

int func(int x){
  return x + a;  // funcの値はグローバル変数aに依存する。すなわち、xに対して一意でない
}

また、Haskellでは遅延評価を基本とする。
遅延評価とは、「その値が必要となるまで計算しない」というものである*2。これによって、無限長のリストを扱うこともできる。なぜなら、本当に必要となるまでそのリストは評価されないからだ。

というわけで、Haskell手続き型言語とはまた異なった趣向の言語である。これがどういう世界で有効なのかというと、まぁ、今の私では数学への応用くらいしか思いつかない。要するに暇つぶしで読み始めた。

環境構築

Ubuntu12.04にて。

sudo apt-get install haskell-platform

これでHaskellコンパイラインタプリタ・基本ライブラリがインストールされる。
対話環境を起動するには、端末でghciと打つ。

okabi@pc:~/haskell$ ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> 

試しに。

Prelude> 2 * 4
8

基本演算

Prelude> 1 + 2 * 3
7
Prelude> (1 + 2) * 3
9
Prelude> 5 / 2
2.5
Prelude> True && False
False
Prelude> True || False
True
Prelude> not False
True
Prelude> not (True && False)
True
Prelude> 5 == 5
True
Prelude> 5 /= 0
True
Prelude> "hello" == "hello"
True

Haskellは型が厳密なので、同じ型同士でないと比較はできない。

基本関数

関数succは与えた引数の次に位置する値を返す。

Prelude> succ 8
9
Prelude> succ 'a'
'b'

関数名を引数の前に置く関数を前置関数と呼ぶ。演算子'+'や'-'も、その前後にある数を引数とする関数と捉えることができる。このように引数の間に関数名を置く関数を中置関数と呼ぶ。

Prelude> min 7 9
7
Prelude> max 7 9
9
Prelude> div 5 2
2
Prelude> 5 `div` 2
2

2引数関数は中置関数っぽくすることも可能。その場合、関数名をバッククォート(Ctrl+@)で囲む必要がある。

関数定義

baby.hsというファイルを作って、以下のように書く。

doubleMe x = x + x

これは、引数1つ(x)を取り、x+xの値を返すdoubleMeという関数である。なお、関数名の最初の文字は小文字である必要がある。何かしら理由があるらしいがまだ知らない。
実際にghci上で使用するには、コードを保存した上でロードする。

Prelude> :l baby.hs
[1 of 1] Compiling Main             ( baby.hs, interpreted )
Ok, modules loaded: Main.
*Main> doubleMe 7
14

次に、「xとyを与えて2x+2yを返す」関数doubleUsを定義する。

doubleUs x y = doubleMe x + doubleMe y

評価の優先順位は演算子より関数のほうが高い。
次に、「xが100より大きい時はxを、それ以外の場合は2xを返す」関数doubleSmallNumberを定義する。

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

Haskellでは、if文に必ずelseが必要である。なぜなら、未定義領域が出来るのでif文が「関数」とならないためである。

リスト

*Main> let lostNumbers = [4,8,15,16,23,42]
*Main> lostNumbers 
[4,8,15,16,23,42]

こんな感じで、'['と']'で囲むとリストになる。リストの要素はすべて同じ型でなければならない。つまり、

[1,'a']  # できない

みたいなことはできない。なお、GHCiでletを使うのは、定数宣言みたいなものだと思えば良い。
文字列は文字のリストである

*Main> let string = ['c','a','t']
*Main> string
"cat"

リストの結合には'++'を使う

*Main> let st1 = "a "
*Main> let st2 = "cat"
*Main> st1 ++ st2
"a cat"

リストの先頭に要素を追加するには':'を使う

*Main> let st = " cat"
*Main> 'a' : st
"a cat"

以下のような書き方はできない。

*Main> 'a' ++ " cat"  # できない。++はリストとリストをつなぐため。
*Main> "a " : "cat"  # できない。:は第1引数を要素で取らねばならないため。

リストのリストとかも書ける。

*Main> let a = [[1,2,3], [4,5,6], [7,8,9]]
*Main> a
[[1,2,3],[4,5,6],[7,8,9]]
*Main> a ++ [[10,11,12]]
[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]
*Main> [-2,-1,0] : a
[[-2,-1,0],[1,2,3],[4,5,6],[7,8,9]]

リストの操作

*Main> let a = [9,8,7,6,5,4,3,2,1]
*Main> let b = [1,8,3,6,5,4,7,2,9]
*Main> a !! 2  # !! n はインデックスナンバーnの要素を取り出す。
7
*Main> a > b  # リストは先頭要素から順に比較ができる。
True
*Main> [9,8,7] < [9,9,1]  # 先頭要素が同じ場合、再帰的に次の要素の大小を見る。
True
*Main> [9,8,7] > [9,8,6,9]  # サイズが違っていても良い。あぶれた分は無視される(空リストとの比較という見方も可能)。
True
*Main> [1,2,3] > []  # 空リストは必ず最小。
True
*Main> head a  # aの先頭要素を返す。
9
*Main> tail a  # aの第2要素以降のリストを返す。
[8,7,6,5,4,3,2,1]
*Main> last a  # aの最終要素を返す。
1
*Main> init a  # aの最終要素を除いたリストを返す。
[9,8,7,6,5,4,3,2]
*Main> length a  # aのサイズを返す。
9
*Main> null a  # aが空リストならTrueを返す。
False
*Main> null []
True
*Main> reverse a  # aを反転させたリストを返す。
[1,2,3,4,5,6,7,8,9]
*Main> take 3 a  # aの第n要素までをリストにして返す。
[9,8,7]
*Main> take 1 a
[9]
*Main> take 0 a
[]
*Main> drop 3 a  # aの第n要素までを落としたリストを返す。
[6,5,4,3,2,1]
*Main> drop 0 a
[9,8,7,6,5,4,3,2,1]
*Main> drop 100 a
[]
*Main> maximum a  # aの最大要素を返す。
9
*Main> minimum a  # aの最小要素を返す。
1
*Main> sum a  # aの総和を返す。
45
*Main> product a  # aの積を返す。
362880
*Main> 6 `elem` a  # aに要素が存在する場合はTrueを返す。
True
*Main> 0 `elem` a
False

レンジ

範囲を指定して、それをすべて要素とするリストを作ることができる。これをレンジと呼ぶ。
また、最初の要素を指定することで等差数列のリストを作ることもできる。これをステップと呼ぶ。

*Main> [1..9]
[1,2,3,4,5,6,7,8,9]
*Main> [1,3..9]
[1,3,5,7,9]
*Main> [1,4..20]
[1,4,7,10,13,16,19]

レンジを利用して無限リストを作ることもできる。たとえば、3から始まる3の倍数のうち24番目までのリストを得るには、以下のようにすれば良い。

*Main> take 24 [3,6..]
[3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72]

これは、Haskellが遅延評価だからこそできる芸当である。つまり、無限リスト生成時には実際にはリスト内容が計算されていないのである。take 24を適用させることで、初めて無限リスト(の一部)が評価される。
その他、特筆すべきリスト利用について。

*Main> take 10 (cycle [1,2,3])  # cycleは与えたリストを繰り返す無限リストを生成する。どこかで切らないと無限ループするので注意。
[1,2,3,1,2,3,1,2,3,1]
*Main> take 12 (cycle "LOL ")
"LOL LOL LOL "
*Main> take 10 (repeat 5)  # repeatは与えた要素を繰り返す無限リストを生成する。
[5,5,5,5,5,5,5,5,5,5]
*Main> replicate 10 5  # replicateはx回yを繰り返すリストを生成する。
[5,5,5,5,5,5,5,5,5,5]
*Main> [0.1,0.3..1]  # 実数は浮動小数点数なので、期待した値とは異なるものが得られる場合があるので注意
[0.1,0.3,0.5,0.7,0.8999999999999999,1.0999999999999999]

リストの内包表記

Haskellの大きな特徴の一つだと思う。
Haskellでは、リストを数学の集合表記と似たような表記法で記述可能である。
たとえば、\{2x | x \in N, x \lt 10\}のような集合表記があったとする(10未満の自然数xについて2xを要素とする集合)。これは、Haskellでは以下のように表記する。

*Main> [2*x | x <- [1..9]]
[2,4,6,8,10,12,14,16,18]

50から100のうち、7で割ったあまりが3であるすべての数を含むリストは以下のように定義する。

*Main> [x | x <- [50..100], x `mod` 7 == 3]
[52,59,66,73,80,87,94]

また、要素表記に複数のリストを使った場合は、すべての組み合わせが要素となる。

*Main> [x+y | x <- [1,2,3], y <- [10,100,1000]]
[11,101,1001,12,102,1002,13,103,1003]

ここで、リストの長さを返すlength'を自分で定義してみよう*3

length' xs = sum [1 | _ <- xs]

ちょっと分かりにくいので説明。
引数として与えたリストxsから、順番にアンダーバーに要素を取っていく(事実的に要素を1つずつ捨てている)。取るたびに、1を要素としてリストに追加する。つまり、xsのサイズの、1のみを要素としたリストができる。その総和を取ることで、xsの長さを得る。

タプル

タプルとは、リストのようなものである。リストとの違いは以下の通り。

  • 異なる型を1つのタプルに纏められる
  • タプル比較の際、長さが等しくすべてのインデックスの型が等しい必要がある

長さ2のタプルはペア、長さ3のタプルはトリプルと呼ばれる。
以下、タプルを扱う基本関数と共に使用例。

*Main> (1,'a') < (1,'b')
True
*Main> fst (1, 2)  # fstはペアの最初の要素を返す。
1
*Main> snd (1, 2)  # sndはペアの最後の要素を返す。
2
*Main> zip [1, 2, 3, 4, 5] "abcde"  # zipは2つのリストからペアのリストを作る。
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')]
*Main> zip [1..5] "abc"  # リストの長さが異なる場合、短い方に合わせられる。
[(1,'a'),(2,'b'),(3,'c')]
*Main> zip [1..] ['a'..'f']  # よって、無限リストにも対応可能。
[(1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e'),(6,'f')]

感想

1回生の頃に触っていたSchemeLispの方言)と似たような感じだと思った(特にリストの扱い方とか)。まぁ、Lispも一応は関数型言語に属するので、当然といえば当然である。
数学の集合記法でリストが表現できる点が面白い。また、Schemeと同じく遅延評価を持つ言語なので、無限リストを扱えるという点も面白い。

*1:すなわち、逆関数が存在しないということ。

*2:私が知る限りでは、Lispにもそのような機構が存在する。尤も、Lispは破壊的代入が可能であるという点で副作用のある関数を書けてしまうので、純粋関数型言語ではない。

*3:Haskellでは「'」に特別な意味はないので、普通に関数名に使うことができる。