|ハイブリッドOS|File System|ARM|Android|Java|制御システム|オープンシステム

 

技術者コラム

 
フォーム
 
参考画像1〜2
2014-12-27
 
第37回目:Haskellでテトリス(Part5)
2014-12-20
筆者:村田

こんにちは。

さてさて、前回はボード画面を表示しました。これからテトリミノの表示に向けて頑張りましょう。テトリミノには複数の種類がありますが、それぞれに中心位置や形状という属性を持ちそうだなと想像ができます。これらはテトリミノの共通項です。したがって「型」を通じて同じものと見なした方が設計し易いはずです。他の言語でも構造体やクラスとして共通項をくくり出しますよね。では早速テトリミノを表すデータ型を作りましょう。Part3では単純なデータ型の作り方を紹介しました。

data 型名 = 値名 | 値名 ...

ついでに具体例も。

data Bool = True | False

今回はもう少し複雑な型を定義します。

data 型名 = 値コンストラクタ パラメータの型 パラメータの型 ・・・

こちらも具体例を見てみましょう。

data Book = Book String [String] Int

型名と値コンストラクタは両方ともBookとしましたが異なっていても構いません。値コンストラクタであるBookは3つの引数をとります。1つ目はString型で本のタイトル、2つ目はString型のリストで著者リスト、3つ目は管理番号です。ところで単純な値として紹介したTrueやFalseですが、実はBookと同じく値コンストラクタです。TrueやFalseはパラメータを一つも取らずにBool型の値を生成する値コンストラクタというわけです。では、Book型の値を実際に作ってみましょう。3つのパラメータを値コンストラクタに渡します。

Book "The Lord of the Rings" ["Tolkien"] 8

値コンストラクタは引数をとって値を返します。まるで関数みたいですね。型を表示させてみましょう。

> :t Book
Book :: String -> [String] -> Int -> Book

ふむ、やはり関数っぽい。実際に関数を引数にとるような場面で値コンストラクタを渡すことができます。しかし値コンストラクタは関数と同じではありません。そもそも普通の関数とは異なり大文字から始まっていますしね。また値コンストラクタは関数とは異なりパターンマッチができます。このパターンマッチを利用してBookデータからタイトルを抜き出してみましょう。

getTitle :: Book -> String
getTitle (Book title _ _) = title

(Book title _ _)の部分がパターンマッチです。値として渡されたBookデータをバラバラにして、著者リストと管理番号を捨て、タイトルだけ変数に束縛してます。試しにBookの値を作り、getTitleでタイトルを取り出してみましょう。

> let loR = Book "The Lord of the Rings" ["Tolkien"] 8
> getTitle loR
"The Lord of the Rings"

さて、これで新しいパラメータ付きのデータ型を作ったり、各パラメータをパターンマッチを使って取り出すことができるようになりました。実は同じことをもっと短く書ける「レコード構文」というテクニックがあるので紹介します。

data Book = Book { getTitle :: String, getAuthor :: [String], getNo :: Int }

ダブルコロンの前に関数名を書くとパラメータを取り出せる関数が自動生成されます。Wコロンといえば先日笑点で漫才をやっていました。コンビ仲ネタの緊張感もあって目が離せないというかなんというか・・・。それはともかくghciでもう一度テストしてみましょう。

> let loR = Book "The Lord of the Rings" ["Tolkien"] 8
> getTitle loR
"The Lord of the Rings"
> getAuthor loR
["Tolkien"]

レコード構文を使うと、データ型の中身へのアクセサが簡単に手に入るので便利ですね。それではレコード構文をつかってテトリミノのデータ型を定義してみましょう。今のところパラメータとして必要になりそうなのは、ブロック種別と位置と形状くらいでしょうか。それと位置についてはボードの2次元座標を使いますのでエイリアスもついでに用意しておきましょう。

type Pos = (Int, Int)

data Mino = Mino { getBlock :: Block
                 , getPos   :: Pos
                 , getShape :: [Pos]
                 } deriving Show


形状[Pos]は位置Posを基準にした相対位置をリストにしたものです。ではMino型の値として手始めにI-ミノを作ってみましょう。

i_Mino = Mino I (5,2) [(0,0),(0,-1),(0,-2),(0,1)] -- I-ミノ

(5,2)という位置はテトリスで新しいミノが落ちてくる初期位置です。x座標は中央の5です。y座標は2となっていますが、これはy座標0と1が画面に表示しないダミー領域だからです(Part3参照)。形状を表す位置リストからI-ミノがイメージできますか?中心位置から1つ上と2つ上(これらの部分は画面外)、そして1つ下にブロックを並べるとI-ミノになります。

なお、レコード構文で作ったデータ型の場合、もっと明示的にパラメータを渡して値を生成することができます。例えばI-ミノの別の作り方は以下のようになります。

i_Mino = Mino { getBlock = I, getPos = (5,2),
                getShape = [(0,0),(0,-1),(0,-2),(0,1)] }

各パラメータの意味が明確になり、またパラメータを任意の順番で記述することも可能になります。

次にテトリミノをボードに置く関数を考えましょう。テトリミノとボードを渡すとテトリミノが置かれた新しいボードを返したいと思います。既存のボードデータの一部を書き換えるという副作用を起こすのではなく、"新しいボードデータを返す"ところが関数型プログラミングのポイントですね。というわけで型はきっとこんな感じです。

putMino :: Mino -> Board -> Board

んでputMinoを実装しようとすると、うーん難しい。こーかな?どーかな??あーでもない、こーでもないと考えた末、まずはこんなことがやりたくなりました。ボードの位置を指定して1マス分のブロック(IとかZとか)で置き換えたいなと。さらに、2次元ボードで考える前に1次元リストの任意の位置を置き換える方法を考えなくちゃ。どんな型の関数が欲しいのかと言うとこんな感じ。

replace :: [a] -> Int -> a -> [a]

aは覚えていますか?型変数ですね。任意の型を表しています。リストの位置を指定して同じ型の新しい値で置き換えたリストを返す関数です。結構汎用的なので既に存在するかもしれませんね。こんな時はHoogle(http://www.haskell.org/hoogle/)というWebサイトが役に立ちます。Haskellの関数を型で検索することができます。まあ実際に検索してみたところ、所望の関数は見つかりませんでしたので自分で作ることにしましょう。

replace :: [a] -> Int -> a -> [a]
replace xs n v = ys ++ [v] ++ zs
  where ys = take n xs
        zs = tail $ drop n xs

えいっと。takeやtailやdropは詳しく紹介しません。型を見たり実際に使ってみてください。whereは今まで登場していなかったかもしれません。replaceという関数定義の中だけで使える一時変数ys,zsを定義するための構文です。whereを使わずに以下のようにも書けます。

replace xs n v = (take n xs) ++ [v] ++ (tail $ drop n xs)

好みの問題もありますが、ワンラインで長くなり過ぎる場合は、適度に一時束縛に置き換えるべきでしょう。ではghciでテストしてみます。

> replace [1,2,3,4,5] 3 100
[1,2,3,100,5]

> replace "abcde" 4 ‘z’
"abcdz”

OKOK。次はreplaceを行と列に2回使って、指定位置を新しいブロックで置き換えてみたいと思います。

putBlock :: Block -> Pos -> Board -> Board
putBlock b (x,y) board = board'
  where xs = replace (board!!y) x b
        board' = replace board y xs

ちょっと複雑かもしれません。まずはボードのy行目(board!!y)を取り出し、そのx列目を新しいブロックで置き換え、できた新しい1行分のデータをxsに束縛します。次にボードのy行目をxsに丸々置き換えて新しいボードを作って返します。テストしてみましょう。

> let board = initBoard
> putBlock I (5,0) board
[[G,E,E,E,E,I,E,E,E,E,E,G],[G,E,E,E,E,E,E,E,E,E,E,G],・・・(省略)

無事0行目の5列目をIに置き換えることができました。今度はputBlockを繰り返し呼び出して複数のブロックを置き換えましょう。これは「畳み込み」という頻出パターンです。複数の位置を順番に処理して新しいボードを作り上げます。言い換えると、リストをなめて一つの値にまとめる処理です。このようなパターンに遭遇したら自分で再帰ループを書くよりfoldrという関数を使いましょう。

> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b

ちょっと関数の型が難しいですね。最初の引数(a -> b -> b)は2引数関数です。例えば(+)関数がそうです。2つ目の引数bは基準となる値、3つ目の引数[a]は処理対象のリストです。戻り値はまとめ上げた値です。最初の引数に戻って(a -> b -> b)という関数を見ます。この関数の2つ目の引数bにはまとめ上げ途中の合計値が渡されます。ちょっと使ってみましょう。

> foldr (+) 0 [1,2,3]
6
> foldr (++) [] ["abc", "def", "ghi"]
"abcdefghi"
> foldr (\x y -> x + y * 2) 1 [1,2,3]
25

最初の例は足し算を繰り返し、一つの合計値にまとめています。次の例は文字列結合を繰り返し、一つの文字列にまとめています。最後の例は少し複雑な計算を繰り返し、一つの結果にまとめます。みんな同じですね。なお最後の例で2倍されるyには途中経過の値が入ります。map、filter、foldr(他言語ではreduceということも)この3種の神器はリスト処理(繰り返し処理)でよく使われますのでお見知り置きを。

さて、話を戻してfoldrを使ってputBlockの繰り返し処理を作りましょう。foldrの最初の引数は2引数関数でした。putBlockはBlock,Pos,Boardを取る3引数関数ですよね。ここで関数型プログラミングの便利なテクニックの一つ、「カリー化」を用います。カリー化とは、N引数関数の一つ目の引数にだけ値を与え、そこだけ固定化されたN-1引数関数を作ることことです。replicateという複製を行う関数を例にカリー化を試してみます。まずはreplicateの紹介です。

> :t replicate

replicate :: Int -> a -> [a]

> replicate 3 'A'
"AAA"

では第1引数を3に固定したカリー化関数を作って使ってみましょう。(ghci上で関数定義する。let 関数名 = 〜)

> let replicateThree = replicate 3

> :t replicateThree

replicateThree :: a -> [a]

> replicateThree 'B'
"BBB"
> replicateThree "HOGE"
["HOGE","HOGE","HOGE"]

本題に戻り、putBlockという3引数関数から2引数関数を作ります。試しにfという関数にして型を見てみましょう。

> :t putBlock
putBlock :: Block -> Pos -> Board -> Board
> let f = putBlock Z
> :t f
f :: Pos -> Board -> Board

putBlockにZというブロックを与えることで2引数関数が作れていますね。では材料も揃ったので一気にputBlocks(複数形)を書いてみます。

putBlocks :: Block -> [Pos] -> Board -> Board
putBlocks b ps board = foldr (putBlock b) board ps

foldrの第1引数はブロックを固定したカリー化関数ですね。第2引数は基準となるボードです。最後が置き換えたい位置のリストです。ここで一つ思考過程にポイントがあります。

1. putBlockを繰り返し呼び出して一つの値にまとめたい。 ==> foldrを使いたい
2. foldrを使うためには2引数関数が欲しい ==> 手元に2引数関数がなければカリー化すればいいじゃない
3. カリー化するためには固定化したい引数を見定め、第1引数に持ってきておく
4. putBlock関数の第1引数には[Pos]やBoardではなく、Blockを持ってこよう!

上記のようにfoldrを使うと決めたら、putBlockの引数の順番を見直しましょう。では作成したputBlocks関数をテストしましょう。

> let board = initBoard
> putBlocks I [(3,0),(4,0),(5,0),(6,0)] board
[[G,E,E,I,I,I,I,E,E,E,E,G],[G,E,E,E,E,E,E,E,E,E,E,G],・・・(省略)

ふ〜無事0行目の3〜6列目をIに置き換えることができました。結局putMino関数を作り上げるところまでは辿りつきませんでしたが、レコード構文やfoldr、カリー化に触れられましたし、のんびりいきましょう:-) それと今回から途中経過のソースコードもGitHubに上げていくことにしました。本ページの一番下のリンク集から参照できます。

以上
 
第36回目:Haskellでテトリス(Part4)
2014-12-06
筆者:村田
 
こんにちは。
 
もう12月です、早いですね。忘年会シーズンですが皆さん飲み過ぎにはご注意を。私は本日グロッキー状態でした・・・さて、本編の前に一つ紹介です。連載終了した分についてのソースコードをGitHubに上げました。本ページの一番下にリンク集を設けたのでこちらから参照して下さい。
 
それではPart1〜2で紹介した単純なウインドウ表示プログラムをベースに進めていきましょう。ところでプログラムで画面に図形を描くためには何をしたらいいでしょう。大抵の場合、「描画イベントハンドラの中でグラフィックスコンテキストを使用して図形を描く」という処理になると思います。以前書いたSwiftのコードも同じで、drawRectという描画イベントハンドラの中で以下のようなコードを書きました。
 
override func drawRect(rect: CGRect) {
    // グラフィックスコンテキストを取得
    let context = UIGraphicsGetCurrentContext()
    // 塗りつぶしの色に黒を設定
    CGContextSetFillColorWithColor(context, UIColor.blackColor().CGColor)
    // x:50, y:50 幅100,高さ100の正方形に内接する円を描く
    CGContextFillEllipseInRect(context, CGRectMake(50, 50, 100, 100))
}
 
Haskellでもやることは同じです。まずは描画イベントハンドラを探しましょう。HackageというHaskellのAPIマニュアルからGtkパッケージ(http://hackage.haskell.org/package/gtk)に潜って黙々と探索します(結構大変です)。[Graphics.UI.Gtk.Abstract.Widget]というパッケージに各種イベントハンドラがありました。目的の描画イベントハンドラは以下のexposeEventです。
 
exposeEvent :: WidgetClass self => Signal self (EventM EExpose Bool)
 
この型情報から使い方をイメージするのは難しいかもしれません。Signal型をじっくり調べると内部構造を知ることができます。興味のある方は是非:tや:iコマンドでSignal型の構造を追ってみてください。今回はそこまでは踏み込まず、とにかく使ってみようと思います。Part1のGUIサンプルコードではdeleteEventというイベントハンドラを使っていましたね。そのまま真似してみましょう。do構文を使ってきっとこんな形になるはずです。
 
window `on` exposeEvent $ do
  イベント処理
 
ここでのdoはEventMというモナドです。なおモナドやdo構文についてはPart2で解説しているので参考にしてください。次はグラフィックスコンテキストの取得、使い方を探すとしましょう。再度もHackageに潜って調べます。ふむふむ[Graphics.UI.Gtk.Gdk.GC]パッケージのgcNew関数で取得できるようです。
 
gcNew :: DrawableClass d => d -> IO GC
 
引数のdはDrawableClass型クラスに属していなければならないようです。ではどんな型がDrawableClass型クラスのインスタンスになっているか調べてみましょう。
 
> :i DrawableClass
class GObjectClass o => DrawableClass o
 
instance DrawableClass Pixmap
instance DrawableClass Drawable
instance DrawableClass DrawWindow
 
Pixmap型とDrawble型とDrawWindow型がインスタンスになっているようです。3つの型を全て調べるのは大変なので勘を働かせます。gcNew関数の引数dにはきっとDrawWindow型を渡すのでしょう。ではDrawWindow型の値はどうやって手に入れるのでしょう。ここも論理的な道筋ではなくて申し訳ないのですが勘を働かせます。Swiftの例でもそうでしたが、大抵の場合、グラフィックスコンテキストは描画イベントハンドラの中で取得します。おそらくDrawWindowもEventMモナドの中で手に入れられるはず。というわけで[Graphics.UI.Gtk.Gdk.EventM]のマニュアルを探してみるとeventWindowという関数を見つけました。
 
eventWindow :: EventM any DrawWindow
 
EventMモナドとして定義されています。以下のようにEventMモナドの中でDrawWindowを取り出してみましょう。
 
window `on` exposeEvent $ do
  win <- eventWindow
  return True
 
最後にTrueをEventMモナドに包んでいますね。EventMのマニュアルを読むと、描画イベントをハンドルしたらTrueを返さなければならないようです。Falseを返すとどうなるのでしょう?是非実験してみてください。次にgcNew関数を使ってGC(グラフィックスコンテキスト)を取り出したいのですが、ここで気をつけなければなりません。gcNew関数の型をよく見るとIOモナド関数ですね。Part2で少し話しましたが、EventMモナドの中で取り出したwinという値は、モナドの外には『絶対に』出せません。したがってwinを必要とするgcNew関数もEventMモナドの中で使うことになります。ここで疑問。EventMモナドの中でIOモナドを使う・・・うーん、doの中でdoを使う??
 
window `on` exposeEvent $ do -- EventMモナド
  win <- eventWindow
  do -- IOモナド!?
    gc <- gcNew win
    ...
  return True
 
上記はコンパイルできません。2つ目のdoはIOモナドのつもりで書いていますが、そうはならずにEventMモナドのdoと扱われます。こんなときはPart2の最後に解説したテクニックを使います。liftIO関数を使うとIOモナドをEventMモナドに変換する(持ち上げる)ことができるのでした。なおgcNew関数を呼び出すためにはGCパッケージのimportが必要です。
 
import Graphics.UI.Gtk.Gdk.GC
...
 
window `on` exposeEvent $ do -- EventMモナド
  win <- eventWindow
  liftIO $ do -- IOモナド
    gc <- gcNew win
    ...
  return True
 
ようやくgcを入手できたので何か書いて練習しましょう。[Graphics.UI.Gtk.Gdk.Drawable]パッケージのマニュアルを見ると色々な描画APIがあります。drawRectangle関数を使って四角形を書いてみることにします。:tで型を見ておきましょう。
 
> :t drawRectangle
drawRectangle
  :: DrawableClass d =>
     d -> GC -> Bool -> Int -> Int -> Int -> Int -> IO ()
 
Boolは塗りつぶすかどうか、Intはx,y,width,heightを渡します。画面描画という副作用を扱うので当然IOモナド関数ですね。IOモナドの部分だけ抜粋すると以下のような感じです。
 
liftIO $ do
  gc <- gcNew win
  drawRectangle win gc True 10 10 50 50
 
これで黒い四角が描画できます。今度はgcに色を設定して白い四角に変えてみましょう。gcSetValues関数を使ってgcに色を設定します。gcSetValues、newGCValuesについては説明を省きます。
 
liftIO $ do
  gc <- gcNew win
  gcSetValues gc newGCValues {
    foreground = Color 65535 65535 65535 -- white
  }
  drawRectangle win gc True 10 10 50 50
 
そろそろテトリスの画面表示に必要なテクニックが揃いつつあります。次にPart3で定義した各ブロックに対応する色を定義します。Blockを渡すとColorを返してくれるblockColor関数を作りましょう。
 
blockColor :: Block -> Color
blockColor b = case b of
  I -> Color (175*255) (223*255) (228*255) -- aqua
  O -> Color (255*255) (255*255) 0         -- yellow
  S -> Color 0         (255*255) 0         -- green
  Z -> Color (255*255) 0         0         -- red
  J -> Color 0         0         (255*255) -- blue
  L -> Color (243*255) (152*255) 0         -- orange
  T -> Color (167*255) (87*255)  (168*255) -- purple
  E -> Color (180*255) (180*255) (180*255) -- gray
  G -> Color 0         0         0         -- black
 
何となく読めますか?case式を使っていますね。caseの後のbについてパターンマッチが行われI〜Gの値に応じて分岐します。Colorは[Graphics.Rendering.Pango.Structs]パッケージに定義されています。3つの引数(0〜65535)をとってColor値をコンストラクトする関数です。3つの引数とはご想像の通りR,G,Bの値です。
 
もう一つテクニックを覚えましょう。10x20マスのボードの各ブロックを順番に描画したいのですが、IOモナドを繰り返し実行するループ処理が必要になります。こんな時に便利なのがforM_関数です。まずは型を見ましょう。
 
forM_ :: Monad m => [a] -> (a -> m b) -> m ()
 
mはMonadであることが型クラス制約になっています。mをIOモナドに置き換えるとこうなります。
 
forM_ :: [a] -> (a -> IO b) -> IO ()
 
この型定義から関数の内容を想像してみましょう。「繰り返し」は最初の引数[a]で表現されています。繰り返したいデータをリストで渡すということですね。そして二つ目の引数は(a -> IO b)という関数です。aという引数をもらってIO処理を行うIOモナド関数です。aという型引数は最初の[a]のリストの中身の型と一致していますね。このことから想像すると[a]から一つずつ中身が取り出され、(a -> IO b)に渡されて繰り返し実行されるのでしょう。そしてその繰り返しを一つのIOモナド「IO ()」にまとめ上げて返してくれるのがforM_関数の仕事となります。このように関数の型定義から内容を想像することは、Haskellプログラミングでしばしば行われる思考過程です。それでは簡単な例で実際に使ってみましょう。なおforM_関数を使うためにはControl.Monadパッケージをインポートする必要があります。
 
> :m + Control.Monad
> forM_ [1..5] (\i -> print i) 
1
2
3
4
5
 
1〜5のリストの各要素がiに渡されて実行されます。ふむ、書いてみると理解が深まります。(a -> IO b)とは各要素毎に実行されるコールバック関数、あるいはRubyのブロックのようなものですね。
 
とうとうこれで必要なテクニックが揃いました。いよいよ最後の大仕事です。ボード表示処理を含めてここまでの全コードを載せます。
 
 
実行の仕方は以下のようにghciで読み込んでmain関数を呼び出してください。ボード画面が表示されます(参考画像1)
 
$ ghci Tetris.hs
> main
 
本記事では細かく触れていないコードもありますが、コメントを頼りに調べていただけるといいかと思います。それにしてもボードを表示するだけでPart4まで来てしまいましたね。完成までの道のりはまだまだ遠いとです・・・
 
以上
 
参考画像1
2014-12-06
 
第35回目:Haskellでテトリス(Part3)
2014-11-24
筆者:村田
 
こんにちは。
 
3連休はいかがお過ごしですか?私は土曜日にネットワークの勉強会に参加してきました。講師の方は現場で一緒に仕事させていただいているお客様です。良かったらとお誘いを受けてお邪魔いたしまいた。丸一日みっちり勉強し、終了後の打ち上げでもネットワークやシステムの話ができてとても楽しかったです。社内向け勉強会という形式ですが、200ページ以上の資料はかなりの力作で、ところどころ手を動かす実習もあり大変勉強になりました。本を読んだり自分でプログラムを書いて勉強するのも良いですが、たまには外に出てみんなで勉強するというのも楽しいものだなと感じました。準備する側は非常に大変ですが・・
 
さて、それではテトリスの作成を進めていきたいと思います。まず、テトリスのあのブロックの種類はいくつあるか思い出せますか?間違えそうなのでWikipediaで調べてみました。ブロックのことを正しくはテトリミノと呼ぶそうです。全部で7種類あり、それぞれ以下の名前が付いています。
 
・I-テトリミノ(水色) ※縦棒
・O-テトリミノ(黄色) ※四角
・S-テトリミノ(黄緑)
・Z-テトリミノ(赤)
・J-テトリミノ(青)
・L−テトリミノ(オレンジ)
・T-テトリミノ(紫)
 
最初のアルファベットがテトリミノの形をうまく表しています。では各ブロック(テトリミノ)をプログラムで区別するにはどうしましょう。C言語で単純に実装するならば
 
#define I_MINO (1)
#define O_MINO (2)
・・・
 
と数字で定義するかもしれません。C++ならenum値にするかもしれませんね。しかしどちらの方法も型チェック機能が不十分で不具合を出しやすく、やはり専用の型を作って安全性を高めたいところです。Haskellは型を作る手間(記述量)が少ないので、型安全性、及び可読性を上げるために積極的に自作の型を導入していくべきです。Haskellで自前の新しい型を作る方法はdataとnewtypeがありますが、今回はdataで型を作る方法を覚えましょう。
 
data 型名 = 値名 | 値名 ...
 
と書きます。例えばBool型は
 
data Bool = True | False
 
となります。型名と値名はそれぞれ大文字から始めます。Bool型はTrueかFalseという値で構成されます。では各ブロックを表す型を作ってみましょう。なおコードはTetris.hsというファイルに書き、随時ghciで読み込んでテストしながら進めます。
 
$ emacs Tetris.hs
 
data Block = I | O | S | Z | J | L | T
 
$ ghci Tetris.hs
> :t I
I :: Block
 
Iという値の型を調べてみるとBlock型であることが分かりますね。:tを使わずIという値をそのまま評価するとどうなりますか試してみましょう。
 
> I
<interactive>:3:1:
    No instance for (Show Block) arising from a use of ‘print’
    In a stmt of an interactive GHCi command: print it
 
printを使おうとしたらinstanceが無いというエラーが発生しました。ghciは値を評価するときにprintという関数を裏で使っているのですが、print I としたらエラーが出たということです。print関数の型を見てみましょう。
 
> :t print
print :: Show a => a -> IO ()
 
前回の記事を読まれた方は上記の関数定義も大体読めるのではないでしょうか。まずIOモナド関数であることがわかります。ということは、副作用を発生させる関数であり、おそらく引数aの値を画面に表示する副作用関数だろうと想像できます。もうひとつ分かることがあります。引数にはaという型変数が使われていますが、aはShowという型クラスに属していなければなりません。Showという型クラスについてもghciから調べることができます。型ではないので:tコマンドは使えませんが、:iコマンドで一般的なinformationを調べることができます。
 
> :i Show
class Show a where
  showsPrec :: Int -> a -> ShowS
  show :: a -> String
  showList :: [a] -> ShowS
    -- Defined in ‘GHC.Show’
 
(このあとたくさんのinstance定義が表示されますが省略)
 
型クラスはclassというキーワードから定義が始まります。Showという型クラスに属する型はshowsPrec関数とshow関数とshowList関数を定義していなければならないことを表しています。JavaやC#でのinterface定義に近いと思います。型クラスに属することを、「その型クラスのインスタンスになる」と言います。うーむ、クラスやインスタンスという用語が一般的なオブジェクト指向言語の意味とは全然違うのでややこしいですね。では自作のBlock型もShow型クラスに属させてみましょう(インスタンスにしてみましょう)。
 
instance Show Block where
  show I = "I"
  show O = "O"
  show S = "S"
  show Z = "Z"
  show J = "J"
  show L = "L"
  show T = "T"
 
instanceというキーワードから始めます。Show型クラスのインスタンスとしてBlock型を定義します。Show型クラスで定義されているshow関数のインタフェースは、show :: a -> Stringでした。aという型変数はBlock型に置き換えられますので、show :: Block -> Stringという関数を定義しなさいということです。Block型を文字列にすればよいので、上記のように7種類分の変換ロジックを書けばshow関数を定義できますね。Show型クラスは他にもshowsPrec関数やshowList関数を定義しろと言っていますが今回は実装しません。型クラスの関数には実装すべきものと、しなくても良いものとありますが、このあたりは今回説明を省略します。ともかく、これでghciで表示(print)することができるようになったはずです。
 
> print I
I
> I
I
 
OKOK。ついでにもう一つ、別の型クラスのインスタンスを定義しましょう。等値性を表すEq型クラスです。
 
> :i Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
    -- Defined in ‘GHC.Classes’
 
今度は(==)関数を実装しろということですね。(/=)関数の方は実装しなくても良い関数です。(==)関数を定義すると自動的に生成されます。ではインスタンスを定義しましょう。
 
instance Eq Block where
  I == I = True
  O == O = True
  S == S = True
  Z == Z = True
  J == J = True
  L == L = True
  T == T = True
  _ == _ = False
 
最後の_(アンダースコア)はどんなBlock型の値にも一致するので、I == OはFalseになるという意味です。では試してみましょう。
 
> I == I
True
> I == O
False
> I /= O
True
 
正しく比較できています。さて、ここでちゃぶ台を返しましょう。実はShowやEqのような基本的な型クラスに関しては自動的にインスタンスを生成してくれる機能があります。自動導出/派生(derive)すると言います。Blockの型定義の最後にちょっと付け足すだけです。
 
data Block = I | O | S | Z | J | L | T deriving (Show, Eq)
 
型クラスのインスタンス生成の仕組みは一度手を動かしておくといいかと思い、回りくどく説明しましたが、上記のようにderiving節を追加するだけでShow型クラスやEq型クラスのインスタンスを自動的に定義できます。
 
次にテトリスのボード画面を作りたいと思います。早くそれっぽい画面を出してモチベーションを上げたいところです。テトリスのボード画面は縦横20x10マスです。型としては2次元配列がいいでしょう。ボードを描画するときに各ブロックの色が違うので、2次元配列にはブロック定義を並べたら良さそうです。というわけでボードの型は[[Block]]という2次元リストで考えたいと思います。C言語のtypedefのように、Haskellでも既存の型に別名をつけて可読性を上げる方法があります。新規の型を作るときはdataを使いましたが、別名定義にはtypeを使います。type 別名 = 元の型名 です。
 
type Board = [[Block]]
 
ここでBoardのことを考えていて一つ問題が浮かびました。どのテトリミノも置かれていないボードの空白部分をどう表しましょう。Block型のメンバにEmpty値があった方が良さそうですね。あと、この手のボードゲーム実装の常套句ですが、ボードの周囲にガード領域があった方がいいです。配列のインデックスを意識する代わりにガード(番兵)があるかどうかをアルゴリズムに取り入れるのです。ではBlock型の定義にE(Empty)とG(Guard)を追加しましょう。
 
data Block = I | O | S | Z | J | L | T | E | G deriving (Show, Eq)
 
もう一つBoardの定義で工夫したい点があります。ゲーム画面上は縦20マスですが、実装上は縦22マスにしたいのです。理由は縦棒(I-ミノ)を画面に出現させるとき、ゲーム画面には下2ブロックのみ表示し、画面外に残りの上2ブロックがある状態にしたいのです。このように画面に見えない部分にもブロックを置ける領域を作ることで、後の実装が少し楽になります。それと、テトリスミノは左右か下にしか移動しないのでガードは左右と下にあれば十分です。まとめるとボードの実体は23x12マスと定義することになります。
 
ではボードの初期状態を生成する関数を作ってみましょう。以下のようにベタに23x12の2次元リストを書いてもいいですよ。
 
initBoard :: Board
initBoard = [[G,E,E,E,E,E,E,E,E,E,E,G],[G,E,E,E,...
 
しかし、もう少し綺麗に書きたいですね。Haskellにはpythonのように「リスト内包表記」を使ってリスト生成を楽に書くことができます。内包というのはリストの括弧の中に生成式やフィルタ式を書けるという意味です。ghciでいくつか例を試しましょう。
 
> [x*2 | x <- [1,2,3,4,5]]
[2,4,6,8,10]
 
リスト内包表記は一般的に以下のような形をしています。
 
[要素 | 生成器, 生成器, ・・・, ガード式, ガード式, ・・・]
 
先ほどの例は、x <- [1,2,3,4,5]が生成器です。ガード式はありません。生成器がxを1,2,3,4,5に順番に束縛します。それぞれに対し、xを2倍してリストの要素としています。では次の例で生成器が作る値をガード式で間引いてみましょう。
 
> [x*2 | x <- [1,2,3,4,5], x `mod` 2 == 0]
[4,8]
 
ガード式は必ず真偽値を返す式となります。上記例のガード式はxが偶数の場合に真となるので、xに束縛される値は2と4になります。それぞれ2倍してリストの要素にしています。今度は複数の生成器を置いてみましょう。
 
> [(x,y) | x <- [1,2,3], y <- ['a','b','c']]
[(1,'a'),(1,'b'),(1,'c'),(2,'a'),(2,'b'),(2,'c'),(3,'a'),(3,'b'),(3,'c')]
 
xが1に束縛され、yがa~cに順番に束縛されて(x,y)の組みを生成しています。2番目に書いたyの生成器の方が早く回ります。この他にもリスト内包表記は便利なので色々研究してみてください。では、リスト内包表記を使ってinitBoard関数を作ってみましょう。
 
initBoard :: Board
initBoard = [mid | _ <- [1..22]] ++ [last]
  where mid  = [G,E,E,E,E,E,E,E,E,E,E,G]
        last = [G,G,G,G,G,G,G,G,G,G,G,G]
 
生成器が生み出す1〜22の値は読み捨てていますが、要素の生成が22回繰り返されるので、midという1次元リストを22行分作っています。midは両脇がG(ガード)で、その中に10マスのE(Empty)が置いてあります。最後の23行目となるlastはガードのみです。23x12のボード生成に関して、他にも色々な書き方があると思います。リスト内包表記を使わずrepeat関数などを使ってもいいですね。もっとシンプルな書き方があったら是非教えてください。
 
今回はここまでにします。型クラスとインスタンス定義、リスト内包表記に触れましたが、ボードをGUI画面に出すとこまではいけませんでした。次回は頑張りたいと思います。
 
今月もHaskellの新しい書籍が2つ出版されました。「関数プログラミング実践入門」を楽しく読み進めていますが、こちらは訳本ではなくオリジナルなので読みやすく、またWhatやHowの説明に頼らずWhyをきちんと理解させようとする良書に当たったな〜というのが今のところの感想です。冒頭の勉強会でもWhyを要所要所できちんと説明しており、受講者の記憶に残そうという配慮が感じられました。それにしても今月は他にも面白そうなRuby本が出るみたいで、お金がもたないぞい・・・
 
以上
 

連載記事のソースコード

連載記事のソースコード
 
・Haskellで問題を解く(Part1〜Part4) ソースコード
・Clojureで8クイーン問題にチャレンジ(Part1〜Part5) ソースコード
・OCamlでへびゲームを作る(Part1〜Part5) ソースコード
・Swiftでオセロを作る(Part1〜Part5) ソースコード
・Processingでシューティング(Part1〜Part4) ソースコード 
Haskellでテトリス(Part1〜Part9) ソースコード
・プチコン3号(BASIC)でさめがめ(Part1〜Part3) ソースコード
・Prologでさめがめを解く(Part1〜Part6) ソースコード