Login
Immutable PageDiscussionInfoAttachments
alstamber/HaskellSeminar2

MMA

ghciで遊ぼう

ghciという楽しいプログラムを使って遊びましょう。ghciというのはHaskellのインタプリタです。Ruby講習でirbというものに触れましたが、それによく似たものだと思ってください。
それではまずghciをたちあげてみましょう。

ghciが立ち上がったら色々なことを殺らせてみましょう。

まずは四則演算ですね。

1 + 2
4545 * 4545
11312 - 133333

当たり前ですが、1つの式の中に複数の演算子がある場合、数学で学んだ通りの優先順位に従って実行されます。
丸括弧を使って演算の順序を変えることもできます。

ghciで負の数を扱うときには少しだけ気をつける必要があります。

   1 1 * -2  動かない
   2 1 * (-2) 動く

次は論理演算をやってみましょう。Ruby講習に出た人ならおなじみだと思いますが、論理演算というのはTrueとFalseをつかった計算でした。
たとえば

   1 True && True
   2 False || False
   3 not False

のような演算ができます。

2つの値が等しいか等しくないかは、それぞれ==と/=という演算子で判定できます。

   1 5 == 5 True
   2 1 == 0 False
   3 5 /= 5 False
   4 5 /= 4 True
   5 "hoge" == "hoge" True

たとえば次のようなことをしてしまうとエラーになってしまいます。

   1 5 + "hoge"

"hoge"というのが数じゃないので、どうやって5と足せばいいのかわからない!というエラーが出るはずです。
+という計算をするときは、両辺に数の型が来ているかどうかをチェックして、そうでなければエラーが出るようになっているわけです。

関数を呼び出してみよう

これまでに簡単な計算をHaskellにやらせてきましたが、実はこのつまらない演習の中ですでにみなさんはHaskellの重要な概念に触れています。
それは関数です。関数はなにか値をとってそれに何か処理を施した結果を返すものだということを説明しました。
Haskellにおける関数は、Haskellのシステムの中核をなすシステムです。Haskellにおけるプログラミングは小さな関数から出発して、それを組み合わせて目的の処理をする大きな関数を作っていくという行為にほかなりません。

たとえば+というものは2つの数を引数にとって、それを足しあわせた結果を返す関数とみなせます。関数+を呼び出すためには足しあわせたい2つの数で+をはさみます。
このような関数は引数の間に置いて使うので、中置関数と言います。

しかしHaskellの関数の多くは中置関数ではありません。Haskellの関数の多くは前置関数、すなわち引数の前において使います。
Haskellで前置関数を呼び出すためには、関数名をはじめに書いて、スペースをあけて、スペースで区切られた引数を書きます。たとえば

succ 8

という感じです。succという関数は「後ろ」が定義されているものを何でもいいから引数としてとって、その「後ろ」を返す関数です。整数で「後ろ」といえば、次に大きな数です。

複数の引数を取る関数の例としてminという関数を挙げてみましょう。

   1 min 9 10

minは2つの引数をとって、どちらか小さい方を返す関数です。

関数の呼び出し(関数を適用するといいます)は、すべての計算の中で最も高い優先順位を持ちます。なので次の式はそれぞれ同じ意味を持ちます。

   1 succ 9 + 1
   2 (succ 9) + 1

また関数が2引数の時は、その関数をバッククオートで囲むことで中置関数に出来ます。たとえばmodという関数は2つの整数を引数にとって、割り算の余りを求める関数です。

   1 mod 9 2 1が答え

この呼び出し方ではどっちがどっちで割られるのかわからないかもしれません。そこで

   1 9 `mod` 2

と書くこともできます。

関数を作ってみよう

Haskellの基本は関数なので、関数を使うだけではなく作れるようにしたいものです。
Haskellでの関数の定義の仕方を覚えましょう。関数の定義は関数名の後ろにスペースで区切った引数が続きます。その後=という記号が続き、その後ろに関数の本体を表すコードを書きます。

とりあえず一つ関数を作ってみることにしましょう。

   1 doubleMe x = 2 * x

上のコードを皆さんの好きなエディタでかいてdouble.hsという名前で保存しましょう。それからdouble.hsを保存したディレクトリでghciを実行しましょう。
ghciが起動したら次のようにタイプします。

:l double

これで先ほど書いたdouble.hsがghciに読み込まれ、doubleMeという関数を使えるようになります。

じっさいにdoubleMeを使って遊んでみましょう。

doubleMeでひとしきり遊んだらdouble.hsに次のような関数を追加してみましょう。

   1 doubleUs x y = x * 2 + y * 2

追加できたらファイルを保存して、ghciで:l doubleとタイプして新しい関数をロードして遊んでみましょう。

関数の定義の中で自分が定義した関数を呼び出すこともできます。たとえばdoubleUsの定義の中にdoubleMeに共通する部分があることに気づけば、

   1 doubleUs x y = doubleMe x + doubleMe y

と定義することができるということに気づくでしょう。

この考え方は非常に大事です。先程も言った「小さな関数を組み合わせて目的の動作をする関数を作る」というHaskellの思想を表しているからです。

if式

次に数が100以下の時だけ数を2倍するような関数を書いてみましょう。

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

ifはRubyをやっていた人なら馴染みがあるものだと思います。Haskellのifの特徴としてはelseが必須であることが挙げられます。
なぜelseが必須なのでしょうか。

Haskellではプログラムは関数の集まりなのでした。関数というのは何か値をとって必ず何か値を返すものです。もしelseを書かなくても良いのであれば、
条件が成り立たないときにはなにも値を返せないという関数を作ることができてしまうのです。これではHaskellの仕様を満たせないので、elseは必須になっています。

名前

今までは引数を持つ関数だけを扱って来ましたが、引数のない関数を考えることもできます。

   1 rasis = "RASIS is so cute"

このrasisという関数は呼び出されると必ず"RASIS is so cute"という文字列を返す関数になっています。これは言い換えれば"RASIS is so cute"という文字列にrasisという新しい名前をつけたのだということもできます。なのでHaskellでは引数のない関数を名前と呼ぶことがあります。

リスト

Haskellにはリストというデータ構造があります。リストはRubyで言うところの配列に似ていますが、それと違うところもあります。
Haskellのリストには同じ型の要素しか入れられません。なので整数のリストというものを作ることはできますが、整数と文字の両方からなるリストをつくることはできません。

またHaskellでは文字列が文字のリストとして表されています。Haskellのリストには強力な操作をできる関数がたくさん用意されているので、文字列が文字のリストとして表されていることによって、
それら強力な関数を文字列の操作に使うことができます。

リストをつくるには要素をカンマ区切りで並べて、角括弧で囲みます。

   1 let numbers = [1, 2, 3, 4]

letというのはghciの中で変数や関数を定義するための方法です。ghciの中でlet a = 1と入力するのは、なんとか.hsにa = 1と書いて、:lを使って読み込むのと同じ意味です。

リストの連結

リストに対して行う操作として最も一般的なのは連結という操作です。連結は++という演算子で行います。

   1 [1, 2] ++ [3, 4]
   2 "hello" ++ " " ++ "world"

++演算子を使うときには少し注意が必要です。++を使うとき、Haskellは++の左側のリストを最後までスキャンする操作をします(これは仕様です)。
なので++の左側が非常に大きなリストだと++という操作は非常に時間がかかります。

それに対してリストの先頭に何か一つ要素を追加するのは軽い操作になっています。この操作をするためには:演算子(consえんざんし)を使います。

   1 'a' : " small world"

:と++の違いに注目してください。:の第一引数は追加しようとしているリストの要素と同じ型の単一の要素になっています。それに対して++は必ず2つのリストを引数として受け取ります。
たとえば++を使ってリストの最後に1つだけ要素を追加したいときにも次のように書く必要があります。

   1 [1, 2, 3, 4] ++ [5]

[1, 2, 3, 4] ++ 5 は間違いです。なぜなら++は2つの引数が両方ともリストでなければいけないからです。5はリストではなくて数です。

実はHaskellでは[1, 2, 3]というのは1:2:3:[]のただのSyntax sugarです。[]は空のリストです。その先頭に3を追加すると[3]になります。そこにさらに2を先頭に追加すると[2, 3]になります。最後に1を追加すれば、[1, 2, 3]になります。

リストの要素を取り出す

リストの要素を取り出したいときには!!演算子を使います。Rubyなどと同じようにリストの中の位置は0から数えます。

   1 "hoge" !! 2
   2 [1, 2, 3, 4] !! 1

リストのリスト

Haskellのリストはリストの中にリストを要素として含むことができます。

リストの中のリストはそれぞれ違う長さでも構いませんが、違う型のリストを入れることはできません。文字のリストと数のリストを要素として持つようなリストは作れません。

リストの比較

リストは中の要素が比較可能であれば、比較可能です。リストを<や>=といった演算子で比較すると、辞書順で比較されます。
まず先頭の要素が比較され、それが等しければ2番めの要素同士が比較されます。これを違う要素が見つかるまでくりかえします。

また空でないリストはどんなリストでも必ず空のリストより大きいとみなされます。

リストの関数

リストに対して使える便利な関数を幾つか紹介しましょう。

headはリストの先頭の要素を返す関数です。

tailはリストの先頭をのぞいた残りのリストの部分を返します。

lastはリストの最後の要素を返します。

initはリストの最後の要素を除いた残りのリストの部分を返します。

これらの関数を空のリストに対して適用するとエラーを吐きます。

lengthはリストの長さを返します。

nullはリストが空かどうかを教えてくれます。空ならTrueを、そうでなければFalseを返します。

reverse関数はリストを逆順にします。

take関数は数とリストを引数にとって、先頭から指定された数の要素を取り出したリストを返します。

   1 take 3 [1, 2, 3, 4, 5] [1, 2, 3]が結果

takeでリストの要素数より多い要素を取ろうとすると、リスト全体が返ってきます。0個の要素を取ると空リストが返ってきます。

maximumは、リストの中で最大の要素を返します。minimumは同じように最小の要素を返します。

sumは数のリストを受け取ってそれらの和を返します。同じようにproductは数のリストの要素の積を返します。

elem関数は要素とリストを受け取って、それがリストの要素に含まれているかどうかを返します。これは普通中置記法で使います。

   1 4 `elem` [1, 2, 3, 4] Trueが結果
   2 10 `elem` [1, 2, 3, 4] Falseが結果

Range

1から10までの数からなるリストを作りたい時どうすればよいでしょうか。

   1 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これでも動きますが、非常に面倒くさいです。そこでHaskellにはこういったリストを簡単に作れる書き方が用意されています。これをRangeといいます。

たとえば1から10までの数から成るリストをつくるには

   1 [1..10]

と書きます。

Rangeは数でなくても使うことができます。

   1 ['a'..'z']

Rangeにはステップを指定することもできます。1から100までのすべての偶数のリストがほしい時はどうすればよいでしょう。最初の要素と2つ目の要素をカンマで区切って、その後上限を指定すればHaskellがよしなに解釈してくれます。

   1 [2,4..100]

Rangeをつかうことで無限のリストを定義することができます。無限のリストを使うと何が良いのでしょうか。たとえば12の倍数の最初の100個からなるリストが欲しいとします。一つの書き方としては次のような書き方があります。

   1 [12,24...12*100]

しかし次のように書くこともできます。

   1 take 100 [12,24...]

下の書き方は、12の倍数からなる無限に続くリストを作って、その先頭100個を取り出すという書き方になっています。Haskellは遅延評価なので、無限リストが出てきてもそれ全体を評価しません。その代わりに無限リストの要素が必要になるまで待ちます。上の例では最初の100個の要素が必要になったので、その100個の部分だけを評価して結果として返します。
Rubyのような遅延評価でない言語で同様な書き方をすれば、無限リスト全体を評価しようとしてプログラムは止まってしまうでしょう。

無限リストが出てきたので、それにまつわる幾らかの関数を紹介しましょう。

cycle関数はリストを受け取って、その要素を無限に繰り返し、無限リストを生成する関数です。無限リストが湧いてくるので、takeなどを使って途中で打ち切らないとプログラムが止まらなくなります。

repeat関数はひとつの要素を受け取って、その要素のみが無限に繰り返される無限リストを作ります。

replicate関数はリストの長さと複製する要素を与えると、与えられた長さ分だけ要素を並べたリストを生成します。

   1 replicate 3 'a' ['a', 'a', 'a']が結果

リスト内包表記

リスト内包表記という新しい表現を覚えましょう。これを使うことでリストというデータ構造が更にパワーアップします。

みなさんは数学の集合の内包的記法というものに馴染みがあるでしょうか。たとえば

A = {2x | x∈N, x≦10}

のような書きかたです。このように書いた時、集合Aというのは「10以下の自然数を取ってきてそれぞれを2倍した結果」の集合になります。自然数を1から始まることにすれば、
A = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}ということになります。

このようなことをHaskellで殺りたいなら、take 10 [2,4..]と書くことができるでしょう。しかしリスト内包表記という表現を使えば、これを数学の集合の内包的記法っぽく書けます。

   1 [2 * x | x <- [1..10]]

上の表記をよく見てみましょう。[2 * x | x <- [1..10]]という表現は、リスト[1..10]から要素を取り出すということをいっています。x <- [1..10]という部分は、[1..10]からとりだした各要素の値をxが受け取るという意味です。
縦棒より前の部分はリスト内包表記の出力をあらわします。この部分ではリストから取り出した値を使ってどのようなリストを出力したいかを書きます。今の例だと取り出したものを2倍したいということを言っています。

今の例だとあまりリスト内包表記のありがたみがわからないかもしれません。リスト内包表記には条件(述語ともいいます)を追加することができます。条件はリスト内包表記の最後に書いて、他の部分とはカンマで区切ります。

たとえば10以下の自然数をとってきて、それを2倍した値が12以上になるようなものだけから構成されるリストをつくるには

   1 [2 * x | x <- [1..10], 2*x >= 12]

と書けばよいでしょう。50から500のうち7で割った余りが3であるような数のリストをつくるには

   1 [x | x <- [50, 500], x `mod` 7 == 3]

と書けばよいでしょう。

このように条件を使ってリストの間引きを行う操作はHaskellではちょくちょく見られる手法で、フィルタリングと呼ばれます。

条件はカンマで区切ることでたくさん並べることができます。

   1 [x | x <- [100..200], x /= 111, x /= 123, x/= 199]

条件だけではなく、取り出す対象のリストをたくさん並べることもできます。複数のリストから値を取り出すと、それらのリストの要素のすべての組み合わせが結果のリストに反映されます。

   1 [x+y | x<-[1,2,3], y<-[10, 100, 1000]]

リスト内包表記を使えばlength関数を自分で定義することもできます。

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

リストxsから何か一つ要素をとってきてそれを1に置き換えたリストをまず作ります。最後のそのリストをsumで全部足しあわせればリストの長さになる、という考え方です。
この例ではリストから取り出した値そのものはどうでもいいので、_という特別な変数名を使っています。_は値を読み出す必要はあるけど、それを後で使う必要はない、使い捨てをしたいときに使う変数名です。_のことをプレースホルダと呼ぶこともあります。

タプル

タプルは複数の違う型の要素を格納することができるデータ構造です。リストと雰囲気は似ていますが、根本的な違いが2つあります。
まずリストは同じ型しか入れることができませんが、タプルには違う型の要素を同時に入れることができます。もう一つはタプルは大きさが固定されているので、一度生成したら大きさを変えられないということです。

タプルを作るには、要素をカンマで区切って、丸括弧で囲みます。

   1 (1, 3)
   2 ('a', 3.14)

タプルの使い道

タプルはどのような所で有用でしょうか。たとえば2次元ベクトルを表すときにタプルは便利です。
Haskellで2次元ベクトルを表す時、まず思いつくのは2つの要素から成るリストを使うという方法です。この方法では三角形の頂点は

[[x1, y1], [x2, y2], [x3, y3]]

のように表現出来ます。

この方法の問題点は、

[[x1, y1, z1], [x2, y2], [x3, y3]]

のようなリストをつくることができて、しかもこれを2次元ベクトルのリストが期待されるような場所で使えてしまうということです。
上のリストは2次元ベクトルのリストとしての意味はまったくもって崩壊していますが、型が一致しているのでなんのエラーも吐かないというわけです。

これに対してタプルというのは大きさ2のタプルと大きさ3のタプルは違う型として扱われるので、大きさ2のタプルと大きさ3のタプルを同時に含むようなリストを作ることはできません。

また大きさが同じでも違う型を含んでいるようなタプルも別の型として扱われます。

タプルは一旦作ってしまうとその大きさを変えることができません。先ほど書いたとおり、タプルは大きさもその型を決定するための要素になっているからです。

ペア

大きさが2のタプルはよく使われるので、ペアという特別な名前が与えられています。ペアにまつわるいくつかの関数を紹介します。 fstとsndはそれぞれペアを受け取って、1つ目の要素と2つ目の要素を返します。

zip関数はペアのリストをつくるための関数です。2つのリストを受け取ってジッパーのように1つのリストにします。

   1 zip [1,2,3] [4,5,6]   [(1,4), (2,5), (3,6)]が結果

zip関数の引数のリストの長さが異なるときは、長い方は必要な部分だけ使われ残りは捨てられます。

演習

演習1

次の式の結果は何でしょう?

   1 succ 9 * 9

   1 succ (9 * 9)

演習2

リストを引数にとって、それぞれの要素が偶数なら"odd", 奇数なら"even"に置き換えるような関数、oddEvenをつくりましょう。

   1 oddEven [1, 2, 3, 4]

を実行すると

   1 ["odd", "even", "odd", "even"]

になります。

演習3

次の条件を満たすxとyをHaskellを使って求めてください。

解き方のヒント

  1. まずxとyの候補としてそれぞれ1から32の間の整数しか考えられないことに着目します。
  2. リスト内包表記を使って、xとyの候補のペアを洗いざらい生成します。
  3. 最後にx*y=32という条件をリスト内包表記に追加します。

alstamber/HaskellSeminar2 (last edited 2013-06-25 02:39:50 by alstamber)