関数型言語のモナド (3)
プログラム言語では、データを扱う場合、データの集合に名前を付けて扱うのが一般的です。
まずは、下の方にある Test1.hs まで一気に読み進めてみてください。
例として、生徒の「名前」、「学年」、「組」のデータを Student という名前を付けて扱う場合、以下のように定義します。
以下において、String というのは文字列を表し、Int というのは整数値を表します。
s1 は、2年1組に在籍する Bob を表すことになります。
Student String Int Int
s1 = Student "Bob" 2 1
上の例では、Student というキーワードでデータ値を生成できるようになるので、Student を 値構築子 と呼びます。
さらに、教員の「名前」と「担当科目」をデータ値として生成する 値構築子 Teacher も作る場合、以下のように定義します。
t1 は、英語を担当する Mary を表すことになります。
Teacher String String
t1 = Teacher "Mary" "English"
関数型言語では、データ構造の異なる Student と Teacher をまとめて扱うことができます。
Haskell においては、以下のようにすると、SchoolRoster としてまとめて扱うことができるようになります。
一般的なプログラム言語と同じく、縦棒「|」は、「または」という意味で利用されます。
data SchoolRoster =
Student String Int Int
| Teacher String String
上のようにすると、SchoolRoster は、「Student または Teacher のデータ値を扱う型」という定義になります。
関数型言語では、データ値を作り出す 値構築子 があり、1つ以上の値構築子をまとめて1つの型とし、その型を定義するものを 型構築子 といいます。
今回の例では、Student と Teacher が 値構築子 であり、SchoolRoster が 型構築子 となります。
ここで、実際にプログラムを動かしてみてほしいので、以下のプログラムを Test1.hs という名前でカレントディレクトリに保存してみてください。
今はモナドを理解することが目標であるため、Haskell の文法は気にしないでください。 Test1.hs で使われている文法については、後で解説をしていますので、関心のある方は是非読んでみてください。
-- Test1.hs
data SchoolRoster =
Student String Int Int
| Teacher String String
s1 = Student "Bob" 2 1
t1 = Teacher "Mary" "English"
f :: SchoolRoster -> String
f x = case x of
Student name _ _ -> name
Teacher name _ -> name
まず、カレントディレクトリに Test1.hs を作れたかどうか確認してみましょう。
ghci では、「:!」によってシェルコマンドを利用することができます。
Windows の場合は「:! dir」、Mac, Linux の場合は 「:! ls」 を入力してみてください。
ghci> :! ls (Windows の場合は :! dir)
...
Test1.hs (<- Test1.hs があることを確認してください。)
...
次に、Test1.hs をロードしてみましょう。ghci では「:l」または「:load」でファイルをロードすることができます。
ghci> :l Test1.hs
[1 of 2] Compiling Main
Ok, one module loaded.
Test1.hs で定義している関数 f は、SchoolRoster 型のデータを受け取り、名前を返す関数となっています。 実際に f を使ってみましょう
(注:関数型言語では、関数に値を適用する場合、かっこを書かないのが一般的です)
ghci> f s1
"Bob"
ghci> f t1
"Mary"
さて、ここで注目しておいてほしいのは、関数 f は、データ構造が Student であるものでも Teacher であるものでも受け取れている、ということです。
関数型言語では、Student や Teacher のようにデータ構造が異なるものでも、それらを1つの関数で受け取ることができます。
データの先頭に、Student "Bob" 2 1 のように値構築子を目印として付けているので、このようなことが実現できるようになっています。
Haskell の文法に関心がある方へ
関数型言語に関心がある方もいると思いますので、Test1.hs で利用されている文法について紹介します。 モナドのみに関心がある方は、次の章に進んでもらってまったく問題ないです。
関数型言語は、「バグを減らす」ことに重点を置いて設計されている言語 ですので、関数型言語を学ぶということは大変有意義なことだと思います。
自分で新しく 型構築子 と 値構築子 を作る場合には、data キーワード を用います。
data SchoolRoster =
Student String Int Int
| Teacher String String
1行コメントは「--」で表し、複数行コメントは、「{-」と「-}」で括ります。自分でコードを書き足してみる場合などに活用してください。
{-
関数型言語では、識別子(s1 や t1)に値を設定すると、その値は変更できません。
値が変更されないため、バグが減り、なおかつ最適化もしやすくなります。
-}
s1 = Student "Bob" 2 1
t1 = Teacher "Mary" "English"
-- したがって、下の行のように s1 に別の値を設定しようとすると、エラーが発生します。
s1 = Student "Alice" 1 3
上で注釈しているように、一般的な関数型言語では、一度値を設定すると、その値が変更できません。
そのため、C++ や java などで馴染みのある 「変数」や「代入」という言葉がありません。
変数や代入という言葉を使わずに、識別子 s1 に Student "Bob" 2 1 を 「束縛」させる、と言います。
プログラム中のどこで s1 を参照しても、いつでも Student "Bob" 2 1 を表すことになります。そういう意味で「束縛」なんですね。
関数について
1. 関数の型
例えば Int を受け取り String を出力する関数の型は Int -> String と表します。
2つの Int を受け取り String を出力する関数の型は Int -> Int -> String と表します。
今後、いろいろな関数を書きながら慣れていけばよいと思います。
2. 関数は必ず出力値を持つ
C++ や java などと異なり、関数は必ず出力値を持たなければなりません。
以下の例にあるように、関数型言語では、if も出力値を持ちます。
-- if も関数として扱われます。必ず出力値が必要となるため、else を省略することはできません。
ghci> f x = if x > 5 then x + 1 else x - 1
ghci> f 8
9
ghci> f 3
2
では、Test1.hs の続きを読んでいきましょう。
{- 以下の1行は、f の型を示していますが、これはプログラムを読みやすくするために書いているだけで、
書く必要はありません。一般的に、型は書かないことが普通です。-}
f :: SchoolRoster -> String
上の例のように、プログラムを読みやすくするために型の注釈を書く場合、:: を用います。 例えば、以下のように s1 などにも注釈を入れて書くことができます。
s1 :: SchoolRoster
s1 = Student "Bob" 2 1
-- 下のように書くこともできます。
s1 :: SchoolRoster = Student "Bob" 2 1
Haskell は、コンパイル時に型チェックが行われる型に厳しい言語です。 しかし、Haskell はほとんどの場合、型を自動で推論してくれるので、原則として型をプログラマが書く必要はありません。
型を書いた方が読みやすくなるときに限り、型を書くとよいと思います。
パターンマッチについて
関数型言語は、「バグを減らすため」に条件分岐をできるだけ見やすくしようとする努力 をしています。
そのため、条件分岐を行うためのパターンマッチの文法が豊富に用意されています。
そのうちの1つが case ... of であり、おおよそ見た目通りの動作となります。
以下のように書くと、A が B1 にマッチしたら C1 が評価され、B2 にマッチしたら C2 が評価される、という動作になります。
case A of
B1 -> C1
B2 -> C2
...
Bn -> Cn
case ... of の文法は簡単ですが、2つ気を付けるべき点があります。
どの分岐をしても結果の値が「同じ型」とならなければなりません。
パターンマッチは上の段から順番に実行されるため、B1 と B2 の順序を入れ替えると結果が異なってしまうことがあります。
では、Test1.hs の続きを読んでいきましょう。
f x = case x of
Student name _ _ -> name --(A)
Teacher name _ -> name
(A) では、x が Student String Int Int にマッチしたら、名前の文字列が出力値となるようにしています。
name と書く代わりに、Student n _ _ -> n と書いても意味は変わりません。
名前以外の情報は使わないため、_ という記号を用いて省略しています。
Teacher の方のパターンマッチも同様ですね。
この記事が気に入ったらサポートをしてみませんか?