Creatable a => a -> IO b

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

Haskellでポーカーを作ろう〜第七回 プロジェクトを整理するよっ〜

外はすっかり真夏日よりですが、今日も冷房の前ででプログラミング充しています。 嘘です、ずっとスプラトゥーンやってました。
結果として相変わらずGithubのContributionsは真っ白です。 どもども、ちゅーんさんです。

このエントリは、ちゅーんさんによるポーカー開発連載記事の第七回目です。
過去のエントリはこちら

第一回 リストのシャッフルとカードの定義
第二回 ポーカー・ハンドの判定をする 前編
第三回 ポーカー・ハンドの判定をする 中編
第四回 ポーカー・ハンドの判定をする 後編
第五回 カードの入れ替え処理を作る
第六回 CPU対戦機能を付けよう

前回までは、やや場当たり的に作って行きましたが、 そろそろ程度整理しないと全体像が掴みにくい規模になってきたようです。

そこで今回は、Haskellのビルドツールについて簡単に紹介/説明し、 ここまで書いたプログラムをプロジェクトとして管理できるように移行します。

その後、前回までに作成した関数を洗い出して、モジュール構成を整理しましょう。

前準備とか

まず、前回までの内容を手順通りに勧めていれば、以下の3ファイルが出来ているはずです。

  • Main.hs
  • Cards.hs
  • Hands.hs

Main.hsPoker.hsに書き換えて、main関数の名前をsimpleGame関数に変更してください。 そして、Poker.hsのモジュール宣言部を次のように書き換えます。

module Game.Poker
    ( module Game.Poker.Hands
    , module Game.Poker.Cards
    , simpleGame
    ) where

...

その上で、新しくMain.hsを作りなおしましょう。

module Main where
import Game.Poker

main :: IO ()
main = simpleGame

これらのファイルを以下のように配置します。

.
├── app
│   └── Main.hs
└── src
    └── Game
        ├── Poker
        │   ├── Cards.hs
        │   └── Hands.hs
        └── Poker.hs

srcディレクトリ以下に基本的なプログラムは書くようにし、 appディレクトリは、アプリケーション本体のソースコードを配置する事にしましょう。

こうする事によって、例えば「GUI版を作りたい」といった時に、このプロジェクトをインストールし、 Game.Pokerモジュールを読み込む事によって、ポーカーゲームを実装するための全ての関数を再利用する事ができます。
開発者はUIだけ作れば良くなるというわけですね。

ディレクトリの構成に合わせて、各モジュールのモジュール名を変えます。

  • Cards.hs -> Game.Poker.Cards
  • Hands.hs -> Game.Poker.Hands

併せて、各ファイルのimport文も書き換えましょう。

ビルドのための設定を行う

これまで紹介したcabalの使い方といえば、cabalコマンドを使ってパッケージをインストールするだけでしたが、 cabalにはプロジェクトをビルドして、パッケージや実行ファイルをインストールしたりするビルドツールとしての機能もあります。

cabal自体はかなり優秀なツールなのですが、依存関係が壊れやすいという少々ややこしい問題を抱えており、 Haskellで巨大なライブラリを使ったり、色々なライブラリを一緒に使おうとした時に問題が発生する事が多く、 長らく問題になっていまして、それがほんの数カ月ほど前にリリースされたstackというビルドツールによって解決される事になりそうな状況なのです。

基本的にstackの方を推奨したいのですが、 stackはまだ開発中のツールですし、cabalの上で動作するものでもあるので、今回は両方の使い方を説明しようと思います。

いちおう、どちらかが使えれば良いという方に向けて、それぞれ独立して読んでも使えるように説明します。

  • ちゃんと理解したい人 -> 「cabalの使い方」「stackの使い方」両方読む
  • 使えれば良い人 -> stackの使い方を読む
  • 枯れていない技術は信用できない人 -> cabalの使い方を読む

cabalの使い方

というわけで、ビルドツールとしてcabalを使う方法を簡単にご説明しましょう。

cabalを使ったパッケージ管理を行うためには、*.cabalという拡張子の設定ファイルを作成する必要があります。
cabal initというコマンドを実行する事によって、対話形式によって入力された内容や現在のディレクトリの状態等といった 情報を元に、ある程度の項目が埋められた状態のcabalファイルを簡単に生成する事ができます。

$ cabal init
Package name? [default: draw-poker] 
Package version? [default: 0.1.0.0] 
Please choose a license:
 * 1) (none)
   2) GPL-2
   3) GPL-3
   4) LGPL-2.1
   5) LGPL-3
   6) AGPL-3
   7) BSD2
   8) BSD3   9) MIT
  10) ISC
  11) MPL-2.0
  12) Apache-2.0
  13) PublicDomain
  14) AllRightsReserved
  15) Other (specify)
Your choice? [default: (none)] 8

...

残念ながら、このコマンドによってすぐにビルド出来る状態にはなりません。 いくつか追加設定が必要な項目があるのですが、今回は以下のようにcabalファイルを設定します。

-- Initial draw-poker.cabal generated by cabal init.  For further 
-- documentation, see http://haskell.org/cabal/users-guide/

name:                draw-poker
version:             0.1.0.0
synopsis:            playing draw poker
description:         for blog entry
homepage:            http://tune.hateblo.jp/entry/2015/05/12/023112
license:             BSD3
license-file:        LICENSE
author:              Tokiwo Ousaka
maintainer:          its.out.of.tune@gmail.com
-- copyright:           
category:            Game
build-type:          Simple
-- extra-source-files:  
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     Game.Poker
                     , Game.Poker.Hands
                     , Game.Poker.Cards
  build-depends:       base >= 4.6 && < 4.7
                     , random-shuffle >=0.0&& <0.1
                     , safe >=0.3 && <0.4
  default-language:    Haskell2010

executable draw-poker
  main-is:             Main.hs
  -- other-modules:       
  -- other-extensions:    
  build-depends:       base >=4.6 && <4.7, draw-poker
  hs-source-dirs:      app
  default-language:    Haskell2010

source-repository head
  type:     git
  location: https://github.com/tokiwoousaka/draw-poker

変更箇所については以下のdiffをご覧ください。

https://gist.github.com/tokiwoousaka/37c93cd7a067dbac1483/revisions

各項目について詳しい説明は行いません、 得に設定する事の多い、以下の二点のみ、認識しておいてください。

  • モジュールが増えた場合はexposed-modulesに項目の追加が必要
  • 依存するパッケージが増えた場合、build-dependsに項目の追加が必要

それ以外で今後修正が必要な場合は都度説明します。

ポイントとしては、

  • descriptionの項目を設定した
  • libraryおよびexecutable両方の項目
  • executableのソースファイルがapp/Main.hsを指定しているようになっている必要がある
  • 外部ライブラリへの依存しているのはexecutableではなくlibrary
  • executabledraw-pokerパッケージそのものに依存している旨記載が必要
  • source-repositoryの設定は任意(しなくても良い)

といったところでしょうか。
その他、以下の基本的な機能についてはざっくりと覚えておきましょう。

  • 動作確認
    • cabal replコマンド:libraryをGHCiで読み込みます
    • cabal run app/Main.hsコマンド:app/Main.hsを実行します
  • インストール
    • cabal installコマンド:ライブラリおよび実行ファイルのインストール
      • 初回はその前にcabal configureを実行する必要があります
      • cabalファイルのlibraryで設定されている情報を元にライブラリをインストール
      • cabalファイルのexecutableで設定されている
  • ドキュメント作成
    • cabal haddockコマンド:dis/dock/html配下にドキュメントを作成します
      • ビルド通ったら一度は確認しときましょう。とても便利です。

なお、全体的な説明については、以下の記事を参考にすると良いでしょう。

http://itpro.nikkeibp.co.jp/article/COLUMN/20121106/435201/?ST=develop&P=2

stackの使い方

この節では、stackの使い方を説明します。

stackのインストールは、以下のREADMEの、How to Install を参照してください。 (今の所、全環境へのインストール方法の日本語訳はされていないのです・・・。)

https://github.com/commercialhaskell/stack

stack newコマンドを利用する事により、新規プロジェクトを作成する事ができます。

既存のソースがある場所では実行する事ができませんので、 空のフォルダでstack newコマンドを利用し、プロジェクトを作成したら、 予め作っておいたソースコードをコピーして来てください。

生成されたファイルと、コピーしたファイルを含めて、以下のようなディレクトリ構成になっていればOKです。

.
├── app
│   └── Main.hs
├── LICENSE
├── new-template.cabal
├── Setup.hs
├── src
│   └── Game
│       ├── Poker
│       │   ├── Cards.hs
│       │   └── Hands.hs
│       └── Poker.hs
├── stack.yaml
└── test
    └── Spec.hs

そして、cabalファイルをdraw-poker.cabalにリネームし、以下を参考にして、内容を書き換えます。

name:                draw-poker
version:             0.1.0.0
synopsis:            playing draw poker
description:         for blog entry
homepage:            http://tune.hateblo.jp/entry/2015/05/12/023112
license:             BSD3  
license-file:        LICENSE
author:              Tokiwo Ousaka
maintainer:          its.out.of.tune.this.my.music@gmail.com
category:            Game
build-type:          Simple
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     Game.Poker
                     , Game.Poker.Hands
                     , Game.Poker.Cards
  build-depends:       base >= 4.7 && < 5
                     , random-shuffle
                     , safe
  default-language:    Haskell2010

executable draw-poker
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , draw-poker
  default-language:    Haskell2010

test-suite draw-poker-test
  type:                exitcode-stdio-1.0
  hs-source-dirs:      test
  main-is:             Spec.hs
  build-depends:       base
                     , draw-poker
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  default-language:    Haskell2010

source-repository head
  type:     git
  location: https://github.com/tokiwoousaka/draw-poker

変更すべき箇所については以下のdiffを参照していただくと良いでしょう。

https://gist.github.com/tokiwoousaka/802654fd6e4ab3ed2f0c/revisions
http://tune.hateblo.jp/entry/2015/07/13/034148

各項目について詳しい説明は行いません、 得に設定する事の多い、以下の二点のみ、認識しておいてください。

  • モジュールが増えた場合はexposed-modulesに項目の追加が必要
  • 依存するパッケージが増えた場合、build-dependsに項目の追加が必要

それ以外で今後修正が必要な場合は都度説明します。

ポイントとしては、

  • パッケージ名をnew-templateから変更
  • synopsisdescription等のメタ情報を設定した
  • source-repositoryの設定は任意(しなくても良い)

といった所でしょうか。 その他、以下の基本的な機能については覚えておきましょう。

  • 動作確認
    • stack replコマンド:libraryをGHCiで読み込みます
    • stack runghc app/Main.hsコマンド:app/Main.hsを実行します
  • インストール
    • stack installコマンド:ライブラリおよび実行ファイルのインストール
      • cabalファイルのlibraryで設定されている情報を元にライブラリをインストール
      • cabalファイルのexecutableで設定されている
  • ドキュメント作成
    • stack haddockコマンド:
      • ビルド通ったら一度は確認しときましょう。とても便利です。

なお、残念ながら現状、日本語の資料は整いきっていない状況です。 以下の記事を参考にすると良いでしょう。

http://qiita.com/tanakh/items/6866d0f570d0547df026

Hackageへのアップロード

あまり詳細には説明しませんが、開発したアプリケーションやライブラリをHackageにアップロードするための機能として、 cabalにはcabal upload、stackにはstack uploadというコマンドが存在します。

本連載で開発中のポーカーも、Hackageにアップロード済です。

http://hackage.haskell.org/package/draw-poker

プロジェクトの整理

さて、これでひと通りビルドツールの説明を終えました、動く事が確認出来たはずです。
いよいよ開発っぽくなってきましたね。

ところで、前々回/前回で作成した関数はMain.hs(Poker.hsに変更したのでした)に纏めて作ったのでした。 このままでは今後整理し切れなくなる可能性があるため、これらをちゃんと整理し、機能毎のモジュール化を行いましょう。

モジュール化

まず、現状のMain.hsモジュールに宣言されている型や関数を列挙してみましょう。 前々回では「ハンドの入れ替え処理」を作りました。そして前回作成したのAIの思考ルーチンもありますね。

  • ハンドの入れ替え
      • type DiscardList = [Card] -- 捨て札
    • 関数
      • getHand :: Deck -> Maybe (Hand, Deck)
      • drawHand :: Deck -> DiscardList -> Hand -> Maybe (Hand, Deck)
      • getDiscardList :: Hand -> IO (Maybe DiscardList)
      • toIntList :: String -> Maybe [Int]
      • selectByIndexes :: [a] -> [Int] -> Maybe [a]
  • AIの思考ルーチン(カードの入れ替え)
    • 関数
      • aiSelectDiscards :: Hand -> DiscardList
      • nOfKindDiscards :: Hand -> DiscardList
  • 勝敗判定
    • 関数
      • judgeVictory :: (PokerHand, Card) -> (PokerHand, Card) -> Ordering
  • プロトタイプ
      • data Player = Player | Enemy deriving Eq
    • 関数
      • simpleGame :: IO ()
      • showPlayerName :: Player -> String
      • matchPoker :: (Hand, Deck) -> IO ()
      • playPoker :: Hand -> Deck -> Player -> IO ((PokerHand, Card), Deck, Hand)
      • inputDisuse :: Hand -> IO DiscardList
      • aiDisuse :: Hand -> IO DiscardList
      • printResult :: Hand -> Hand -> (PokerHand, Card) -> (PokerHand, Card) -> IO ()
      • printHand :: DiscardList -> Hand -> Player -> IO ()
      • printHand dis hand player =
      • ynQuestion :: String -> IO a -> IO a -> IO a
      • showChangeHand :: DiscardList -> Hand -> String

src/Game/Pokerディレクトリ以下に新たにAI.hsおよびHands.hsを用意します。 で、この4つに分類した関数を、以下のとおりモジュール分けしましょう。

  • ハンドの入れ替え:Hands.hs
  • AIの思考ルーチン:AI.hs
  • 勝敗判定:Hands.hs
  • プロトタイプ:Simple.hs

結果として、ソースコードは以下のような配置になっているはずです。

.
├── app
│   └── Main.hs
└── src
    └── Game
        ├── Poker
        │   ├── AI.hs <- 追加  
        │   ├── Cards.hs
        │   ├── Hands.hs
        │   └── Simple.hs <- 追加  
        └── Poker.hs

移動させた各関数をエクスポートした上で、Poker.hsを次のように書き換えます。

module Game.Poker
    ( module Game.Poker.Hands
    , module Game.Poker.Cards
    , module Game.Poker.AI
    ) where

import Game.Poker.Hands
import Game.Poker.Cards
import Game.Poker.AI

このように、module モジュール名のようにエクスポート文を記述する事によって、 そのモジュールがエクスポートしている全ての関数をエクスポートする事ができ、 Poker.hsはポーカー開発に便利なあらゆるモジュールを全て一気にエクスポートする事ができるようになります。 また、ポーカーゲーム本体になるIO処理は、このモジュールに作っていく事にしましょう。

プロトタイプの扱い

さらに、Game.Pokerモジュールではエクスポートしなかった、Simple.hsについて考えます。 ここでエクスポートしているsimpleGame関数は、前回までで作成したプロトタイプですね。

このプロトタイプも遊べるものにはなっているので、捨ててしまうのももったいないですし、 得にリソースを消費するほどのものでも無いですから、一緒に配布する事を考えましょう。

appフォルダ以下にSimple.hsを追加し、cabalファイルに以下のような記述を追加します。

executable draw-poker-simple
  hs-source-dirs:      app
  main-is:             Simple.hs
  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
  build-depends:       base
                     , draw-poker
  default-language:    Haskell2010

そして、追加したSimple.hsで、Poker.Simpleモジュールを読み込み、main関数でsimpleGame関数を呼び出すようにします。 同時に、先ほどまでsimpleGameを呼んでいたMain.hsmain関数については、ひとまずHello Worldでも書いておけば良いと思います。

これらをひと通り設定したら、ビルドが実行してみて、 上手く行ったらcabal installまたはstack installを実行し、 2つのアプリケーションがインストールされて実行出来るか確認すると良いでしょう。

色々と設定する事が多くて大変かもしれません、stack向けに構成したプロジェクトをGithubに上げてあるので、 もし上手く行かない場合はこちらも参考にしてみてください。

https://github.com/tokiwoousaka/draw-poker/tree/aa33fc7fdbf3e1292cbc33a580bd21758ae4a6b6

まとめ

というわけで、今回はビルドツールの使い方を説明し、モジュールの整理を行いました。

一定以上大きなプログラムを開発する上ではプログラミング言語そのものだけではなく、 さまざまな開発ツールの使い方を習得する必要がある事は、言うまでもないでしょう。

これらツールの使い方は本記事では解説し切れない事も多いので、 cabalやstack等ツールの使い方については、各々でも色々と調べてみてください。

さて、せっかくプロジェクトを整えたのですから、 この構成を利用して、プログラムの検証を行えるようになると良いですね。
というわけで、次回はHUnitやQuickCheckといったテスト用のライブラリについて説明し、 テストコードの書き方について説明して行きます。

それではまた、ノシノシ

←前