参考画像1〜22014-12-27第37回目:Haskellでテトリス(Part5)2014-12-20筆者:村田こんにちは。前回はボード画面を表示しました。これからテトリミノの表示に向けて頑張りましょう。テトリミノには複数の種類がありますが、それぞれに中心位置や形状という属性を持ちそうだなと想像ができます。これらはテトリミノの共通項です。したがって「型」を通じて同じものと見なした方が設計し易いはずです。他の言語でも構造体やクラスとして共通項をくくり出しますよね。テトリミノを表すデータ型を作りましょう。Part3では単純なデータ型の作り方を紹介しました。...ついでに具体例も。False今回はもう少し複雑な型を定義します。・・・こちらも具体例を見てみましょう。Int型名と値コンストラクタは両方ともBookとしましたが異なっていても構いません。値コンストラクタであるBookは3つの引数をとります。1つ目はString型で本のタイトル、2つ目はString型のリストで著者リスト、3つ目は管理番号です。ところで単純な値として紹介したTrueやFalseですが、実はBookと同じく値コンストラクタです。TrueやFalseはパラメータを一つも取らずにBool型の値を生成する値コンストラクタというわけです。Book型の値を実際に作ってみましょう。3つのパラメータを値コンストラクタに渡します。8値コンストラクタは引数をとって値を返します。型を表示させてみましょう。Bookふむ、やはり関数っぽい。実際に関数を引数にとるような場面で値コンストラクタを渡すことができます。値コンストラクタは関数と同じではありません。そもそも普通の関数とは異なり大文字から始まっていますしね。値コンストラクタは関数とは異なりパターンマッチができます。このパターンマッチを利用してBookデータからタイトルを抜き出してみましょう。_)の部分がパターンマッチです。値として渡されたBookデータをバラバラにして、著者リストと管理番号を捨て、タイトルだけ変数に束縛してます。試しにBookの値を作り、getTitleでタイトルを取り出してみましょう。Rings"さて、これで新しいパラメータ付きのデータ型を作ったり、各パラメータをパターンマッチを使って取り出すことができるようになりました。実は同じことをもっと短く書ける「レコード構文」というテクニックがあるので紹介します。}ダブルコロンの前に関数名を書くとパラメータを取り出せる関数が自動生成されます。Wコロンといえば先日笑点で漫才をやっていました。コンビ仲ネタの緊張感もあって目が離せないというかなんというか・・・。それはともかくghciでもう一度テストしてみましょう。loR["Tolkien"]レコード構文を使うと、データ型の中身へのアクセサが簡単に手に入るので便利ですね。それではレコード構文をつかってテトリミノのデータ型を定義してみましょう。今のところパラメータとして必要になりそうなのは、ブロック種別と位置と形状くらいでしょうか。それと位置についてはボードの2次元座標を使いますのでエイリアスもついでに用意しておきましょう。Show形状[Pos]は位置Posを基準にした相対位置をリストにしたものです。Mino型の値として手始めにI-ミノを作ってみましょう。I-ミノ(5,2)という位置はテトリスで新しいミノが落ちてくる初期位置です。x座標は中央の5です。y座標は2となっていますが、これはy座標0と1が画面に表示しないダミー領域だからです(Part3参照)。形状を表す位置リストからI-ミノがイメージできますか?中心位置から1つ上と2つ上(これらの部分は画面外)、そして1つ下にブロックを並べるとI-ミノになります。レコード構文で作ったデータ型の場合、もっと明示的にパラメータを渡して値を生成することができます。I-ミノの別の作り方は以下のようになります。}各パラメータの意味が明確になり、またパラメータを任意の順番で記述することも可能になります。次にテトリミノをボードに置く関数を考えましょう。テトリミノとボードを渡すとテトリミノが置かれた新しいボードを返したいと思います。既存のボードデータの一部を書き換えるという副作用を起こすのではなく、"新しいボードデータを返す"ところが関数型プログラミングのポイントですね。いうわけで型はきっとこんな感じです。BoardんでputMinoを実装しようとすると、うーん難しい。?どーかな??あーでもない、こーでもないと考えた末、まずはこんなことがやりたくなりました。ボードの位置を指定して1マス分のブロック(IとかZとか)で置き換えたいなと。2次元ボードで考える前に1次元リストの任意の位置を置き換える方法を考えなくちゃ。どんな型の関数が欲しいのかと言うとこんな感じ。[a]aは覚えていますか?型変数ですね。任意の型を表しています。リストの位置を指定して同じ型の新しい値で置き換えたリストを返す関数です。汎用的なので既に存在するかもしれませんね。こんな時はHoogle(http://www.haskell.org/hoogle/)というWebサイトが役に立ちます。Haskellの関数を型で検索することができます。まあ実際に検索してみたところ、所望の関数は見つかりませんでしたので自分で作ることにしましょう。takeやtailやdropは詳しく紹介しません。型を見たり実際に使ってみてください。whereは今まで登場していなかったかもしれません。replaceという関数定義の中だけで使える一時変数ys,zsを定義するための構文です。whereを使わずに以下のようにも書けます。xs)好みの問題もありますが、ワンラインで長くなり過ぎる場合は、適度に一時束縛に置き換えるべきでしょう。ghciでテストしてみます。‘z’"abcdz”OKOK。次はreplaceを行と列に2回使って、指定位置を新しいブロックで置き換えてみたいと思います。xsちょっと複雑かもしれません。ボードのy行目(board!!y)を取り出し、そのx列目を新しいブロックで置き換え、できた新しい1行分のデータをxsに束縛します。次にボードのy行目をxsに丸々置き換えて新しいボードを作って返します。テストしてみましょう。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という関数を使いましょう。bちょっと関数の型が難しいですね。b)は2引数関数です。(+)関数がそうです。2つ目の引数bは基準となる値、3つ目の引数[a]は処理対象のリストです。戻り値はまとめ上げた値です。b)という関数を見ます。この関数の2つ目の引数bにはまとめ上げ途中の合計値が渡されます。[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の紹介です。'A'"AAA"では第1引数を3に固定したカリー化関数を作って使ってみましょう。(ghci上で関数定義する。"HOGE"["HOGE","HOGE","HOGE"]本題に戻り、putBlockという3引数関数から2引数関数を作ります。試しにfという関数にして型を見てみましょう。BoardputBlockにZというブロックを与えることで2引数関数が作れていますね。材料も揃ったので一気にputBlocks(複数形)を書いてみます。psfoldrの第1引数はブロックを固定したカリー化関数ですね。第2引数は基準となるボードです。最後が置き換えたい位置のリストです。ここで一つ思考過程にポイントがあります。putBlockを繰り返し呼び出して一つの値にまとめたい。putBlock関数の第1引数には[Pos]やBoardではなく、Blockを持ってこよう!上記のようにfoldrを使うと決めたら、putBlockの引数の順番を見直しましょう。作成したputBlocks関数をテストしましょう。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に置き換えることができました。それと今回から途中経過のソースコードもGitHubに上げていくことにしました。本ページの一番下のリンク集から参照できます。第36回目:Haskellでテトリス(Part4)2014-12-06筆者:村田こんにちは。12月です、早いですね。忘年会シーズンですが皆さん飲み過ぎにはご注意を。私は本日グロッキー状態でした・・・さて、本編の前に一つ紹介です。連載終了した分についてのソースコードをGitHubに上げました。本ページの一番下にリンク集を設けたのでこちらから参照して下さい。それではPart1〜2で紹介した単純なウインドウ表示プログラムをベースに進めていきましょう。ところでプログラムで画面に図形を描くためには何をしたらいいでしょう。場合、「描画イベントハンドラの中でグラフィックスコンテキストを使用して図形を描く」という処理になると思います。以前書いたSwiftのコードも同じで、drawRectという描画イベントハンドラの中で以下のようなコードを書きました。100))}Haskellでもやることは同じです。描画イベントハンドラを探しましょう。HackageというHaskellのAPIマニュアルからGtkパッケージ(http://hackage.haskell.org/package/gtk)に潜って黙々と探索します(結構大変です)。[Graphics.UI.Gtk.Abstract.Widget]というパッケージに各種イベントハンドラがありました。目的の描画イベントハンドラは以下のexposeEventです。Bool)この型情報から使い方をイメージするのは難しいかもしれません。Signal型をじっくり調べると内部構造を知ることができます。興味のある方は是非:tや:iコマンドでSignal型の構造を追ってみてください。今回はそこまでは踏み込まず、とにかく使ってみようと思います。Part1のGUIサンプルコードではdeleteEventというイベントハンドラを使っていましたね。そのまま真似してみましょう。do構文を使ってきっとこんな形になるはずです。doイベント処理ここでのdoはEventMというモナドです。モナドやdo構文についてはPart2で解説しているので参考にしてください。次はグラフィックスコンテキストの取得、使い方を探すとしましょう。再度もHackageに潜って調べます。ふむふむ[Graphics.UI.Gtk.Gdk.GC]パッケージのgcNew関数で取得できるようです。GC引数のdはDrawableClass型クラスに属していなければならないようです。どんな型がDrawableClass型クラスのインスタンスになっているか調べてみましょう。DrawWindowPixmap型とDrawble型とDrawWindow型がインスタンスになっているようです。3つの型を全て調べるのは大変なので勘を働かせます。gcNew関数の引数dにはきっとDrawWindow型を渡すのでしょう。DrawWindow型の値はどうやって手に入れるのでしょう。ここも論理的な道筋ではなくて申し訳ないのですが勘を働かせます。Swiftの例でもそうでしたが、大抵の場合、グラフィックスコンテキストは描画イベントハンドラの中で取得します。DrawWindowもEventMモナドの中で手に入れられるはず。いうわけで[Graphics.UI.Gtk.Gdk.EventM]のマニュアルを探してみるとeventWindowという関数を見つけました。DrawWindowEventMモナドとして定義されています。以下のようにEventMモナドの中でDrawWindowを取り出してみましょう。True最後にTrueをEventMモナドに包んでいますね。EventMのマニュアルを読むと、描画イベントをハンドルしたらTrueを返さなければならないようです。Falseを返すとどうなるのでしょう?是非実験してみてください。次にgcNew関数を使ってGC(グラフィックスコンテキスト)を取り出したいのですが、ここで気をつけなければなりません。gcNew関数の型をよく見るとIOモナド関数ですね。Part2で少し話しましたが、EventMモナドの中で取り出したwinという値は、モナドの外には『絶対に』出せません。したがってwinを必要とするgcNew関数もEventMモナドの中で使うことになります。True上記はコンパイルできません。2つ目のdoはIOモナドのつもりで書いていますが、そうはならずにEventMモナドのdoと扱われます。こんなときはPart2の最後に解説したテクニックを使います。liftIO関数を使うとIOモナドをEventMモナドに変換する(持ち上げる)ことができるのでした。gcNew関数を呼び出すためにはGCパッケージのimportが必要です。Trueようやくgcを入手できたので何か書いて練習しましょう。[Graphics.UI.Gtk.Gdk.Drawable]パッケージのマニュアルを見ると色々な描画APIがあります。drawRectangle関数を使って四角形を書いてみることにします。:tで型を見ておきましょう。()Boolは塗りつぶすかどうか、Intはx,y,width,heightを渡します。画面描画という副作用を扱うので当然IOモナド関数ですね。IOモナドの部分だけ抜粋すると以下のような感じです。50これで黒い四角が描画できます。今度はgcに色を設定して白い四角に変えてみましょう。gcSetValues関数を使ってgcに色を設定します。gcSetValues、newGCValuesについては説明を省きます。50そろそろテトリスの画面表示に必要なテクニックが揃いつつあります。次にPart3で定義した各ブロックに対応する色を定義します。Blockを渡すとColorを返してくれるblockColor関数を作りましょう。black何となく読めますか?case式を使っていますね。caseの後のbについてパターンマッチが行われI〜Gの値に応じて分岐します。Colorは[Graphics.Rendering.Pango.Structs]パッケージに定義されています。3つの引数(0〜65535)をとってColor値をコンストラクトする関数です。3つの引数とはご想像の通りR,G,Bの値です。一つテクニックを覚えましょう。10x20マスのボードの各ブロックを順番に描画したいのですが、IOモナドを繰り返し実行するループ処理が必要になります。こんな時に便利なのがforM_関数です。()mはMonadであることが型クラス制約になっています。mをIOモナドに置き換えるとこうなります。()この型定義から関数の内容を想像してみましょう。「繰り返し」は最初の引数[a]で表現されています。繰り返したいデータをリストで渡すということですね。aという引数をもらってIO処理を行うIOモナド関数です。aという型引数は最初の[a]のリストの中身の型と一致していますね。b)に渡されて繰り返し実行されるのでしょう。()」にまとめ上げて返してくれるのがforM_関数の仕事となります。このように関数の型定義から内容を想像することは、Haskellプログラミングでしばしば行われる思考過程です。それでは簡単な例で実際に使ってみましょう。forM_関数を使うためにはControl.Monadパッケージをインポートする必要があります。i)123451〜5のリストの各要素がiに渡されて実行されます。ふむ、書いてみると理解が深まります。b)とは各要素毎に実行されるコールバック関数、あるいはRubyのブロックのようなものですね。これで必要なテクニックが揃いました。ボード表示処理を含めてここまでの全コードを載せます。実行の仕方は以下のようにghciで読み込んでmain関数を呼び出してください。main本記事では細かく触れていないコードもありますが、コメントを頼りに調べていただけるといいかと思います。それにしてもボードを表示するだけでPart4まで来てしまいましたね。12014-12-06参考画像1第35回目:Haskellでテトリス(Part3)2014-11-24筆者:村田こんにちは。3連休はいかがお過ごしですか?私は土曜日にネットワークの勉強会に参加してきました。講師の方は現場で一緒に仕事させていただいているお客様です。良かったらとお誘いを受けてお邪魔いたしまいた。丸一日みっちり勉強し、終了後の打ち上げでもネットワークやシステムの話ができてとても楽しかったです。社内向け勉強会という形式ですが、200ページ以上の資料はかなりの力作で、ところどころ手を動かす実習もあり大変勉強になりました。本を読んだり自分でプログラムを書いて勉強するのも良いですが、たまには外に出てみんなで勉強するというのも楽しいものだなと感じました。準備する側は非常に大変ですが・・さて、それではテトリスの作成を進めていきたいと思います。テトリスのあのブロックの種類はいくつあるか思い出せますか?間違えそうなのでWikipediaで調べてみました。ブロックのことを正しくはテトリミノと呼ぶそうです。全部で7種類あり、それぞれ以下の名前が付いています。・S-テトリミノ(黄緑)・Z-テトリミノ(赤)・J-テトリミノ(青)・L−テトリミノ(オレンジ)・T-テトリミノ(紫)最初のアルファベットがテトリミノの形をうまく表しています。各ブロック(テトリミノ)をプログラムで区別するにはどうしましょう。(2)・・・と数字で定義するかもしれません。C++ならenum値にするかもしれませんね。どちらの方法も型チェック機能が不十分で不具合を出しやすく、やはり専用の型を作って安全性を高めたいところです。Haskellは型を作る手間(記述量)が少ないので、型安全性、及び可読性を上げるために積極的に自作の型を導入していくべきです。Haskellで自前の新しい型を作る方法はdataとnewtypeがありますが、今回はdataで型を作る方法を覚えましょう。Falseとなります。型名と値名はそれぞれ大文字から始めます。Bool型はTrueかFalseという値で構成されます。各ブロックを表す型を作ってみましょう。コードはTetris.hsというファイルに書き、随時ghciで読み込んでテストしながら進めます。BlockIという値の型を調べてみるとBlock型であることが分かりますね。:tを使わずIという値をそのまま評価するとどうなりますか試してみましょう。itprintを使おうとしたらinstanceが無いというエラーが発生しました。したらエラーが出たということです。print関数の型を見てみましょう。()前回の記事を読まれた方は上記の関数定義も大体読めるのではないでしょうか。IOモナド関数であることがわかります。いうことは、副作用を発生させる関数であり、おそらく引数aの値を画面に表示する副作用関数だろうと想像できます。ひとつ分かることがあります。引数にはaという型変数が使われていますが、aはShowという型クラスに属していなければなりません。Showという型クラスについてもghciから調べることができます。型ではないので:tコマンドは使えませんが、:iコマンドで一般的なinformationを調べることができます。‘GHC.Show’(このあとたくさんのinstance定義が表示されますが省略)型クラスはclassというキーワードから定義が始まります。Showという型クラスに属する型はshowsPrec関数とshow関数とshowList関数を定義していなければならないことを表しています。JavaやC#でのinterface定義に近いと思います。型クラスに属することを、「その型クラスのインスタンスになる」と言います。うーむ、クラスやインスタンスという用語が一般的なオブジェクト指向言語の意味とは全然違うのでややこしいですね。自作のBlock型もShow型クラスに属させてみましょう(インスタンスにしてみましょう)。"T"instanceというキーワードから始めます。Show型クラスのインスタンスとしてBlock型を定義します。Stringという関数を定義しなさいということです。Block型を文字列にすればよいので、上記のように7種類分の変換ロジックを書けばshow関数を定義できますね。Show型クラスは他にもshowsPrec関数やshowList関数を定義しろと言っていますが今回は実装しません。型クラスの関数には実装すべきものと、しなくても良いものとありますが、このあたりは今回説明を省略します。これでghciで表示(print)することができるようになったはずです。ついでにもう一つ、別の型クラスのインスタンスを定義しましょう。等値性を表すEq型クラスです。‘GHC.Classes’今度は(==)関数を実装しろということですね。(/=)関数の方は実装しなくても良い関数です。(==)関数を定義すると自動的に生成されます。インスタンスを定義しましょう。OはFalseになるという意味です。OTrue正しく比較できています。ここでちゃぶ台を返しましょう。実はShowやEqのような基本的な型クラスに関しては自動的にインスタンスを生成してくれる機能があります。自動導出/派生(derive)すると言います。Blockの型定義の最後にちょっと付け足すだけです。Eq)型クラスのインスタンス生成の仕組みは一度手を動かしておくといいかと思い、回りくどく説明しましたが、上記のようにderiving節を追加するだけでShow型クラスやEq型クラスのインスタンスを自動的に定義できます。次にテトリスのボード画面を作りたいと思います。早くそれっぽい画面を出してモチベーションを上げたいところです。テトリスのボード画面は縦横20x10マスです。型としては2次元配列がいいでしょう。ボードを描画するときに各ブロックの色が違うので、2次元配列にはブロック定義を並べたら良さそうです。いうわけでボードの型は[[Block]]という2次元リストで考えたいと思います。C言語のtypedefのように、Haskellでも既存の型に別名をつけて可読性を上げる方法があります。新規の型を作るときはdataを使いましたが、別名定義にはtypeを使います。[[Block]]ここでBoardのことを考えていて一つ問題が浮かびました。どのテトリミノも置かれていないボードの空白部分をどう表しましょう。Block型のメンバにEmpty値があった方が良さそうですね。あと、この手のボードゲーム実装の常套句ですが、ボードの周囲にガード領域があった方がいいです。配列のインデックスを意識する代わりにガード(番兵)があるかどうかをアルゴリズムに取り入れるのです。Block型の定義にE(Empty)とG(Guard)を追加しましょう。Eq)もう一つBoardの定義で工夫したい点があります。ゲーム画面上は縦20マスですが、実装上は縦22マスにしたいのです。理由は縦棒(I-ミノ)を画面に出現させるとき、ゲーム画面には下2ブロックのみ表示し、画面外に残りの上2ブロックがある状態にしたいのです。このように画面に見えない部分にもブロックを置ける領域を作ることで、後の実装が少し楽になります。それと、テトリスミノは左右か下にしか移動しないのでガードは左右と下にあれば十分です。まとめるとボードの実体は23x12マスと定義することになります。ボードの初期状態を生成する関数を作ってみましょう。以下のようにベタに23x12の2次元リストを書いてもいいですよ。[[G,E,E,E,E,E,E,E,E,E,E,G],[G,E,E,E,...しかし、もう少し綺麗に書きたいですね。Haskellにはpythonのように「リスト内包表記」を使ってリスト生成を楽に書くことができます。内包というのはリストの括弧の中に生成式やフィルタ式を書けるという意味です。ghciでいくつか例を試しましょう。[1,2,3,4,5]][2,4,6,8,10]リスト内包表記は一般的に以下のような形をしています。[1,2,3,4,5]が生成器です。ガード式はありません。生成器がxを1,2,3,4,5に順番に束縛します。それぞれに対し、xを2倍してリストの要素としています。次の例で生成器が作る値をガード式で間引いてみましょう。0][4,8]ガード式は必ず真偽値を返す式となります。上記例のガード式はxが偶数の場合に真となるので、xに束縛される値は2と4になります。それぞれ2倍してリストの要素にしています。今度は複数の生成器を置いてみましょう。['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関数を作ってみましょう。[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を要所要所できちんと説明しており、受講者の記憶に残そうという配慮が感じられました。