Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

Haskellでポーカーを作ろう〜第二回 ポーカー・ハンドの判定をする 前編〜

はいはいどーも、皆さん進捗どうですか? 毎度おなじみのちゅーんさんですこんにちは。

この記事は、ちゅーんさんの連載エントリ「Haskellでポーカーを作ろう」の第二回目です。

第一回 リストのシャッフルとカードの定義

今回から2〜3回にわけて、ポーカー・ハンドを判定する処理を作っていきます。
若干ややこしい部分も含みますので、一つ一つ確実に理解しながら進めていきましょう。

尚、この記事では各ポーカーハンドの説明は行いません。
↓↓↓覚えてないよーって人は、Wikipediaを見ときましょう↓↓↓

http://ja.wikipedia.org/wiki/%E3%83%9D%E3%83%BC%E3%82%AB%E3%83%BC%E3%83%BB%E3%83%8F%E3%83%B3%E3%83%89%E3%81%AE%E4%B8%80%E8%A6%A7

はじめに

この連載エントリでは、なるべくとっかかりやすい部分から少しづつプログラムを組み立てていきますが、 単に写経して出来上がったものを動かすだけでは、個々のコードの意味を理解するのは難しいかもしれません。

関数型プログラミングのメリットは宣言的である、とよく言われますが、 基本的に関数を一つ、型を一つ定義した段階でコンパイルしたり動かしたりする事が出来るのです。

もし本エントリを読みながら実際に手を動かす場合、コンパイル出来そうな段階では実際にコンパイルしてみて、 動かせそうな単位でGHCiやrunghcを使った動作確認をしてみる事が、Haskellプログラミングを身につける要になるでしょう。

値チェックは型の力を借りるべし

Hands.hsというファイルを作って下さい。 ポーカー・ハンドを判定するための型や関数はこのモジュールに作っていく事にします。

ポーカー・ハンドを判定するにあたって、 カードの枚数は5枚であるべきとか、ソートされていた方が判定しやすいとか、本題に入る前に多少考えるべき事がありますね。

この「カードの枚数が間違いないか判定したり、カードをソートする」処理について、 どのタイミングで実施するかはさておき、何処かで実施する必要がある事は分かってるんですから、作っちゃいましょうか。

decision :: [Card] -> Maybe [Card]
decision l = 
  if length l == 5 
    then Just $ sort l
    else Nothing

Haskellには、部品同士を繋げるための糊が呆れるほど沢山ありますので、 とにかく、必要だと解っている部分は作ってしまうのがポイントです。

ところで、上のdecision関数について、もうちょっと考えてみましょう。 次のような型になっています。

decision :: [Card] -> Maybe [Card]

decision関数の引数の[Card]型と、返り値の[Card]型では少し意味合いが違う事に気が付きませんか?
この関数を通して得られた[Card]型の値は、カードが5枚である事や、ソートされている事が保証されています。 (カードが5枚以外の場合結果がNothingになるため、そもそもソートされた[Card]型の値を得る事ができないですよね。)

にも関わらず、[Card] -> Maybe [Card]という型からその情報が得られないのですが、これって何かもったいない気がしません? もし、この「もったいない」感覚が解るなら、あなたはもう立派なHaskellerですw

「もったいない」感覚を解決するために、次のようなHand型を定義しましょう。

newtype Hand = Hand { fromHand :: [Card] } deriving (Show, Eq, Ord)

データコンストラクタHand[Card]型の値を一つ内包する事が出来るだけですから、 扱える情報は本質的に[Card]と同じですね。(このような関係を同型といいます)

続いてdecision関数を次のようなtoHand関数に書き換えます。

toHand :: [Card] -> Maybe Hand
toHand l = 
  if length l == 5 
    then Just $ Hand (sort l)
    else Nothing

この関数の成すことは、本質的ににdecision関数とまったく違いはありません。 しかし、返り値の型がHandになっているだけで、ずいぶんと印象は違って見えますね。

そして、Hand型をエクスポートする際に、データコンストラクタはエクスポートせずに、 代わりにtoHand関数をエクスポートします。
Hand型の値を作成する際にtoHand関数を使う事を強制する事によって、 Hand型が内包するリストが必ず5枚で、ソートされている事が保証されるのです。

module Hands 
  ( Hand
  , toHand, fromHand
  ) where

ここで、toHand関数とfromHandフィールドの型を並べてみましょう。

toHand :: [Card] -> Maybe Hand
fromHand :: Hand -> [Card]

toHandfromHandの意図や使い方は、型定義を見ただけで明らかです。
この例を見るとなんとなく、Haskellerが「型はドキュメントだっ!」なんていう理由が伝わるのでは無いでしょうか。


Haskellの場合、よく知られたオブジェクト指向言語よりも型を作るのがずっと簡単なので、 Hand型のケースのような、ちょっとした条件の保証や、軽い意味付けを与えるためだけに、 新たな型を定義するという事をよくやります。 細かい単位で型の制約を与える事で、プログラマの間違いをコンパイル時に検出して、 しょうもないバグを未然に防ぐ事ができるというわけです。

ポーカー・ハンドの型定義

さて、ここから本格的にハンドを判定するプログラムを書いていきます。

端的に言えば、手札からポーカー・ハンドを返す、次のような型を持つ関数が必要なわけですね。
(引数が[Card]型ではなくHand型になっている事によって、不正な枚数のカードを渡す事ができなくなっていますね。)

pokerHand :: Hand -> PokerHand
pokerHand = ...

あっ、今、ナチュラルに未定義のPokerHandという型名を使いました。
ちょっとへんてこな手順に感じるかもしれませんが、まず欲しい関数の型を書くことで、 どのような型を定義する必要があるのか、整理する事ができるのです。

もし、具体的なPokerHand型をすぐに定義出来そうに無い場合、 以下のようにデータコンストラクタを持たない型を定義し、関数の方はundefinedとする事でひとまずコンパイルできます。

pokerHand :: Hand -> PokerHand
pokerHand = undefined

data PokerHand

詳細が決まっていない箇所をundefinedとしておいて、不要なコンパイルエラーを回避しながら、 型の整合性を整えたり、APIをデザインしたりする事は良くあります。

その際にひとつ注意が必要なのは、 undefinedを評価しようとすると、以下のような実行時エラーが発生するという事です。

*Hands> undefined
*** Exception: Prelude.undefined

そのため、プロダクトの完成時にundefinedが残っているような事のないようにしましょう。

ちなみに、undefinedは評価された時のエラー情報が少なすぎるため、 $notImplementedを使うのがナウい方法みたいです・・・

http://maoe.hatenadiary.jp/entry/20120214/1329211696

が、色々と前準備が必要だったりと、面倒な部分もあるようなので、 開発しているプログラムの規模等によって使い分けても良いかもしれませんね。

とかなんとかいいつつ、今回のPokerHand型の要件は明確なので、とっとと定義してしまいましょう。

data PokerHand 
  = HighCards --ハイ・カード(いわゆるブタ)
  | OnePair --ワンペア
  | TwoPair --ツーペア
  | ThreeOfAKind --スリーカード
  | Straight --ストレート
  | Flush --フラッシュ
  | FullHouse --フルハウス
  | FourOfAKind --フォーカード
  | StraightFlush --ストレート・フラッシュ
  deriving (Show, Read, Eq, Ord, Enum)

例によって、弱いハンドからデータコンストラクタを記述しOrd型クラスのインスタンスにする事によって、 ハンドの強弱の比較が行いやすいようにしておきました。

設計はトップダウン、実装はボトムアップ

はじめに、pokerHand関数の型が、本当にこれで良いかという点について触れておきましょう。

pokerHand :: Hand -> PokerHand
pokerHand = undefined

単純に、ポーカー・ハンドを判定したいだけならこれで良さそうですが、 ポーカーでは対戦相手と同じハンドだった場合、より強いカードでハンド作ったほうが勝ちになります。

例えば、以下の2つのハンドはどちらもツーペアですが、 ①はキング、②はジャックがそれぞれハンドを構成する最強のカードなので、①の勝ちになります。

①[S3_,C8_,S8_,HK_,DK_] -- DK_ 勝ち"
②[H2_,S2_,HJ_,DJ_,CK_] -- DJ_ 負け"

そのため、ハンドの種類と一緒に、そのハンドを構成する最強のカードを一緒に返却出来ると良さそうです。

pokerHand :: Hand -> (PokerHand, Card)
pokerHand = undefined

さて、手続き的なプログラミング言語に慣れていると、 いきなりこのpokerHand関数の中身を書きに行きたくなってしまいますが、 あまり大きな所からいきなり手を付けはじめると、最終的にカオスなプログラムになったり、 そもそもコンパイル出来なかったりという状態に陥る可能性があるのです。
(手続きプログラムだとそうならない、というわけでは無いですが。 その、誤魔化しが効きやすいというか・・・)

というわけで、問題をもう少し小さな単位に切り分けましょう。
どのように組み合わせるかはさておき、 各ポーカー・ハンドを判定する処理が必要なのは、間違いなさそうです。

例えば、ツーペーアだったら、次のような感じです。

-- 引数がツーペーアだったら `Just (TwoPair, 最強カード)`という値を、
-- そうで無い場合は `Nothing` を返す。
twoPair :: Hand -> Maybe (PokerHand, Card)
twoPair = undefined 

こんな感じで、ひと通りのポーカー・ハンド分の関数を定義します。

straightFlush :: Hand -> Maybe (PokerHand, Card)
straightFlush = undefined

fourOfAKind :: Hand -> Maybe (PokerHand, Card)
fourOfAKind = undefined

fullHouse :: Hand -> Maybe (PokerHand, Card)
fullHouse = undefined

flush :: Hand -> Maybe (PokerHand, Card)
flush = undefined

straight :: Hand -> Maybe (PokerHand, Card)
straight = undefined

threeOfAKind :: Hand -> Maybe (PokerHand, Card)
threeOfAKind = undefined

twoPair :: Hand -> Maybe (PokerHand, Card)
twoPair = undefined
  
onePair :: Hand -> Maybe (PokerHand, Card)
onePair = undefined

いずれも同じような目的をもった関数ですね。 このような場合、可能であれば全て同じ型になるように定義することで、 あとで一纏めに扱える可能性があります。

実際、最終的にこれらの関数を上手く繋げてpokerHand関数を作成するわけですが、 その話はまた後の回で行う予定です。


さて、だんだん全体図が見えてきました。
徐ろに「ポーカー・ハンドを判定せよ」と言われてもどうすれば良いか困ってしまいますが、 役の一つ一つの判定だったら、既に知っているリスト処理を頑張ればどうにかなりそうです。 (ストレート・フラッシュやフルハウス等、 判定が複雑になりそうな関数もありますから、もう少し掘り下げが必要ですが)

このように欲しい関数の型を並べていくことによって、 「上から下に」型を設計していけば、徐々に目的の関数を実装するための道標が見えてきます。 そして、具体的な実装がイメージしやすい粒度まで掘り下げる事ができたら、 今度は「下から上へ」と中身を作りこんでいけば良いのです。

「設計はトップダウン」「実装はボトムアップ
この方法が常に最善手というわけではありませんが、Haskellプログラミングにおける、良いヒントになるでしょう。

まとめ

今回は、ポーカー・ハンドを判定するプログラムの前編という事で、関数の型を定義して行きました。

実装の前に型定義を行う事によって、全体のイメージを掴む事ができるというのも、本エントリの重要な所ではあります。
しかし、それよりも、本エントリではHand型のtoHand関数を定義して以降、型を定義しただけで実装については何も触れていないという事に注目してください。にも関わらず、皆さんは今後の開発の進め方や実装方法等について色々とイメージする事ができたはずです。 このことから、「Haskellの型は実に多くの情報を持っている」という感覚が、少しでも伝われば幸いです。

ところで、本エントリで紹介しているプログラムは、一旦動く所まで書き上げたものを、 冗長な部分を修正したり、より読みやすくリファクタリングする事で、現在の形に落とし込んだものです。
大雑把な手順に違いはありませんが、型設計はあくまで「下書き」の段階であり、 熟練したHaskellプログラマでも、いきなり完璧な設計が出来るわけではありません。 もしご自身のプロジェクトで上手く設計出来なくても気を落とさずに、少しづつ感覚を掴んで行けば良いと思います。

と、なんとなく色々とポエムってしまいましたが、 ポーカー開発エントリの第二回は、お楽しみいただけたでしょうか。

次回から、今回undefinedとした部分を具体的に実装していくフェーズに入ります。 その際にMaybeモナドの話をしますので、焦らずにゆっくり進んでいきましょう。

それではノシノシ

←前 次→