見出し画像

関数型言語のモナド (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

カレントディレクトリが分かりにくい方へ
PowerShell やターミナルを利用している場合は、「Ctrl + D」を押すと ghci を抜けて、現在のカレントディレクトリが表示されます。

まず、カレントディレクトリに Test1.hs を作れたかどうか確認してみましょう。
ghci では、「:!」によってシェルコマンドを利用することができます。
Windows の場合は「:! dir」、Mac, Linux の場合は 「:! ls」 を入力してみてください。

ghci> :! lsWindows の場合は :! 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 の方のパターンマッチも同様ですね。

この記事が気に入ったらサポートをしてみませんか?