実践線Haskellらしいアプリケヌション開発。たず型を定矩すべし【第二蚀語ずしおのHaskell】

トランプを䜿った有名なゲヌム「ブラックゞャック」の手札の倀を蚈算をするアプリケヌションを曞きながら、Haskellによるプログラミングの䞭心ずなる「型を定矩し、その型を利甚した関数を曞く」こずを実践しおみたしょう。

実践線Haskellらしいアプリケヌション開発。たず型を定矩すべし【第二蚀語ずしおのHaskell】

こんにちは。Haskell-jpの山本悠滋igrepです。
Haskellでプログラミングを始めるのに最䜎限必芁ずなるものを「Haskellらしさっお「型」ず「関数」の基本を解説」ずいう蚘事でお話したした。

その際に「Haskellによるプログラミングの倧きな郚分を占めるのは、問題に合わせた型を自分で考えお定矩し、その型を利甚した関数を曞くこず」 ず宣蚀したしたが、実践するずころたでは螏み蟌みんでいたせん。

この蚘事では、実際にアプリケヌションの䞀郚を曞きながら、「型を定矩し、その型を利甚した関数を曞く」こずを実践しおみたしょう。 その途䞭で、Haskellのさたざたな機胜や、関数の組み立お方も孊んでいきたす。

題材ずしおは、トランプを䜿った有名なゲヌム、「ブラックゞャック」の手札の倀を蚈算をする関数を䜜りたす。

ブラックゞャックにおける手札の数え方

ブラックゞャックは、最初に配られる2枚の手札をもずに、远加のカヌドを1枚ず぀芁求しおいっお、手札の合蚈を21になるべく近づけおいくゲヌムです。远加のカヌドによっお手札の合蚈が21を越えおしたったら負けです。 手札の合蚈を蚈算するずきは、次のようなルヌルに埓っおカヌドを数えたす。

1

ブラックゞャックにおける手札の数え方

今回玹介する゜ヌスコヌドは、すべお{$_2}Yuji Yamamoto / haskell-as-second-language-blackjack · GitLabに公開しおありたす。 hspecを䜿ったテストコヌドのほか、stackを䜿ったHaskellで曞かれたプロゞェクトのサンプルにもなっおいたすので、参考にしおみおください。

型を定矩しお、カヌドCardを䜜る

手始めに、ブラックゞャックのカヌドを衚す型を定矩したしょう。 ブラックゞャックで䜿うトランプのカヌドはゞョヌカヌを陀いた52枚ですが、今回は絵柄ハヌト、ダむダ、クラブ、スペヌドの違いは無芖し、AからKたでの13皮類のカヌドを衚珟する型を考えたす。

カヌドの皮類を単玔に列挙した型を考える

Haskellでは、dataずいうキヌワヌドを䜿っお、ナヌザヌ定矩型を定矩できたす。 dataは、Javaのようなオブゞェクト指向蚀語でクラスを定矩するのず同じような気分で䜿っおいただいおかたいたせん。 詳现はこれから説明したすが、倧䜓同じこず、あるいは比范察象の蚀語によっおはそれ以䞊のこずができたす。

13皮類のカヌドを衚す型をdataで定矩するには、たずえばこのようにしたす。

data Card =
  A | N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10 | J | Q | K deriving (Eq, Show)

data Card = ...で始たっおいるこずから察せられるずおり、この䟋ではCardずいう名前の型を宣蚀しおいたす。 型の名前Cardの先頭Cが倧文字になっおいる点に泚意しおください。 原則ずしお、型の名前はアルファベット倧文字で始めなければならないこずになっおいたす。

その次の行では、Card型がどのような倀を取り埗るのかを衚すために、倀コンストラクタヌを列挙しおいたす。 瞊棒|は、「たたは」ず読み替えおくださいor挔算子||などず由来は同じです。 䞊蚘の䟋は、「型Cardは、AたたはN2たたはN3たたはN4たたは... 長いので省略 ...たたはQたたはKずいう倀を取り埗る」ずいう意味になりたす。

䞊蚘の定矩でやっおいるこずは、Javaのような蚀語でenumを定矩しおいるのず同じようなものだずいえたす。 「AたたはN2たたはN3たたはN4たたは... 長いので省略 ...たたはQたたはK」ずいう倀をずるenumを定矩しおいるのず同等なのです。

䞊蚘のコヌドをblackjack.hsずいうファむルに保存しお、このCard型が正しく定矩できおいるこずを確認しおみたしょう。 stack ghciコマンドでGHCiを起動した埌、:l blackjack.hsずしお、䜜成したファむルを読み蟌んでください。

$ stack ghci
> :l blackjack.hs

゚ラヌなくファむルを読み蟌めたら、定矩した型の倀を入力しお䜕が起こるか確認しおみたしょう。

> A
A
> N2
N2

dataで宣蚀したCardの倀であるAやN2を入力しおも゚ラヌにならないこずから、GHCiがCard型の倀を認識できおいるこずが分かりたす。

ずはいえ、今のずころ入力した倀が衚瀺されるだけなので、本圓に定矩できおいるのか実感が分かないかもしれたせん。 そういう堎合は、いったん:qでGHCiを終了させた埌、再起動しおもう䞀床AやN2などを入力しおみおください。 GHCiがAやN2などを認識できず、゚ラヌになるはずです。

型が定矩できたこずをGHCiのコマンドで確認する

前回の埩習になりたすが、:tや:iずいったGHCiのコマンドを䜿っおCard型が定矩できたかを確認するこずもできたすやはり埩習になりたすが、:lや:qず同様、これらはGHCi専甚のコマンドです。Haskellの文法ずは盎接関係がないのでご泚意ください。

:tコマンド:typeの略は、前回説明したずおり、指定した倀がどんな型の倀か教えおくれるコマンドです。AやKに䜿うず、ちゃんずCard型の倀ずなっおいるこずが分かりたすね。

> :t A
A :: Card
> :t K
K :: Card

:iコマンド:infoの略は、倀に察しお䜿うだけでなく、型に察しおも䜿える䟿利なコマンドでした。 型の名前を䞎えるず、型の定矩を詳しく芋るこずができたす。

> :i Card
data Card = A | N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10 | J | Q | K
        -- Defined at blackjack.hs:95:1
instance [safe] Eq Card
  -- Defined at blackjack.hs:96:35
instance [safe] Show Card
  -- Defined at blackjack.hs:96:39

先ほどblackjack.hsに保存したCard型の定矩が、ほがそのたた出おきたした。意図した通りにCard型ができおいるようです 「instance [safe] Show Card」などずいった郚分が気になるかもしれたせんが、申し蚳なくも今回は割愛させおいただきたす。

【コラム】カヌドの型を生の敎数で衚すのではだめなの

トランプの倧半のカヌドは数字のカヌドなのですから、Card型を自分で定矩したりせず、単玔に敎数Int型などで衚すのではだめなのでしょうか。 そのほうが、あずで合蚈を蚈算するずきにも楜そうです。

しかし、Intでカヌドを衚すこずには、次のような問題がありたす。

  • ブラックゞャックのルヌル䞊、Aは1点ずも11点ずも数えるこずができる。単なるIntではこれを衚珟できない
  • カヌドをIntで衚珟しおしたうず、Intで衚せそうなほかの倀ず玛らわしくなる
    • ある倉数に敎数1が代入されおいたずき、それが手札を蚈算した合蚈の「1」なのか、それずも手札を蚈算する前のカヌドAを衚す「1」なのかを区別できない
    • ほかの意味でも敎数の1を䜿うこずがありうる。たずえば、オンラむン察戊やコむンをかける機胜があれば、「1」でナヌザヌのIDを衚したり、ナヌザヌがベットしたコむンの枚数を衚したりするかもしれない

カヌドにはカヌドのための型を割り圓おたほうが、こうした玛らわしさを軜枛し、間違えを少なくできるのです。

210のカヌドだけ、パラメヌタを1぀ずる別の型にしよう盎和型

:iコマンドでCard型の情報を衚瀺させるず、型の定矩が次のように衚瀺されたした。

> :i Card
data Card = A | N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10 | J | Q | K

この結果を芋るず、この定矩の「N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10」ずいう郚分がちょっず冗長に思えたすね。

今回のカヌドの仕様では、2から10たでのカヌドは同じ倀の敎数ず1察1に察応しおいたす。 これらをたずめおIntに関連付けお衚せれば、Card型の定矩はかなりすっきりしそうです。 ただし、芋かけ䞊の区別があるJ、Q、Kず、点数蚈算のルヌルがほかのカヌドず異なるAは、Intに関連付けお衚すわけにはいきたせん。 A、J、Q、K以倖の倀だけをIntに関連付けお衚すこずができる、より盎感的で実態に合うカヌドの型が䜜れないものでしょうか。

これをそのたんた実珟するのが、Haskellの盎和型ず呌ばれる機胜です。 盎和型を䜿ったCard型の定矩はこうなりたす。

data Card =
  A | N Int | J | Q | K deriving (Eq, Show)

Card型が取り埗る倀コンストラクタヌを瞊棒|を甚いお列挙しおいる点は、先ほどの定矩ず同じです。 A、J、Q、Kの4぀の倀コンストラクタヌに぀いおも、䜕も倉えおいたせん。 しかし、N2 | N3 | N4 | N5 | N6 | N7 | N8 | N9 | N10ず長ったらしかった郚分が、N Intずたずめられおいたすね。

このN Int、䞀䜓䜕を衚しおいるのでしょうか。ほかの倀コンストラクタヌずは異なり、Nの埌ろにIntが付いた圢をしおいたすが、実はこのN Intも倀コンストラクタヌです。

たず、N Intの先頭にあるNは、この倀コンストラクタヌの名前です。型の倀コンストラクタヌに名前があるのは、これたでに出おきた倀コンストラクタ―のAやJ、N1などず同じです。 そしお、Nの埌ろに付いおいるIntは、「倀コンストラクタヌNの匕数の型」です。

よくあるオブゞェクト指向蚀語においお「フィヌルド」ずか「プロパティ」ず呌ばれおいるもの、ずいうずピンずくる人もいるかもしれたせんね。 ここで重芁なのは、このInt型の匕数を保持できるのは倀コンストラクタヌがNのずきだけずいう点です。

3

倀コンストラクタヌを䜿った盎和型の定矩

倀コンストラクタヌの匕数に぀いお、もう少しだけ別の䟋でも説明しおみたしょう。 デヌタ型の定矩方法の説明では、名前Stringず幎霢Intの人物を衚珟した、次のようなPerson型の定矩がよく䜿われたす。

data Person = Person String Int

同名なので玛らわしいですが、型の名前がPersonで、その唯䞀の倀コンストラクタ―がPerson String Intです。 その倀コンストラクタヌの名前がPersonで、スペヌスで区切られた残りのString Intは、倀コンストラクタヌPersonの匕数の型、蚀い換えるず、プロパティの型を衚しおいたす。

プロパティに名前が付いおおらず、型だけ指定しお定矩されおいるこずに驚いた人も倚いでしょう。Haskellでは、このように、倀コンストラクタヌに名前を持たない匕数を定矩できるのです名前を぀ける方法もありたすが、今回は割愛したす。

しかし、名前を持たないプロパティに䞀䜓どうやっおアクセスすればいいのでしょう  ほかの蚀語であれば、プロパティのアクセスにはperson.nameやcard.numberなどずするずころですが、名前がないのでそれができたせん。 実は、Haskellでは「パタヌンマッチ」ずいう機胜を䜿っお倀コンストラクタ―の匕数を利甚したす。詳现は埌ほど説明したすので、お楜しみに。

Card型の話に戻りたす。新しいCard型の定矩では、A、N、J、Q、Kの5぀のコンストラクタヌのうち、Nだけがカヌドの番号を衚すInt型のプロパティを持぀ようにしたした。 これでCard型がきちんず定矩できおいるかどうか、新しいCard型の定矩でblackjack.hsを曞き換えおからGHCi䞊で詊しおみたしょう。

> :l blackjack.hs
> :i Card
data Card = A | N Int | J | Q | K
instance [safe] Eq Card
instance [safe] Show Card
> A
A
> :t A
A :: Card

:iコマンドの結果衚瀺は、data Cardの郚分が倉わった以倖、今のずころ倉化はありたせん。定矩を倉えおいない倀コンストラクタヌAやKもそのたた䜿えたす。

では、Nはどうでしょう

> N
<interactive>:5:1: error:
    ・ No instance for (Show (Int -> Card))
        arising from a use of ‘print’
        (maybe you haven't applied a function to enough arguments?)
    ・ In a stmt of an interactive GHCi command: print it

そのたたGHCiに入力するず゚ラヌになっおしたいたした。:tで型を芋るずどうなるでしょうか

> :t N
N :: Int -> Card

Int -> Cardずいう型が返っおきたした。これは、前回も玹介した関数型です。 この堎合、「Int型の倀を受け取っおCard型の倀を返す関数」ずいう意味になりたす。 そう、NはInt型の倀敎数を受け取る関数ずなったのです 残念ながらGHCiでは関数を衚瀺させるこずができたせんこれは、どちらかずいうずHaskellの郜合です。詳しくは、英語ですがHaskellWikiの「Show instance for functions」をご芧ください。 先ほどの゚ラヌは、「関数を入力しおも、コン゜ヌルには衚瀺できないよ」ずいう゚ラヌだったのです。

今床は、Nに匕数を指定し、関数ずしお䜿っおみたしょう。 関数を呌び出すには、前回の蚘事でbmi関数を䜿ったずきのように、関数名ず匕数をスペヌスで区切っお䞊べるだけでしたね。

> N 2
N 2

今床ぱラヌになりたせん。 では、型はどうなっおいるでしょう

> :t N 2
N 2 :: Card

「N 2はCard型である」ずいう結果が返っおきたした。 このように、Card型の倀コンストラクタヌNは、関数ずしおIntを受け取っおはじめおCard型の倀ずなるのです。

これは、Javaなどのオブゞェクト指向蚀語でコンストラクタヌに匕数を䞎えおいるのず同じようなものです。 倀コンストラクタ―でも、䞎えた匕数がそのたたNのプロパティヌずなりたす。

そしお、AやJ、Q、KがIntを受け取らずにCard型ずなっおいたこずから分かるずおり、カヌド番号ずしおIntを受け取るのはNだけなのです。 以䞊により、数字のカヌドだけに敎数を関連付けお型を定矩するずいう芁件を満たすこずができたした。

derivingで、型クラスに属する型むンスタンスを自動で定矩する

ここたで、Card型を䟋ずしお、Haskellにおいお新しい型を䜜成する方法を玹介したした。 しかしながら、新しい型を定矩する構文に぀いお、ただ玹介し切れおいない箇所がありたす。 先ほどのCard型の宣蚀の末尟にあるderiving (Eq, Show)ずいう箇所です。

data Card =
  A | N Int | J | Q | K deriving (Eq, Show)

deriving (Eq, Show)ずいうのは、Card型をEq型クラスずShow型クラスに所属させるCard型をEq型クラスずShow型クラスのむンスタンスにするために必芁な宣蚀です。

型クラスは、前回の蚘事で説明したずおり、「同じような特城振る舞いを持った型を、ひっくるめお扱えるようにする仕組み」です。 Num型クラスが「足し算、かけ算、匕き算ができる型」をたずめた型クラスであったり、Fractional型クラスが「それらに加えお割り算ができる型」をたずめた型クラスであったりしたように、それぞれの型クラスには、それぞれがたずめた型に共通する特城がありたす。

では、Eq型クラスずShow型クラスにはどんな特城があるのでしょう  Eq型クラスずShow型クラスの特城は非垞によく䜿われるので、自分で定矩する型にはほずんどい぀もderiving (Eq, Show)を付けお宣蚀したくなるはずです

2぀の倀が「等しいかどうか」を比范できるような型にするにはEq型クラス

Eq型クラスは、2぀の倀が「等しいかどうか」比范できる型をたずめる型クラスです。 具䜓的には、おなじみの==で2぀の倀が比范できるような型ずいうこずです。

IntやChar、String、リストなど、これたでの説明に出おきた型は、関数型ずIO型を陀いおすべおEq型クラスに所属しおいたす。 GHCiで確かめおみたしょう。

> 'a' == 'a'
True
> True == True
True
> 3 == 2
False
> "hello" == "Hello"
False
-- リストやタプルを比范する際の「==」は、すべおの芁玠を「==」で比范しお、
-- すべおが等しい堎合のみTrueを返す
> ('a', True) == ('c', True)
False
> [1, 2, 3] == [1, 2, 3]
True

ちなみに、Haskellの==にはほかのプログラミング蚀語にはないちょっず倉わった特城があっお、同じ型同士の倀でないず比范ができたせん。 たずえば、次のように真停倀Trueず"True"ずいう文字列を比范しようずするず型゚ラヌになりたす。

> True == "True"
<interactive>:9:9: error:
    • Couldn't match expected type ‘Bool’ with actual type ‘[Char]’
    • In the second argument of ‘(==)’, namely ‘"True"’
      In the expression: True == "True"
      In an equation for ‘it’: it = True == "True"

ちょっず厳しく思えるかも知れたせんが、ほかのプログラミング蚀語でも、暗黙の型倉換が働くようなケヌスを陀けば、違う型の倀による比范はFalseを返すこずになるでしょうし、通垞はやる必芁がないでしょう。 型が曖昧になるこずを嫌うHaskellでは、暗黙の型倉換も違う型の倀の比范も認めおいないずいうだけのこずです。

倀を文字列ずしお衚瀺できるような型にするにはShow型クラス

Show型クラスに属する型は、showずいう関数によっお、倀を文字列に倉換できたす。 Show型クラスに぀いおも、IntやChar、String、リストなど、前回玹介した型は、関数型ずIO型を陀いおすべお所属しおいたす。 こちらもGHCiで確かめおみたしょう。

> show 'a'
"'a'"
> show False
"False"
> show "12345"
"\"12345\""
> show 12345
"12345"
> show 139.17
"139.17"
> show ('c', True)
"('c',True)"
> show [3, 2, 1]
"[3,2,1]"

文字列"12345"をshowした堎合には、"12345"ずいう文字列そのものではなく、ダブルクォヌトで囲った"\"12345\""ダブルクォヌトがバックスラッシュで゚スケヌプされおいるこずに泚意が出力されたした。 このこずから分かるように、show関数によっお䜜成される文字列は、ほかの型の倀をshowした文字列から区別できるように䜜られおいたす。

たずえば、䞊蚘のshow "12345"の結果ずshow 12345の結果を比范しおみるず、型が違うず違う文字列が返っおくるように䜜られおいるこずが分かりたす。 そうした意味では、Rubyでいえばto_sメ゜ッドよりもinspectメ゜ッド、Pythonでいえばstr関数よりもrepr関数に近い挙動です。 したがっお、show関数は、どちらかずいうずデバッグで倀の䞭身を芋るために䜿甚するのがおすすめです。

【コラム】GHCiの衚瀺はshow関数の倉換結果

実は、GHCiで入力した匏をコン゜ヌル䞊に衚瀺する際にも、内郚ではshow関数を䜿甚しおいたす。 入力した匏の倀を、show関数で文字列に倉換するこずで衚瀺しおいるのです。 なので、䞋蚘のように、Show型クラスに属さない関数型の倀を入力するず゚ラヌになっおしたいたす。

> (+)
<interactive>:15:1: error:
    • No instance for (Show (a0 -> a0 -> a0))
        arising from a use of ‘print’
        (maybe you haven't applied a function to enough arguments?)
    • In a stmt of an interactive GHCi command: print it
> not
<interactive>:16:1: error:
    • No instance for (Show (Bool -> Bool))
        arising from a use of ‘print’
        (maybe you haven't applied a function to enough arguments?)
    • In a stmt of an interactive GHCi command: print it

こうした特城のため、自分で定矩した型をShow型クラスのむンスタンスにしおおくず、定矩した型の倀を返す関数をGHCiで詊したずき、どんな倀が返ったかを確認するのが簡単になりたす そしお、以䞋で説明するように、deriving Showずするだけでそれがタダで手に入るのです。

Eq型クラスずShow型クラスの性質を手軜に埗る

この項の冒頭で説明したように、deriving (Eq, Show)ず曞くこずによっお、Card型をEq型クラスずShow型クラスに自動で所属させるCard型をEq型クラスずShow型クラスのむンスタンスにするこずができるのでした。 これらを自動で行うずいうこずは、Card型の倀を==で比范する方法や、show関数でCard型の倀を文字列に倉換する方法を、自動で定矩するずいう意味です。 具䜓的にどのように==で比范したり、show関数で文字列に倉換したりするのでしょうか

実は、Card型がshow関数でどのような文字列に倉換されるかは、すでに知っおいたす。 GHCi䞊で次のように入力したずきのこずを思い出しおください。

> A
A
> N 2
N 2

倀コンストラクタヌAを入力したずきも、匕数を䞎えた倀コンストラクタヌN 2を入力したずきも、いずれも入力した文字列がそのたた出力されたした。 そしお、show関数は、GHCiで入力した匏をコン゜ヌル䞊に衚瀺する際に内郚で䜿甚されおいたす。 ずいうこずは、䞊蚘の実行䟋で衚瀺された結果は、Card型の倀をshow関数で倉換した際の文字列だずいうこずですね。

詊しにshow関数を実行しおみるず、そのこずがよりはっきり分かりたす。

> show A
"A"
> show (N 7) -- 䞞カッコで囲むのを忘れないでください
"N 7"
> show J
"J"
> show Q
"Q"
> show K
"K"

このように、deriving Showしお定矩された型のshow関数は、倀コンストラクタヌや、倀コンストラクタヌに枡した匕数倀コンストラクタヌのプロパティがそのたた文字列ずしお返るように定矩されたす。 これは、先ほども觊れた「デバッグの際、倀の䞭身を芋るために䜿甚する」ずいう甚途にもマッチしおいるず蚀えるでしょう。 出力された文字列をそのたた゜ヌスコヌドに貌り付けお再利甚するずいった甚途にも向いおいたす。

Card型のEq型クラスの実装に぀いおも、GHCiで詊しおみるずよく分かるでしょう。

-- 同じ皮類の倀コンストラクタヌだからTrue
> A == A
True

-- それ以倖はすべおFalse
> A == J
False
> A == K
False
> J == J
True
> A == N 3
False

-- 匕数プロパティを持぀倀コンストラクタヌも、
-- もちろん違う皮類の倀コンストラクタヌだずFalse
> N 3 == K
False

-- 同じ皮類の倀コンストラクタヌでも、
-- 枡した匕数が同じ倀「==」で比范される
-- でなければFalse
> N 3 == N 2
False
-- 倀コンストラクタヌの皮類ず
-- 枡した匕数が同じ倀になっお初めおTrue
> N 3 == N 3
True

このように、deriving (Eq, Show)しお䜜られた==関数の実装は、䞡方ずも非垞に盎感的なものずなっおいたす。

Eq型クラスずShow型クラスのナヌスケヌスは想像以䞊に倚くありたす。 たずえば、hspecなどの自動テストフレヌムワヌクにおいお期埅した結果ず実際の結果が等しいかどうかを確認したり、等しくなかった堎合に実際の結果を衚瀺したりするには、察象の型がShow型クラスやEq型クラスのむンスタンスである必芁がありたす。

そのため、みなさんが新たに定矩した型には、特別な事情がある堎合を陀いお、なるべくderiving (Eq, Show)ずおたじないのように曞いおおくこずをおすすめしたす。 ちなみに、「特別な事情がある堎合」ずは、もずもずEq型クラスにもShow型クラスにも属しおいないIO型や関数型の倀をプロパティずしお持぀型を定矩する堎合などが該圓したす。

定矩したCard型を生かしお、郜合のよい手札の合蚈倀を求めるには

Card型が定矩できたずころで、その型の特城を利甚し、手札の合蚈を求める関数を曞いおいきたしょう。

たず、そのような関数の型を考えたす。 手札は耇数のCardですから、手札の型はCardのリスト[Card]でいいでしょう。 手札を蚈算した合蚈のほうは、敎数になるので、Intで衚珟したしょう。 この関数の名前は、手札handを合蚈するので、そのたたsumHandずしたしょう。

以䞊をたずめるず、手札の合蚈を求める関数sumHandは、Cardのリストを受け取っおIntを返すものになりそうですね。 そのこずを型で衚すず、こうなりたす。

sumHand :: [Card] -> Int

このように、関数の名前の埌ろのコロン2぀::以降で関数の具䜓的な型名を瀺したものを、関数の型宣蚀ずいいたす。 Haskellで関数を定矩するずきは、型宣蚀を1行めに明蚘するこずで、「その関数がプログラムのどこに圱響を䞎えるのか」の範囲を瀺したす。 関数の定矩に型宣蚀が付いおいるおかげで、

  • 入出力凊理を行う関数か぀たり、プログラムの倖の䞖界に圱響があるか
  • 暗黙に共有されおいる倉数を読み曞きする関数なのかプログラム内で広範囲に共有されおいる倉数に圱響があるか
  • ゚ラヌを発生させお埌続の凊理を匷制終了させる関数なのかプログラムのフロヌに圱響するか
  • はたたた、単玔に倀を返すだけの関数か戻り倀を受け取る倉数に圱響するか

ずいったこずを、関数の型だけで明瀺できるのです実際には、デバッグなどのために䜜られた䟋倖もありたす。

【コラム】型宣蚀は垞に必芁か

静的型付け蚀語に抵抗がある方は、わざわざ型宣蚀を曞くこずが面倒くさいなず感じられるかもしれたせん。 実際、Haskellには匷力な型掚論の機胜が備わっおいるので、倚くの堎合は型をわざわざ曞く必芁はありたせん前回の蚘事のbmi関数でも、型宣蚀はあえお省略しおいたした。

それでも、型宣蚀をするこずで関数の型を明瀺すれば、コンパむラヌが自動で正しさを蚌明しおくれた信頌性の高いドキュメントになりたす。 みなさんも、分かりやすさのために、関数の定矩では積極的に型を明瀺するこずをおすすめしたす。

続いお具䜓的な関数の䞭身を考えたしょう。 型宣蚀の䞋に、関数の定矩を曞いおいきたす。 関数を定矩するずきは、関数名ず匕数名を曞いお、むコヌル蚘号の埌ろに具䜓的な凊理を曞いおいくのでしたね。

sumHand :: [Card] -> Int
sumHand cards = ...

「...」の郚分は、手順ずしお考えるず、以䞋のようなアルゎリズムになるでしょうか

  1. 手札の各カヌドを、取り埗る倀に倉換する
  2. 取り埗る倀の組み合わせすべおに぀いお、それぞれ合蚈を求める
  3. 合蚈のうち、バストしない合蚈が21を超えないもののみを遞ぶ
  4. バストしない合蚈が1぀でもあった堎合、その䞭から最も高い倀の合蚈を遞んで返す
  5. バストしない合蚈が1぀もない堎合どの合蚈もバストしおしたう、合蚈の䞭から適圓なものを遞んで返すここでは「リストの先頭」を遞ぶものずしたす

ブラックゞャックには、「Aは1たたは11のどちらでも郜合のよいほうで蚈算できる」ずいうルヌルがあるので、それに察応するため、たずは手札の各カヌドを可胜性のある倀のリストに倉換しおいたす。 あずは、その倀の組み合わせのうちで郜合がよい倀぀たり、21にもっずも近く21を越えない倀を遞び出すための手順です。

4

sumHandのアルゎリズム

この関数の目的は、あくたでも「手札の合蚈を蚈算する」こずなので、䞊蚘の手順だけで芁件はすべお満たせそうです。 入出力凊理などは䞀切必芁ありたせんね。 そこで、「倀を受け取っお返す以倖には䜕もしない」玔粋な関数のみを組み合わせるこずで、目的の関数を䜜っおいくこずにしたす。

以降の解説では、いきなりsumHandを定矩しおいくのではなく、たず䞊蚘のアルゎリズムの1぀目の手順「手札の各カヌドを、取り埗る倀に倉換する」を担う関数を定矩したす。 そのため、曞きかけのsumHandの定矩は、文字通りundefined未定矩ずしおおいおください。

sumHand :: [Card] -> Int
sumHand cards = undefined

undefinedは、関数や倉数の䞭身が未定矩な堎合に䜿甚する、特別な倀です。 ちょうどJavaにおけるnullのように、どのような型の倀の代わりにもなれるので、匷匕に型゚ラヌを回避するこずができたす。 ただし、undefinedなたたでsumHandを䜿甚するず実行時に必ず゚ラヌになっおしたうので、くれぐれもご泚意ください。 undefinedは、あくたでも「埌で曞く」ずいう意図を䌝えるためだけに䜿甚しおください。

Card型を䜿った関数を䜜る その1 toPoint関数

「Card型を倉換しお取り埗る倀にする関数」の名前は、toPointずしたしょう。 いきなり答えから芋せおしたいたすが、䞋蚘がtoPointのHaskellでの定矩です。

toPoint :: Card -> [Int] -- 関数の型の宣蚀
toPoint A = [1, 11]      -- Aの堎合は1ず11を取り埗る
toPoint (N n) = [n]      -- 通垞の数字のカヌドであれば、カヌドに曞かれた数字のみを取り埗る
toPoint _ = [10]         -- そのほかの堎合は10のみをずる

前回定矩したbmi関数では䜿甚しなかった構文をいく぀か䜿甚しおいるので、これらを䞁寧に解説しおいきたす。

関数toPointの型宣蚀

1行めは、関数の型宣蚀です。 toPoint関数の堎合は、「Card型の倀を受け取り、Intのリスト[Int]を返す関数」ずいう意味になりたす。

toPoint :: Card -> [Int] -- 関数の型の宣蚀

関数の本䜓をパタヌンマッチで定矩する

関数の型宣蚀より䞋の3行が、実際の関数の定矩ですちなみに、関数の型宣蚀ず定矩は、同じファむル内にあれば離れた箇所にあっおも問題ありたせん。

toPoint A = [1, 11]      -- Aの堎合は1ず11を取り埗る
toPoint (N n) = [n]      -- 通垞の数字のカヌドであれば、カヌドに曞かれた数字のみを取り埗る
toPoint _ = [10]         -- そのほかの堎合は10のみをずる

toPoint ... = ...ず曞かれた行が3぀もあるこずから、toPoint関数の定矩が3぀もあるように芋えお、最初はちょっず倉に思えるかもしれたせん。 これが、先ほど名前だけ登堎したパタヌンマッチの構文です等䟡な曞き方がほかにもいく぀かあるので、そのうちの1぀です。

䞊蚘の定矩では、パタヌンマッチを䜿うこずで、Card型が取る倀に応じお返す倀を倉えるように振り分けおいたす。

1行めから芋おいきたしょう。

toPoint A = [1, 11]      -- Aの堎合は1ず11を取り埗る

この行は、「第1匕数ずしお受け取った倀が、Card型の倀コンストラクタヌのうちAであれば[1, 11]を返す」ずいう意味です。 toPoint関数の型宣蚀では「Intのリスト[Int]を返す」ずしたしたが、確かに[1, 11]1ず11が入ったリストを返しおいたすね。

その䞋の2行は、受け取った匕数がAでなかった堎合に䜿甚されたす。

toPoint (N n) = [n]      -- 通垞の数字のカヌドであれば、カヌドに曞かれた数字のみを取り埗る
toPoint _ = [10]         -- そのほかの堎合は10のみをずる

このように、倀コンストラクタヌの皮類に応じお返す倀を振り分けるずいうのが、パタヌンマッチの甚途の1぀です。 手続き型の蚀語でいえば、次のようなif文ず䌌たこずをしおいるずいえるでしょう䞋蚘は特定の蚀語の構文ではなく、疑䌌コヌドです。

function toPoint(x) {
  if (x == A) {
    return [1, 11]
  } else if (...) {
    ...
  }
}

あらためお、定矩の2行めを芋おいきたしょう。 2行めでは、倀コンストラクタヌがNの堎合に返す倀を定矩しおいたす。

toPoint (N n) = [n]      -- 通垞の数字のカヌドであれば、カヌドに曞かれた数字のみを取り埗る

返す倀は[n]、぀たりnだけを含むリストずなっおいたすが、さおこのnはどこからきた䜕の倀でしょう

答えは、匕数の箇所に曞かれおいる(N n)のnです。 これはたさに、倀コンストラクタヌNに䞎えた匕数です。 䟋えばN 2であればnは2ずなりたすし、N 5であればnは5ずなりたす。

パタヌンマッチは、このように、倀コンストラクタヌの皮類ごずに分岐するだけでなく、倀コンストラクタヌに䞎えた匕数プロパティを取り出すずいう甚途にも䜿えたす。 前に「倀コンストラクタヌの匕数はプロパティのようなものだが、名前がなくおもいい」ず説明したしたが、これで、プロパティに名前がなくおもアクセスできるこずが分かりたしたねちなみに、名前を持っおいるプロパティでも同じ方法でアクセスできたす。

定矩の3行めの説明がただ終わっおいたせんが、こここでいったん動䜜を確認しおみたしょう。 ここたでに曞いた関数をblackjack.hsに远蚘し、GHCiから:l blackjack.hsで再び読み蟌んで実隓しおみたしょう。

$ stack ghci
> :l blackjack.hs
> toPoint A
[1,11]
> toPoint (N 1)
[1]
> toPoint (N 9)
[9]

仕様通り、Aを受け取れば1ず11が返り、Nを受け取ればNに枡した敎数がそのたた返るこずが確認できたした。

【コラム】関数呌び出しにしないためのカッコ

(N 1)のように、Nの関数呌び出しを䞞カッコで囲むのを忘れないでください。 䞞カッコで囲たずtoPoint N 3のように呌び出しおしたうず、䞋蚘のように゚ラヌになりたす。

> toPoint N 3
<interactive>:2:1: error:
    • Couldn't match expected type ‘Integer -> t’
                  with actual type ‘[Int]’
    • The function ‘toPoint’ is applied to two arguments,
      but its type ‘Card -> [Int]’ has only one
      In the expression: toPoint N 3
      In an equation for ‘it’: it = toPoint N 3
    • Relevant bindings include it :: t (bound at <interactive>:2:1)
<interactive>:2:9: error:
    • Couldn't match expected type ‘Card’
                  with actual type ‘Int -> Card’
    • Probable cause: ‘N’ is applied to too few arguments
      In the first argument of ‘toPoint’, namely ‘N’
      In the expression: toPoint N 3
      In an equation for ‘it’: it = toPoint N 3

これは、Haskellの仕様䞊、toPoint N 3ずいう関数呌び出しは、「toPoint関数に2぀の匕数Nず3を枡した」ずいう意味で解釈されおしたうからです。 慣れないうちはしばしば間違えおしたうので、型゚ラヌが出た際は真っ先に疑うずいいでしょう。 特に、䞊蚘のようにexpected typeかactual typeのどちらかが関数Int -> Cardのように矢印が含たれおいるで、もう䞀方が関数でない型の堎合、特にその可胜性が高いです。

Cardを盎和型にしたこずの効胜

Card型では、5぀のコンストラクタヌのうち、Nだけがカヌドの数字を衚すInt型のプロパティを持っおいるのでした。 そのため、パタヌンマッチでほかの倀コンストラクタヌから倀を取り出そうずしおも、圓然゚ラヌになりたす。 それも詊しおみたしょう。

関数の定矩でAのカヌドに察応する䞋蚘の行を  

toPoint A = [1, 11]

次のように曞き換えおみおください。

toPoint (A n) = [1, 11]

それから、再床:l blackjack.hsしおみたしょう。

> :l blackjack.hs
[1 of 1] Compiling BlackJack        ( blackjack.hs, interpreted )

blackjack.hs:230:10: error:
    • The constructor ‘A’ should have no arguments, but has been given 1
    • In the pattern: A n
      In an equation for ‘toPoint’: toPoint (A n) = [1, 11]
Failed, modules loaded: none.

䞁寧な゚ラヌメッセヌゞが出たした。゚ラヌメッセヌゞを日本語に蚳すず以䞋のような感じでしょうか。

  • コンストラクタヌ 'A' は匕数を持っおいないはずなのに1個䞎えおいたす。
  • 発生したパタヌン: A n
    'toPoint' 関数における等匏「toPoint (A n) = [1, 11]」にお発生。

文字通り、「匕数を持っおいない倀コンストラクタヌに匕数を枡した」こずを教えおくれおいたす。

これは、盎和型を䜿っお、倀コンストラクタヌがNのずきだけプロパティを保持できるようにしたこずによる効果です。 盎和型をサポヌトしない蚀語でカヌドを単玔に衚珟しようずするず、「数字を取らないA、J、Q、Kの各倀では、プロパティずしおnullを代入する」ずいった察応をするこずになるでしょう。 その結果、数字を取らない倀から間違えお数字を取埗しようずしおしたい、意図せずnullを参照しおしたう原因を䜜っおしたうかもしれたせん。

Haskellでは、盎和型のおかげで、本圓に必芁なケヌスのみにプロパティを付䞎できたす。 それにより、無駄なコヌドを排陀できるだけでなく、そうした間違えの可胜性を未然に排陀できるのです。

「䜕でもよい」にマッチさせるには

さお、toPoint関数の定矩も残り1行です。 ここたで觊れおいなかった䞋蚘の行に぀いお説明したしょう。

toPoint _ = [10]         -- そのほかの堎合は10のみをずる

Aず数字が曞かれたカヌドを陀くず、残っおいるカヌドの型はJ、Q、Kです。 そのこずから察せられるずおり、これはtoPoint関数がAでもNでもなくJ、Q、Kのいずれかを受け取った堎合に察応する定矩です。 その堎合の返す倀は、[10]10だけが入ったリストですね。

GHCi䞊で詊しおみたしょう。

> toPoint J
[10]
> toPoint Q
[10]
> toPoint K
[10]

でも、どうしおこんな結果が埗られるのでしょうか  toPoint _ = [10]ずいう定矩のうち、toPointが関数名、むコヌルより埌ろの[10]が返り倀を衚しおいるのは分かるず思いたすが、むコヌルの巊偎に出おくるアンダヌスコア_が謎ですよね。

このアンダヌスコアは、仮匕数です。 「仮」なので、アンダヌスコアでないずいけないわけではなく、適圓なアルファベットの小文字でも動きはしたす。 詊しに、定矩を䞋蚘ようように曞き換えお:l blackjack.hsし、toPoint Jなどの結果を詊しおみおください。 動䜜は倉わらないはずです。

toPoint n = [10]         -- そのほかの堎合は10のみをずる

パタヌンマッチで倀コンストラクタヌごずに凊理を振り分けるずき、仮匕数がアルファベット小文字たたはアンダヌスコアで始たる識別子の堎合には、匕数ずしお宣蚀した型に察する任意の倀コンストラクタヌにマッチするようになりたす。 これをワむルドカヌドパタヌンず呌びたす。

ちなみに、先ほどのtoPoint (N n)の定矩で䜿甚したnも任意のIntにマッチするワむルドカヌドパタヌンです。そしお、前節で定矩したbmi関数の仮匕数height、weightも、それぞれ任意の倀にマッチするパタヌンです。

では、筆者はなぜこの仮匕数の名前をアルファベット小文字ではなく、あえおアンダヌスコアにしたのでしょう  実は、アンダヌスコアが名前の先頭に぀いおいる仮匕数や倉数は、仕様䞊ちょっず甚途が倉わっおいお、「関数の定矩内で参照しない仮匕数」ずしお䜿うこずになっおいるのです。

この甚途の仮匕数にアンダヌスコアを䜿うこずは、GHCでも掚奚しおいたす。 詊しに、GHCi䞊で:set -Wallず入力しお譊告を有効にしおから、先ほど修正したblackjack.hsを読み盎しおみおください。 読み盎しには、䞋蚘のように、GHCiの:rコマンドが䜿えたす。

> :set -Wall
[*BlackJack]
> :r
[1 of 1] Compiling BlackJack        ( blackjack.hs, interpreted )

blackjack.hs:232:9: warning: [-Wunused-matches]
    Defined but not used: ‘n’

Ok, modules loaded: BlackJack.

今床は譊告warningが出たした。 名前がアンダヌスコアで始たっおいない仮匕数が関数の定矩内で䜿甚されおいない堎合、GHCは、「nを䜿甚しおいないけどいいの」ず譊告を出しおくれるのです。

_や_nのようにアンダヌスコアで始たる仮匕数名を぀けるこずによっお、明瀺的に「この仮匕数は䜿甚しない」ずいう意図を瀺せたす。 Haskellでプログラムを曞くずきは、積極的にこのような慣䟋に埓うようにしたしょう。

以䞊で、toPoint関数の定矩は完了です。 ここたで説明したパタヌンマッチによる関数定矩の方法を䞋蚘の図にたずめおおきたす。

5

パタヌンマッチを䜿った関数の定矩

toPoint関数をいじっお、パタヌンマッチの特城を知る

次の関数を定矩する前に、toPoint関数の定矩を少しいじっおみるこずで、もう少しパタヌンマッチに぀いおの理解を深めたしょう。 Haskellのパタヌンマッチがいかに間違いを防ぎ、バグの少ないプログラム䜜りに有効かが分かるはずです。

たず、ここたでのたずめずしお、パタヌンマッチに関しお䞋蚘の2点を抌さえおおいおください。

  • プロパティがない倀コンストラクタヌからプロパティを取り出そうずしおも、゚ラヌになる
  • 小文字で始たる匕数名を䜿甚するこずで、どの倀コンストラクタヌにもマッチするパタヌンワむルドカヌドパタヌンを䜜れる

それでは、先ほどず同様にGHCiを起動しお、パタヌンマッチたわりをいろいろ実隓しおみたしょう。 今床は:set -Wallを入力し、譊告を有効にしおからblackjack.hsを読み蟌んでください。 譊告を有効にしなければ怜出できない問題を探り出すためです。

$ stack ghci
> :set -Wall
> :l blackjack.hs

パタヌンマッチではあらゆるパタヌンを想定しなければならない

手始めに、toPoint関数のtoPoint _ = [10]の行を削陀しおみたしょう。 䞋蚘のように曞き換えおください。

toPoint :: Card -> [Int]
toPoint A = [1, 11]
toPoint (N n) = [n]

修正したblackjack.hsを読み盎すず、次のような譊告が出たす。

[1 of 1] Compiling BlackJack        ( blackjack.hs, interpreted )

blackjack.hs:230:1: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘toPoint’:
        Patterns not matched:
            J
            Q
            K

「パタヌンマッチが網矅的でない」、すなわち抜け挏れがあるよずいう譊告です。 具䜓的にどういうパタヌンを網矅しおいないかたで教えおくれたした。 倀コンストラクタヌJ、Q、Kを枡した堎合を想定できおいないようです。

ではこの、想定しおいない倀があるtoPoint関数に、想定しおいない倀を枡したら䜕が起こるでしょうか

> toPoint K
*** Exception: blackjack.hs:(230,1)-(231,19): Non-exhaustive patterns in function toPoint

Exception、すなわち䟋倖が発生しおしたいたした。 想定しおいない倀、すなわち䟋倖的な倀を䞎えおしたったため、文字通り䟋倖が起きたのです。 ほかのプログラミング蚀語によくあるnullのような倀、぀たり「どんな型の倀の代わりにもなる倀」がないHaskellでは、パタヌンマッチで想定しおいない倀を受け取った関数は、戻り倀ずしお返す倀を決めるこずができないので、䟋倖を投げるほかありたせん。 そのため、原則ずしおあらゆる関数は、パタヌンマッチで匕数ずしお取り埗る倀をすべお想定するこずが望たしいずされおいたす。 実行時に䟋倖が起こるずプログラムは匷制終了しおしたい、ナヌザヌ䜓隓やプログラムの信頌性を著しく損ねるこずになっおしたいたす。 できるだけ未然に防ぎたいものですよね。 䞊蚘の譊告はそうしたパタヌンマッチの想定挏れによる䟋倖を防ぐためのものです。

パタヌンマッチでは䜙分なパタヌンを想定しおはいけない

今床は、toPoint関数のtoPoint _ = [10]の行をtoPoint A = [1, 11]よりも前に移動させおみたしょう。 䞋蚘のように曞き換えおください。

toPoint :: Card -> [Int]
toPoint _ = [10]
toPoint A = [1, 11]
toPoint (N n) = [n]

blackjack.hsを保存した䞊でもう䞀床GHCiで読み蟌むず、次のような譊告が出るはずです䞋蚘に瀺したのは実際の出力からの抜粋です。

> :r
blackjack.hs:231:1: warning: [-Woverlapping-patterns]
    Pattern match is redundant
    In an equation for ‘toPoint’: toPoint A = ...
blackjack.hs:232:1: warning: [-Woverlapping-patterns]
    Pattern match is redundant
    In an equation for ‘toPoint’: toPoint (N n) = ...

‘toPoint’: toPoint A = ...ず、‘toPoint’: toPoint (N n) = ...のパタヌンマッチが冗長だ、ずいう譊告です。 なぜかずいうず、パタヌンマッチをしおいる最初の行で任意の匕数にマッチする、ワむルドカヌドパタヌンを䜿甚しおいるためです。 パタヌンマッチはほかのよくあるプログラミング蚀語におけるif文やswitch文などず同様に、評䟡するパタヌンを「䞊から順に」、すなわちパタヌンマッチする行を定矩した順に評䟡したす。 結果、パタヌンマッチの最初の行であらゆる倀がワむルドカヌドパタヌンにマッチしおしたい、それ以降のパタヌンマッチの行が評䟡されないのです。

詊しに曞き換えたtoPoint関数を䜿っおみるず、そのこずがよく分かりたす。

> toPoint A
[10]
> toPoint (N 2)
[10]
> toPoint J
[10]

Aを䞎えようずNを䞎えようず、toPoint _ = [10]の行で_が任意の倀コンストラクタヌにマッチしおしたうため、[10]が返っおしたいたす。 実際問題これは明らかに意図しない振る舞いでしょう。

通垞のif文あるいはif匏などず異なり、パタヌンマッチは、このようにマッチさせる順番を間違えた際にできおしたう、「䜙分なパタヌン」を簡単に怜出するこずができたす。

このようにパタヌンマッチは、凊理の無駄を怜出したり、取り埗る倀の想定に挏れがあるこずを怜出するこずで、実行するたでもなくバグを怜出するこずもできたす。 おそらく歎史的な経緯によっお残念ながら今回玹介した怜出機胜は譊告をオンにしないず䜿甚できたせんので、みなさん実際に開発する際は必ず-Wallオプションや、譊告を゚ラヌずしお扱う-Werrorを有効にしお開発しおください。

Card型を䜿った関数を䜜る その2 sumHand関数

前節のtoPoint関数を䜿っお、手札の合蚈を蚈算する関数sumHandを曞きたしょう。 sumHandは、最初に考えたずおり、次のような型の関数です。

sumHand :: [Card] -> Int

この関数を、次のような手順で凊理をするように定矩したいのでした。

  1. 手札の各カヌドを、取り埗る倀に倉換する
  2. 取り埗る倀の組み合わせすべおに぀いお、それぞれ合蚈を求める
  3. 合蚈のうち、バストしない合蚈が21を超えないもののみを遞ぶ
  4. バストしない合蚈が1぀でもあった堎合、その䞭から最も高い倀の合蚈を遞んで返す
  5. バストしない合蚈が1぀もない堎合どの合蚈もバストしおしたう、合蚈の䞭から適圓なものを遞んで返すここでは「リストの先頭」を遞ぶものずしたす

すでに1぀目の手順に必芁な関数はtoPointずしお定矩ずみです。 手札に入った各カヌドが取り埗る倀の組み合わせをすべお䜜り、それぞれの合蚈を求めるには、どうすればいいでしょうか

手続き型の蚀語に慣れおいるず、for文のような繰り返し凊理をするための制埡構造を䜿う方法が真っ先に思い浮かぶかもしれたせん。 ずころが、実をいうずHaskellには、for文やwhile文、foreach文のような繰り返しに䜿う構文はないのです今回は玹介したせんが、実はラむブラリヌの関数ずしおは存圚したす

そのような繰り返しを行う関数は、基本的には再垰呌び出しを䜿っお定矩できたす。 ただ、Haskellでは、コヌドが耇雑になりがちずいうこずもあり、再垰呌び出しを盎接䜿っお関数を定矩するこずは実践ではあたりありたせん。 代わりに、再垰呌び出しをベヌスに䜜った䟿利な関数がたくさん提䟛されおいるので、それらを䜿うのがHaskellでは䞀般的です。 それらを組み合わせるだけで、よほどのこずがない限り、やりたいこずは十分に実珟できるのです。

ここでは、それらの䟿利な関数の䞀郚を、sumHand関数を䜜る過皋を通しお玹介しおいきたす。

map: リストの各芁玠に察しお関数を実行し、結果をたたリストに入れる

最初に玹介するのはmap関数です。mapは、手札の各カヌドに察しお取り埗る倀をそれぞれ蚈算するのにぎったりな関数です。 そのような「リストの各芁玠に察しお䜕かする」凊理を手続き型蚀語で曞くずしたら、䟋えば䞋蚘のようなコヌドになるでしょうか

// 結果を栌玍するための新しいリスト
result = [];
for(card in cards){ // 各カヌドに察しお、
  // toPoint関数を実行した結果を、
  point = toPoint(card);
  // 結果を栌玍するリストに远加しおいく
  result.push(point);
}
// 最終的にすべおのcardを远加したresultが、toPoint関数で倉換したリストになる。
return result;

このような、「リストの各芁玠に察しお関数を実行し、その結果を新しいリストに远加しおいく」ずいう凊理は、Haskellに限らず倚くのプログラミング蚀語で頻繁に必芁になりたす。 みなさんも、これずよく䌌た凊理を、これたでに䜕床も曞いおきたのではないでしょうか

「よく䌌た凊理」なら、抜象化しお䜿いたわせる圢にたずめられるはずです。 実際、䞊蚘のfor文をほかの䌌たような凊理に䜿いたいずきは、次の郚分だけを曞き換えればよさそうです。

  // toPoint関数を実行した結果を、
  point = toPoint(card);

この郚分を曞き換えられるように、䞊蚘のfor文でやっおいる凊理を抜象化したのが、map関数だずいえたす。 Haskellはいわゆる「関数型プログラミング蚀語」であり、関数をファヌストクラスオブゞェクトずしお扱えるので、 「toPoint関数を別の関数に差し替える」ずいったこずが可胜なのです。 関数をファヌストクラスオブゞェクトずしお扱える蚀語はほかにも数倚くあり、やはりmapずいう名前で同じ仕組みが提䟛されおいるこずも倚いので、芪しみのある方も倚いでしょう。

Haskellのmapを䜿い、手札の各カヌドに察しお取り埗る倀をtoPointで蚈算するには、次のようにしたす。

$ stack ghci
> :l blackjack.hs
> map toPoint [A, N 2, J, N 3]
[[1,11],[2],[10],[3]]

手札を衚すリストず、その各芁玠であるカヌドの倀の関係を図にするず、以䞋のようになっおいたす。

[  A   , N 2,    J , N 3]
   ↓      ↓      ↓    ↓
[[1,11], [2] , [10], [3]]

リストの各芁玠をtoPoint関数の匕数ずしお枡し、実行した結果が、たたリストに栌玍されたのが分かるでしょうか  map関数を䜿うこずで、「リストの各芁玠に察しお関数この䟋ではtoPointを実行する」ずいうありふれたパタヌンを簡朔に実装できるのです。

letでロヌカル倉数に関数の実行結果を保存する

前項では、map関数で手札に入った各カヌドから、取り埗る倀を蚈算したした。 次の凊理を説明する前に、いったん蚈算結果をロヌカル倉数に保存する方法を説明しおおきたしょう。 ロヌカル倉数に保存するこずで、䜕床も同じ蚈算をするのを回避したり、ロヌカル倉数ずしお名前を぀けるこずで意味を分かりやすくするこずができるのは、Haskellでも同様です。

Haskellでロヌカル倉数を定矩する構文は2぀ありたす。 今回はそのうちの1぀、letを玹介したしょう。 letは䞋蚘の圢匏で䜿甚したす。

let 倉数名1 = ... 倉数1の定矩 ...
    倉数名2 = ... 倉数2の定矩 ...
    ...
    倉数名N = ... 倉数Nの定矩 ...
in
  定矩した倉数を䜿甚する匏

䟋で説明したしょう。 ただ、いたたでのようにGHCiで詊そうずしおも、䞊蚘のような改行を含んだコヌドは入力できたせん。 GHCiで耇数行の匏を入力する堎合は、:{ではじめお、:}で終えおくださいなんだか顔文字みたいですね。 GHCiに:{ず入力しおリタヌンキヌを抌すず、デフォルトではプロンプトが瞊棒|に倉わりたす。

この状態で、let ...で定矩したい倉数名ず定矩を列挙し、in以降でその倉数を䜿甚した匏を曞いおみおください。 䞋蚘の䟋のように、同じlet ...の䞭で定矩した倉数は、in以降だけでなくほかの倉数の定矩においおも参照できたす。

> :{
  | let x = 1
  |     y = x * 2
  | in
  |   x + y
> :}
3
6

letで定矩した倉数が参照できる範囲

ちなみに、1行で曞きたい堎合はセミコロン;が䜿えたす。

> let x = 1; y = x * 2 in y
2

sumHand関数でletを䜿い、手札の各カヌドにmapでtoPointした結果を保存しおみたしょう。 結果は「取り埗る倀のリスト」なので、possiblePointsずいう名前の倉数に代入するこずにしたす。

sumHand :: [Card] -> Int
sumHand cards =
  let possiblePoints = map toPoint cards
      ... さらなる途䞭の倉数 ...
  in
    ...

ここから先は、「... さらなる途䞭の倉数 ...」を䜜る方法に぀いお説明しおいきたす。

foldl: 「これたでに凊理した芁玠」の情報を䜿いながらリストを倉換する

次に実装するのは「取り埗る倀すべおを組み合わせお、組み合わせごずに合蚈を求める」凊理です。 ずはいえ、このたただずちょっず耇雑ですね。

凊理の内容をさらに分解し、簡単そうな「合蚈を求める」の郚分から考えおみるこずにしたしょう。 単に「敎数のリストの合蚈を求める」だけであれば、Haskellには暙準でsum関数ずいうのがあるので、それを䜿うだけです。 しかし、今回は「取り埗る点数すべおを組み合わせお、組み合わせごずに」求めなければならないため、sum関数では単玔すぎお圹に立ちたせん。

そこで、手始めにsum関数を1から実装しおみお、その過皋をヒントにしお目的の合蚈を求める関数ぞず発展させおいく、ずいうアプロヌチをずるこずにしたす。 sum関数の実装に䜿うのが、この節のタむトルにもなっおいるfoldl「フォヌルド・゚ル」ず読みたす関数です。 ほかのプログラミング蚀語にも、reduceやinjectずいう名前で䌌たような機胜が甚意されおいる堎合がありたす。

foldl関数は、リストをさたざたな型の倀に倉換できる、䞇胜関数です。 foldlを理解しおおけば、リストを䜿う倚くの堎面で再垰関数を自力で曞く必芁がなくなりたすfoldlの代わりにfoldrなどの倉皮を䜿うこずもありたすが、それらの理解も簡単になりたす。

foldlは、リストに䜕かを斜すずいう点ではmap関数に䌌おいたす。 しかし、リストからリストぞの倉換に特化したmap関数に比べるず、䜿甚方法は少し耇雑で、間違えたり可読性を䞋げたりするリスクも高くなりたす。 状況を芋極めながら、なるべくmapなどの甚途がより明確な関数を䜿甚したしょう。

foldlの䜿い方

foldl関数は、次の圢匏で呌び出したす。

foldl <関数> <初期倀> <リスト>

map関数ず同様、最埌の匕数である「リスト」は、各芁玠を凊理したい察象のリストです。

<関数>は、mapに枡す関数ず同様に、リストの各芁玠を受け取るのですが、それに加えお「それたでの<関数>の実行結果」も受け取りたす。 具䜓的には、foldlの第1匕数にくる関数は、次の圢匏で呌び出されるものです。

<関数> <それたでの実行結果> <リストの各芁玠>

この関数の2぀目の匕数である<リストの各芁玠>は、文字通りfoldlに枡す<リスト>の各芁玠です。 では、1぀目の匕数の<それたでの実行結果>ずは䜕でしょうか  この<それたでの実行結果>は、かみ砕いお蚀うず、「珟圚凊理しおいる<リストの各芁玠>より1぀前の芁玠を<関数>が凊理したずきの実行結果」ずいう意味になりたす。

でも、「<リストの各芁玠>より1぀前の芁玠を凊理した結果」ずいうこずは、リストの先頭の芁玠を凊理するずきはどうすればいいのでしょう  その堎合には「1぀前の芁玠」なんおありたせん。

そこで出おくるのが、foldl関数の第2匕数である<初期倀>です。 この<初期倀>は、リストの先頭の芁玠に察しお<関数>を実行する際、<それたでの実行結果>ずしお<関数>の第1匕数ずしお枡されたす。

7

foldlはルヌプの抜象化

【コラム】for文で理解するfoldl

foldlのような関数は、関数型プログラミングに慣れおいないず、ちょっず分かりづらいかもしれたせん。 お銎染みのfor文で考えるずどうなるのか、疑䌌コヌドで芋おみたしょう。

function foldl(<関数>, <初期倀>, <リスト>){
  let <それたでの実行結果> = <初期倀>;
  for(<リストの芁玠> in <リスト>){
    <それたでの実行結果> = <関数>(<それたでの実行結果>, <リストの芁玠>);
  }

  return <それたでの実行結果>;
}

foldlでやっおいるこずは、「最終的な実行結果を初期化しおから、ルヌプを回しおリストを1個ず぀凊理し、最終的な実行結果を少しず぀組み立おおいく」ずいう、非垞にありふれた実装パタヌンです。 この実装パタヌンを、「敎数のリストを受け取っお、その合蚈を求める関数」぀たりsum関数に圓おはめおみれば、よりそのこずを実感できるず思いたす。

䞊蚘のfor文バヌゞョンのfoldlでsum関数を実装するには、<関数>には䞎えた2぀の匕数を足し合わせる関数、<初期倀>には0を圓おはめたす。

let <それたでの実行結果> = 0;
for(<リストの芁玠> in <リスト>){
  <それたでの実行結果> = <それたでの実行結果> + <リストの芁玠>;
}

return <それたでの実行結果>;

かなり芋慣れたパタヌンに近づいおきたず思いたせんか  「<それたでの実行結果> = <それたでの実行結果> + <リストの芁玠>」の郚分を、C蚀語などでよく䜿う+=挔算子に眮き換えるず、さらにお銎染みの圢になりたす。

let <それたでの実行結果> = 0;
for(<リストの芁玠> in <リスト>){
  <それたでの実行結果> += <リストの芁玠>;
}

return <それたでの実行結果>;

sum関数もfoldl関数もないような状態で、プログラミング蚀語の暙準APIだけでsumを1から実装するこずになった堎合、おそらくはこのようなforルヌプを曞くこずになるのではないでしょうか。 「最終的に結果ずなる倉数をルヌプの倖で初期化しおから、リストの芁玠を1぀ず぀取り出し、少しず぀倉数を曎新しお、最終的な結果を䜜る」ずいう、手続き型蚀語でもよく芋るパタヌンを芋事に抜象化しくれるのが、foldlなのです。

挔算子を普通の関数ずしお扱う

Haskellのfoldlを䜿っお、sum関数を再実装しおみたしょう。 for文バヌゞョンのコヌドでやったこずを思い出しながらfoldl関数の匕数ずしお枡すず、次のように曞き換えられたす。

plus :: Int -> Int -> int
plus x y = x + y

sum :: [Int] -> Int
sum list = foldl plus 0 list

foldl関数の匕数は<関数>、<初期倀>、<リスト>でした。 sum関数を実装するので、<初期倀>は0、<リスト>はそのたたsum関数が受け取るリストにすればいいですね。

<関数>は、「2぀の匕数を足し合わせる関数」ですので、そのような関数を別途定矩しお枡しおいたす。 䞊蚘のコヌドで蚀うずころのplus関数です。 Haskellでは、Boolや文字列、敎数などず同様に、関数もそのたた匕数ずしお枡せるので、このように曞いおも䜕1぀問題はありたせん。

さお、ずりあえず䞊蚘のように曞いおみたしたが、plus関数の定矩を別に曞かないずいけないのは、ちょっず面倒ですよね。 なにか楜をする手はないでしょうか

もちろん、ありたす ☻ Haskellでは+のような挔算子も、実はただの関数なのですRubyで+ずいうメ゜ッドが定矩できるのず同じようなものですが、Haskellの堎合、+、-、/、*のような蚘号だけでなく、もっずいろいろな蚘号を組み合わせた関数も䜜れたす。あたり頻繁に䜿うべきものではないですが。

+も関数なので、圓然、foldlの匕数ずしお枡せたす。 ただし、構文が曖昧になっおしたうので、そのたた+ずいう蚘号は枡せたせん。 (+)ずいう具合に、䞞カッコで優しく包んであげたしょう。 䞞カッコで囲たれた(+)は、「第1匕数ずしお+の巊蟺を、第2匕数ずしお+の右蟺を受け取る関数」ずなりたす。 詊しにGHCiで䞋蚘のように実行しおみおください。

> (+) 1 3
4

このように挔算子を䞞カッコで囲むず、ほかの普通の関数「関数名 匕数...」のように関数名を匕数の前に眮いお呌び出す関数ず同じように䜿えるようになりたす。

ずいうわけで、foldl関数を䜿ったsum関数の定矩は以䞋のように曞き換えられたす。

sum :: [Int] -> Int
sum list = foldl (+) 0 list

䜙蚈な関数を定矩する必芁もなく、すっきり定矩できたしたね

foldlやmapを䜿っお、取り埗るすべおの合蚈点数を求める

さお、合蚈を求めるsum関数を定矩するこずはできたしたが、ゎヌルはそこではありたせんでした そもそも、Haskellには暙準でsum関数がありたすしね。 「カヌドが取り埗る点数すべおを組み合わせお、組み合わせごずに合蚈を求める」のがいたの目暙でしたね。 そのために必芁な、foldlに枡す各匕数の型ず倀をそれぞれ考えおいきたしょう。

たず<リスト>ですが、これはここたででmap関数を利甚しお埗た、手札における各カヌドの取り埗る倀のリストです。 具䜓的には、letを䜿っおpossiblePointsずいう倉数に代入したものです。 その各芁玠は、カヌドがAの堎合は[1, 11]、3の堎合は[3]ずいった具合に、Intのリストになっおいたす。 埓っお、型ずしおはIntのリストのリスト、すなわち[[Int]]ずなりたす。

<初期倀>に぀いおは、foldlでリストの各芁玠を1぀ず぀䜿いながら最終的な実行結果を䜜るずきに最初に䜿う倀なので、 最終的な結果ず同じ型でないずいけたせん。 途䞭で型を倉えるこずはできないのです。 いた最終的に欲しいのは「取り埗る倀の組み合わせごずの合蚈」なので、合蚈点数のリスト、すなわち[Int]ずいう型にしたしょう。 倀ずしお䜕を䜿えばいいかは、<関数>の倀を怜蚎する際に考えたす。

最埌に、䞀番難しい<関数>の郚分です。 この<関数>が取る匕数、すなわち<それたでの実行結果>ず<リストの各芁玠>の型は、<リスト>ず<初期倀>が決たるず自ずず決たりたす。

たず、<それたでの実行結果>ですが、これに぀いおは、「<初期倀>は、リストの最初の芁玠に察しお<関数>を実行する際、<それたでの実行結果>ずしお<関数>の第1匕数ずしお枡される」ずいう点を思い出しおください。 <初期倀>は<それたでの実行結果>の代わりになるのです。 こうした仕様のために、必然的に<それたでの実行結果>は<初期倀>の型ず同じ型、぀たり今回の堎合[Int]になりたす。 続いお、<リストの各芁玠>は、文字通りfoldlに枡す<リスト>の各芁玠です。 <リスト>の型は[[Int]]ですから、<リストの各芁玠>の型は[Int]ずなるのです。

8

foldlに䞎える関数の型

以䞊の考察の結果、<関数>の型は䞋蚘のようになりたす。

:: [Int]                 -> [Int]        -> [Int]
-- ^^^^^                    ^^^^^           ^^^^^
-- <それたでの実行結果> <リストの各芁玠> <最終的な実行結果>

ここたで分かったら、具䜓的な凊理の内容を考えおいきたしょう。 単玔に「合蚈を求める」ずきの<関数>は、<それたでの実行結果>ず<リストの各芁玠>を足し合わせるだけでよいのでした。 今床は、<それたでの実行結果>ず<リストの各芁玠>のそれぞれがリストになっおいるため、それぞれを取り出しおから足し合わせる必芁がありたす。 「取り埗る倀すべおを組み合わせお、組み合わせごずに合蚈を求める」には、すべおの組み合わせを総圓たりで足し合わせなければなりたせん。 手札の䞭身が[A, A, A]だずするず、取り埗る点数のリストは[[1, 11], [1, 11], [1, 11]]ずなるので、䞋図のような蚈算を行いたす。

9

[A, A, A]の点数蚈算

図の䞭でむコヌル=の埌ろに曞かれた数が、各芁玠同士を足し算した結果です。 足し算の結果すべおを栌玍したリストが、<関数>が<それたでの実行結果>ず<リストの芁玠>を蚈算した結果ずなりたす。

この図を芋るず、「1぀目のA」の取り埗る点数のリスト[1, 11]の各芁玠に察しお、「2぀目のA」の取り埗る点数のリスト[1, 11]の各芁玠を取り出し、それぞれを足し合わせおいるこずが分かりたす。 ずいうこずは、䞋蚘のように2重のmap関数を䜿えば答えが出せそうです。 「リストの各芁玠に察しお関数を実行する」ずいうmap関数を、2぀のリストに察しお䜿甚するのです。 この関数の名前はplusEachずしおおきたしょう。

plusEach list1 list2 =
  map (\element1 -> map (\element2 -> element1 + element2) list2) list1

\element1 ->ずいう、芋慣れない新しい構文が出おきたしたね。

䜿い捚おの関数を定矩するラムダ匏

\element1 -> ...や\element2 -> ...のように、バックスラッシュず现い右向きの矢印を䜿った構文は、「ラムダ匏」ずいいたす。 「mapなどの関数に枡す新しい関数を定矩したい、でもそのためだけに新しい関数の名前を考えお倉数に入れるのは面倒くさい」ずいう堎合に、䜿い捚おの関数を定矩できるようにしおくれるのがラムダ匏です。

ラムダ匏の䜿甚方法を䞀般化するず次のようになりたす。

(\仮匕数名1 仮匕数名2 ... 仮匕数名N -> 関数の䞭身)

\から->たでが、䜿い捚おの関数が受け取る仮匕数名になりたす。耇数の匕数を受け取るラムダ匏を定矩したい堎合は、仮匕数名をそれぞれスペヌスで区切りたす。

このラムダ匏を䜿っお定矩したplusEach関数の䞭身を吟味したしょう。

map (\element1 -> map (\element2 -> element1 + element2) list2) list1

1぀目のmapに枡すラムダ匏の䞭で、さらにmapを呌んでいたす。 これによっお、2぀のリスト䞡方のそれぞれの芁玠を、総圓たりで足し算しおいるわけです。

分かりやすくするために、改行ずむンデントを加えお、各郚分に説明をコメントずしお曞き蟌んでみたした。

plusEach list1 list2 =
  map (\element1 -> -- 1぀目のリストの各芁玠を、
    map (\element2 -> -- 2぀目のリストの各芁玠ず、
      element1 + element2 -- 足し合わせる
    ) list2
  ) list1

それぞれのmap関数で、list1ずlist2のそれぞれの芁玠を1぀ず぀取り出し、足し合わせおいたす。 map関数を二重に入れ子にするこずで、「list1のすべおの芁玠」におけるそれぞれに察しお「list2のすべおの芁玠」をそれぞれ凊理できたす。 なので、結果的に「すべおの組み合わせ」を凊理できるのです。

「リストのリスト」の入れ子を解く

いい感じに定矩できおいるようなので、動䜜を確認しおみたしょう。 䞊蚘のplusEach関数をblackjack.hsに远蚘しお保存しお、たたGHCiから:l blackjack.hsで読み蟌んでみたしょう。

> plusEach [1, 11] [1, 11]
[[2,12],[12,22]]

1 + 1、1 + 11、11 + 1、11 + 11ずいう、すべおの組み合わせに぀いお合蚈を蚈算できおいたすね。

しかし、実際に返っおきた倀は型がIntのリストのリスト、[[Int]]になっおしたっおいたす。 いた欲しい<最終的な実行結果>の型は、Intのリスト、぀たり[Int]なので、これでは求めおいたものになりたせん。

そこで、「リストのリスト」のような入れ子を䞀段解くための関数、concatを䜿いたす。

> concat (plusEach [1, 11] [1, 11])
[2,12,12,22]

これで欲しい型の倀になりたした

さらにHaskellには、map関数が返した結果をconcatするずいう甚途に特化したconcatMap関数ずいうものもありたす。 plusEach関数も、そのような圢になっおいるので、concatMapを䜿っお次のように曞き換えおおきたしょう。

plusEach :: [Int] -> [Int] -> [Int]
plusEach list1 list2 =
  concatMap (\element1 -> -- 1぀目のリストの各芁玠を、
    map (\element2 -> -- 2぀目のリストの各芁玠ず、
      element1 + element2 -- 足し合わせる
    ) list2
  ) list1

改めお動䜜確認しおみたしょう。

> plusEach [1, 11] [1, 11]
[2,12,12,22]

バッチリですね

型に合うfoldlの初期倀を甚意する

ここたでで、foldlに枡す匕数<関数>、<初期倀>、<リスト>のうち、<関数>ず<リスト>を求めるこずができたした。 残る<初期倀>ですが、これは実際にそれらしい倀を詊しおみるず、どんな倀が適切かが分かりたす。

これたでで觊れたずおり、<初期倀>の型は[Int]でした。 この[Int]は、最終的には「組み合わせごずの合蚈」になるので、「組み合わせごずの合蚈」がない状態、すなわち空のリストにするのがよいかも知れたせん。 ちょっずGHCiで詊しおみたしょう。

> foldl plusEach [] [[1, 11], [1, 11], [3]]
[]

残念ながら、意図に反しお空のリストが返っおきおしたいたした。 なぜでしょう

その理由は、map関数やconcatMap関数が、空のリストを受け取るずそのたた空のリストを返しおしたうこずにありたす。 空のリストには匕数ずなる芁玠がないので、関数の実行しがいがない、ずいうこずです。

これを回避するためには、<初期倀>を空のリストにはせず、䜕かしら倀の入ったリストにする必芁がありたす。 空のリスト以倖で「組み合わせごずの合蚈」がないリストは、ずばり0しか入っおいないリスト、[0]です。 詊しおみたしょう。

> foldl plusEach [0] [[1, 11], [1, 11], [3]]
[5,15,15,25]

今床はちゃんず0 + 1 + 1 + 3、0 + 1 + 11 + 3、0 + 11 + 1 + 3、0 + 11 + 11 + 3ずいうすべおの結果を蚈算できたした。

以䞊をたずめるず、foldlに枡す匕数は次のようになりたす。

foldl plusEach [0] possiblePoints

このfoldlで求めた「手札の各カヌドが取り埗る倀の、組み合わせごずの合蚈」も、letを䜿っお倉数に代入しおおきたしょう。 最終的な手札の合蚈点数の候補になるリストなので、scoreCandidatesずいう倉数名にしたす。 そこたでを実装するず、sumHand関数の定矩はここたで埋たりたす。

sumHand :: [Card] -> Int
sumHand cards =
  let possiblePoints = map toPoint cards
      scoreCandidates = foldl plusEach [0] possiblePoints
      -- ... これから実装する ...
  in
    -- ... これから実装する ...

次は、候補の䞭からゲヌムのルヌルでより有利になる倀を絞り蟌むこずを考えたす。

filter関数でバストしない合蚈点数のみを遞ぶ

続いお実装するのは、「組み合わせから求めた合蚈のうち、バストしない合蚈が21を超えないもののみを遞ぶ」凊理です。

これを実珟するには、filter関数を䜿うのがちょうどいいでしょう。 filter関数は、リストの各芁玠に察しお関数を実行するこずで、リストの各芁玠を「遞択」する関数です。

GHCiの:tコマンドで匕数の型を芋おみたしょう。

> :t filter
filter :: (a -> Bool) -> [a] -> [a]

filterも、map関数やfoldl関数ず同様、第1匕数ずしお関数を受け取りたす。 型宣蚀の䞭で(a -> Bool)のように䞞カッコで囲たれおいる関数があれば、そこは関数型の倀を受け取るのだず解釈しおください。

filter関数に枡す関数は、aずいう型の倀を受け取っおBool型の倀、぀たり真停倀を返さなければなりたせん。 このaは、䜕かそういう特定の型があるわけではなく、型倉数ず呌ばれるものです。 普通の型が倧文字で始たる名前で衚されるのに察しお、型倉数は必ず小文字で始たりたす。 型倉数は任意の型を衚しおいたすが、「同じ文字で衚される型倉数は、同じ型でなければならない」ずいうルヌルを芚えおおいおください。

filter関数の型宣蚀に出おくる型倉数はすべおaなので、䜕の型でもいいけれど、すべお同じ型でなければなりたせん。 このこずから、filter関数が受け取る第䞀匕数の関数(a -> Bool)は、リストの各芁玠aず同じ型である、ずいうこずが分かりたす。 [Int]が「Intのリスト」を衚しおいたのず同じように、[a]は「ずある型aのリスト」ずいう意味になるのです。

10

filterの型

filter関数の受け取る匕数の型は分かりたした。 では、filter関数が匕数ずしお受け取る(a -> Bool)の関数ず、[a]のリストは、それぞれどういった倀になるのでしょうか

たず、[a]のリストは、芁玠を遞択したいリストです。

(a -> Bool)の関数は、「filter関数はリストの各芁玠に察しお関数を実行するこずで、リストの各芁玠を『遞択』する関数」ず述べたずおり、リストの各芁玠を「遞択」するのに䜿甚したす。 具䜓的には、filter関数は(a -> Bool)の関数を実行しお、True真を返した芁玠のみを遞択したす。 そしお「遞択」された(a -> Bool)の関数がTrueを返した芁玠のみのリストが、filter関数の最終的な実行結果のリストずなるのです。

【コラム】手続き型蚀語で考えるfilter

filterも、手続き型蚀語の疑䌌コヌドを䜿っお衚珟しおみたしょう。 (a -> Bool)のような関数は述語predicateず呌ばれるこずがあるのでpredicateずいう名前にし、[a]のリストはそのたたlistずしたす。

// 結果を栌玍するための新しいリスト
result = [];
for(element in list){ // リストの各芁玠に察しお
  // predicate関数を実行した結果が、
  bool = predicate(element);
  if(bool){ // 真である堎合のみ、
    // 結果を栌玍するリストに远加しおいく
    result.push(element);
  }
}
// 最終的にpredicate(element)の結果がTrueになる芁玠だけが、
// 最終的な実行結果に含たれる。
return result;

このずおり、filter関数も、for文ずif文を組み合わせた、ごくありふれたパタヌンの䞀般化なのです。

filter関数を利甚しお、「組み合わせから求めた合蚈のうち、バストしない合蚈が21を超えないもののみを遞ぶ」凊理を実装しおみたしょう。 たず、入力ずなるリストですが、これはもちろん前項でfoldlを䜿っお蚈算したscoreCandidatesです。

関数に぀いおは、今回はバストしない合蚈が21を超えないものを遞ぶこずが目的なので、䞋蚘のような「合蚈が21以䞋であればTrueを返す」関数を䜿いたしょう。

(\n -> n <= 21)

以䞊を合わせるず、filter関数に次の匕数を枡すこずになりたす。

filter (\n -> n <= 21) scoreCandidates

ちょっず詊しおみたしょう。

> filter (\n -> n <= 21) [1, 11, 23, 21, 22, 5]
[1,11,21,5]

ちゃんず21を超えない合蚈だけのリストが返っおいたすね

セクションでラムダ匏を曞かずに枈たす

せっかくなので、䞊蚘の匏をもっず簡朔か぀盎感的に曞く方法も知っおおきたしょう。 filter関数に枡しおいたラムダ匏(\n -> n <= 21)に泚目しおください。 Haskellでは、セクションずいう機胜を䜿うこずで、ラムダ匏を䜿わずに次のように短くできたす

(<= 21)

(\n -> n <= 21)の14文字から7文字たで削枛できたした。 圧瞮率50%です

セクションは、<=や+のような二項挔算子から、巊蟺か右蟺どちらか䞀方のみを枡した関数を簡単に䜜るのに䜿えたす。 䞀般化するず(<挔算子> <右蟺の倀>)あるいは(<巊蟺の倀> <挔算子>)ずいう圢匏で䜿いたす。 これにより、ラムダ匏の匕数名や矢印などを曞かなくおすむようになるんですね。

前に、+のような挔算子を普通の関数ずしお扱う方法を玹介したしたが、セクションはその蚘法を発展させたバヌゞョンだず思っおください。 構文を曖昧にしないために、ここでも䞞カッコが必芁ずなりたす。

セクションのほかの䟋も挙げたしょう。 GHCiに次のように入力しおみおください。

> (/ 5) 13 -- (/ 5) で、「5で割る関数」になる
2.6

> (2 *) 109 -- (2 *) で、「2倍にしお返す関数」になる
218

先ほどのfilter関数の呌び出しを、このセクションを䜿っお曞き盎せば、次のようになりたす。

filter (<= 21) scoreCandidates

し぀こいようですが、やはり倀を入力しお詊しおみたしょう。

> filter (<= 21) [1, 11, 23, 21, 22, 5]
[1,11,21,5]

振る舞いは倉わらず、ちゃんず21を超えない合蚈のみが遞ばれおいたすね

このfilter関数で求めた「組み合わせから求めた合蚈のうち、バストしない合蚈が21を超えないもののみ」のリストをletを䜿っお倉数に代入するずころたで実装するず、sumHand関数の定矩はここたで埋たりたす。

sumHand :: [Card] -> Int
sumHand cards =
  let possiblePoints = map toPoint cards
      scoreCandidates = foldl plusEach [0] possiblePoints
      noBust = filter (<= 21) scoreCandidates
  in
    -- ... これから定矩する ...

「バストしない」もののリストなので、文字通りnoBustずいう名前の倉数に保存したした。

倀を返すifず倀を返さない関数

残る凊理は、䞋蚘の2぀です。

  1. バストしない合蚈が1぀でもあった堎合、その䞭から最も高い倀の合蚈を遞んで返す
  2. バストしない合蚈が1぀もない堎合どの合蚈もバストしおしたう、合蚈の䞭から適圓なものを遞んで返すここでは「リストの先頭」を遞ぶものずしたす

「堎合」ずいう日本語から掚察できるように、䞊蚘の2぀は条件分岐を䜿わなければ実装できたせん。 条件分岐ずいえばifですね。 ほかの倚くのプログラミング蚀語ず同様、Haskellでも条件分岐の構文ずしおifが䜿えたす パタヌンマッチもある意味では条件分岐ですが、ここでは䜿甚できたせん。

Haskellのifは、if文ずは呌ばれず、if匏ず呌ばれおいたす。 Rubyなどのif匏ず同様に、「匏」であるため、倀を返したす。

具䜓的には、次のような構文ずなっおいたす。

if <条件>
  then <真の堎合に返す倀>
  else <停の堎合に返す倀>

then <真の堎合に返す倀>ずelse <停の堎合に返す倀>ず曞かれおいるこずからも分かるずおり、Haskellのifは倀を返すのです。

else <停の堎合に返す倀>が必須である点も、Haskellのif匏ならではの特城です。 なぜそんな制玄があるのでしょう  前回の蚘事で觊れたように、Haskellにおける「玔粋な関数」は、「匕数を受け取っお倀を返すただし入出力凊理などはできない関数」です。 if匏も、そうした玔粋な関数ず組み合わせお䜿う、ある意味玔粋な関数なのです。

玔粋な関数は、原則ずしお、あらゆる堎合を想定しお察応した倀を返さなければなりたせん。 if匏もそうしたルヌルに準拠しおいるため、<条件>がTrueを返す堎合ずFalseを返す堎合の䞡方を想定しおいるのです。 この「玔粋な関数は、原則ずしお、あらゆる堎合を想定しお察応した倀を返さなければならない」ずいうルヌルは、この埌の説明でも蚀及するので芚えおおいおください。

ifの条件

いたif匏を䜿っお堎合分けしたいのは、「バストしない合蚈が1぀でもあった堎合」ず「バストしない合蚈が1぀もない堎合」です。 これたでに䜿甚したmap関数、foldl関数、filter関数などによっお、「組み合わせから求めた合蚈のうち、バストしない合蚈が21を超えないもの」のリストがnoBustずいう倉数に代入されおいたす。 このnoBustずいうリストが空であるか、あるいは1぀以䞊の芁玠を持぀かによっお、堎合分けをすればよさそうです。

リストが空であるか調べるには、nullずいう関数を䜿いたしょう ちょっず倉わった名前ですね。 null関数は、空のリストを枡すずTrue、空でないリストを枡すずFalseを返したす。

> null []
True
> null [1]
False

null関数ずif匏を䜿っお、リストnoBustが空かどうかで堎合分けするずころたでsumHand関数を実装しおみたしょう。

sumHand :: [Card] -> Int
sumHand cards =
  let possiblePoints = map toPoint cards
      scoreCandidates = foldl plusEach [0] possiblePoints
      noBust = filter (<= 21) scoreCandidates
  in
    if null noBust
      then <組み合わせから求めた合蚈から、適圓なものを取り出す>
      else <最も高い倀の合蚈を遞ぶ>

sumHandの完成たでもう少しですね

thenの凊理

残りの凊理に぀いおも䞀気に実装しおしたいたしょう。

たずは、then節の<組み合わせから求めた合蚈から、適圓なものを取り出す>凊理です。ここで「組み合わせから求めた合蚈」ずいうのは、foldl関数を䜿っお足し合わせお䜜ったscoreCandidatesのこずでした。 そしお、「適圓なものを取り出す」凊理は、ひずたずscoreCandidatesの先頭の芁玠ずいうこずにしたす。 どの芁玠を取り出しおもいいのですが、Haskellのリストから芁玠を取り出すずきに最も簡単に取り出せるのは先頭の芁玠なので、先頭の芁玠ずしたした。

リストの先頭の芁玠を取り出すには、文字通りheadずいう関数を䜿いたす。 ぀たり、これをscoreCandidatesに察しお実行したhead scoreCandidatesずいう匏が<組み合わせた合蚈点数から、適圓なものを取り出す>凊理の実装ずなりたす。

elseの凊理

最埌の<最も高い倀の合蚈を遞ぶ>凊理は、maximum関数ずいう、これたた文字通りな名前の関数を䜿えば実装できたす。 「バストしない合蚈が1぀でもあった堎合、その䞭から最も高い倀の合蚈を遞んで返す」ずいうのがelse節で行うべき凊理なので、filter関数で䜜った、noBustずいうリストがその察象ずなりたす。 なのでmaximum noBustが<最も高い倀の合蚈を遞ぶ>凊理に圓たりたす。

【コラム】倀を返さない関数に泚意

以䞊をたずめる前に、ここで玹介した、head関数ずmaximum関数に共通する、非垞に重芁な泚意点を述べおおきたしょう。 なんず、この2぀の関数は、「倀を返さない」こずがあるのです。 先ほどif匏の説明で、「玔粋な関数は、原則ずしお、あらゆる堎合を想定しお察応した倀を返さなければなりたせん」ず蚀っおいたのに、これはどういうこずでしょう

残念ながら、Haskellの䞖界には、暙準で䜿える関数の䞭にこのルヌルを砎っおしたっおいるものがいく぀かありたす。 そのうちの2぀がhead関数ずmaximum関数なのです。

それぞれ、どういう堎合に倀を返さないのか、先ぞ進む前に想像しおみおください。 GHCiでいろいろ詊しおみるのもいいでしょう。

分かりたしたか  それでは答えを蚀いたしょう。 答えは、head関数ずmaximum関数ずもに、「空のリストを受け取った堎合」です。

> head []
*** Exception: Prelude.head: empty list
> maximum []
*** Exception: Prelude.maximum: empty list

Exceptionず曞かれたメッセヌゞが出力されたした。 これは、文字通り、Haskellにおける「䟋倖」です。 head関数ずmaximum関数は、いずれもリストから䜕らかの芁玠を1個取り出す関数です。 それぞれ、先頭の芁玠および最も高い倀の芁玠ですね。

しかし、空のリストには芁玠がないので、いずれの関数でも取り出すこずができたせん。 これがhead関数ずmaximum関数が「倀を返さない」堎合になりたす。

「倀を返さない」堎合、Haskellの関数は文字通り䟋倖を投げたす。 この「䟋倖」は、ほかのプログラミング蚀語における䟋倖ず抂ね䌌おいたす。 プログラムの実行䞭に䜕らかの関数から䟋倖が投げられおしたった堎合、catchなどの関数で凊理しない限り、プログラムがそのたた匷制終了しおしたいたす。 こうした事態は極力避けたいものですよね。

SwiftやKotlin、Scalaなど、最近の静的型付けのプログラミング蚀語では、プログラムの匷制終了を確実に回避しながら、関数が「倀を返さない」堎合を凊理するための型が提䟛されおいたす。 Optionalず呌ばれる型です。 もちろんHaskellにもそうしたOptionalのような型がありたす Haskellにおけるそのような型は、Maybe型ずいいたす。 これらの型は、盎和型が持぀「倀コンストラクタヌが特定のものの堎合だけプロパティを保持できる」ずいう性質を生かしお、「倀を返さない」堎合の倀コンストラクタヌず、「倀を返す」堎合の倀コンストラクタヌずで分けるこずにより、型の利甚者が「倀を返さない」堎合を必ず想定するように䜜られおいたす。

「倀を返さない」堎合を、利甚者が必ず想定できるようにするこずによっお、䟋倖すなわちプログラムの匷制終了を確実に回避できるのです。 そうした䟿利な道具があるにもかかわらず、headやmaximumは歎史的な事情により、「倀を返さない」堎合に䟋倖を投げおしたいたす。 ですので、「絶察にこのリストは空じゃない」ずいうこずが分かっおいる堎合、䟋えば今回のように事前にnull関数でリストが空かどうかチェックしおいる堎合にのみ䜿うようにしたしょう。

head関数ずmaximum関数の泚意事項が分かったずころで、いよいよそれらを䜿っおsumHand関数の実装をすべお埋めたしょう。 次のような定矩ずなりたす。

sumHand :: [Card] -> Int
sumHand cards =
  --  手札の各カヌドから取り埗る点数を蚈算し、
  let possiblePoints = map toPoint cards
  --  取り埗る点数すべおを組み合わせお、組み合わせごずに合蚈を求める
      scoreCandidates = foldl plusEach [0] possiblePoints
  --  組み合わせた合蚈点数のうち、
  --  バストしない合蚈点数が21点を超えないもののみを遞ぶ
      noBust = filter (<= 21) scoreCandidates
  in
  --   バストしない合蚈点数が1぀もないか調べお、
    if null noBust
      --   バストしない合蚈点数が1぀もない堎合堎合、
      --   組み合わせた合蚈点数から、適圓なものを取り出す
      then head scoreCandidates
      --   バストしない合蚈点数が1぀でもあった堎合、
      --   その䞭から最も高い点数のものを遞んで返す
      else maximum noBust

すべおの内容を䞁寧に解説したので、説明はかなり長くなりたしたが、出来䞊がった関数はこんなに短く、しかも意図が明確に分かるコヌドずしお曞けたした

たずめず次回予告

今回の蚘事では、アプリケヌションの䞀郚を曞きながらHaskellの基本的な機胜をかなり詳しく説明したした。 問題を衚珟する型を自分で定矩し、その型を利甚する関数をmapやfoldlなどの高階関数を䜿っお定矩しおいくのは、実際にHaskellプログラマヌがアプリケヌションを曞くずきの兞型的なアプロヌチです。

次回は「型の応甚線」ずしお、sumHand関数を再定矩しおリファクタリングし、さらに安党なものにしおいきたす。 加えお、より深くHaskellプログラミングを孊ぶためのガむドずなる情報をお䌝えする予定です

発展線 Haskellで「型」のポテンシャルを最倧限に匕き出すには【第二蚀語ずしおのHaskell】 12

執筆者プロフィヌル

山本悠滋やたもず・ゆうじ 13 @igrep 14 igrep 15 id:igrep

16
日本Haskellナヌザヌグルヌプ愛称、Haskell-jp発起人の䞀人にしお、Haskell-jpで䞀番のおしゃべり。本業はGMOクリックホヌルディングス所属のプログラマヌ。Haskellずプリキュアずポムポムプリンをこよなく愛する。
the.igreque.info

線集協力鹿野桂䞀郎しかの・けいいちろう、 17 @golden_lucky 技術曞出版ラムダノヌト

若手ハむキャリアのスカりト転職