第40回目:Haskellでテトリス(Part8)2015-01-25筆者:村田こんにちは。新しいプログラミング勉強サイト(exercism.io)を試しています。問題を解くと他のユーザから良い書き方についてのフィードバックがもらえます。問題を解くと次の問題が手に入るというゲーム感覚と、対応言語が多い(現在20種類)ところが面白いです。私はまだHaskell問題しか手をつけていませんが、他にC#、Javaの問題や、Swift、CoffeeScriptなど新しい言語もあります。暇な時間にプログラミングでも、という時におすすめです。テトリス作成の続きです。前回、IORefを扱ってテトリミノを操作できるようになりましたが、今のままではテトリミノが画面外にまで移動できてしまいます。移動できるかどうかの判定が必要ですね。そこで、まずはテトリミノをボードに配置できるかチェックする関数を作りたいと思います。その場所が空(E)ならば置いてよしall関数の型は以下のとおりで、述語関数とリストを渡して、リストの全要素がTrueであるかをチェックする関数です。BoolcanPut関数をテストしてみましょう。T−ミノを初期位置から左に3回移動できるかテストします。minoTrue4回は移動できないことをテストします。minoFalse次に、前回作成のキーボードイベント処理を改造します。テトリミノの移動を確定する前にcanPut関数でチェックしたいと思います。if式を使って以下のように書くことができます。()if式はthen句とelse句の型を揃える必要があります。()型の値を生成して返しています。なんだかちょっと無駄な記述ですね。条件を満たした時にのみ副作用を発生させる、というのは頻出パターンですので、もう少しスッキリ書ける便利な関数があります。Control.Monadモジュールのimportが必要です。()という型になります。when関数を使って先程のif式を書き換え、さらにキーボードイベント処理に組み込みましょう。}...else句を書かなくて良いので便利です。これでテトリミノが画面外に移動しないようになりました。次は自動落下処理を実装したいと思います。タイマー処理をどんな風に書くか調べてみましょう。Hackageで見つけてきました。Graphics.UI.Gtk.General.GenralモジュールのtimeoutAdd関数を使います。HandlerId第1引数に実行するIOモナドを渡します。このモナドがTrueを返している間繰り返し実行されます。第2引数はタイマ間隔をミリ秒で設定します。timeoutAdd関数に渡すIOモナド(落下処理)を書きましょう。このIOモナドは少し大きいですが、特に新しいトピックはありませんので一気に載せます。}ポイントは最後のelse句です。落下できないテトリミノはその場所で固定化されユーザーは操作できなくなります。次の新しいブロックが画面上端から落ちてきます。今のところ新しく出現するテトリミノはT-ミノに限定しています。このautoDownというIOモナドをtimeoutAdd関数に渡してタイマー実行してみましょう。main関数の中でtimeout関数を使います。1000...っと、コンパイルエラーです。タイマ呼び出しをずっと繰り返したいのでTrueをIOモナドに包んで最後に返します。複数のIOモナドを合成する方法は、do記法(Part2で解説)の中でIOモナドを並べるだけです。以下のように書いてみましょう。1000コンパイルは通りました。ゲーム状態の上ではテトリミノが落下しているのですが、画面が更新されていません。描画イベントをキックしましょう。最終的に以下のようなコードになります。1000もう一歩実装を進めましょう。新しく出現するテトリミノをT-ミノに限定していましたが、ランダムに出現するようにしたいと思います。ランダムな値の取得も副作用の一つなのでIOモナドの出番です。以下のgetStdRandom関数でランダムな値を取得できます。System.Randomモジュールのインポートが必要です。a型を見ると引数を渡す必要があるようです。StdGen))という型ですね。StdGen型について調べてみましょう。‘System.Random’ここで一つ手掛かりを得ました。StdGenはRandomGen型クラスのインスタンスになっているようです。RandomGen型クラスはどんなインタフェースを持っているか・・・ここでは深く見ていきませんが、:iで調べることができます。StdGen)という型の値(関数)を作れるでしょうか。分からないのでHoogle(https://www.haskell.org/hoogle/)で調べてみましょう。StdGen)と調べたい型を入力すると関連する関数を検索してくれます。ドンピシャの関数は見つかりませんでしたが、以下の2つの関数に目をつけました。g)StdGen型はRandomGenの一員ですので、上記は以下の関数として見ることができます。StdGen)それと型変数aにも型クラス制約が付いていますね。型変数aはRandom型クラスのインスタンスでなければなりません。ghciに聞いてみましょう。‘System.Random’なるほど、数値や文字などランダムな値になりうる型はRandom型クラスのインスタンスになっているようです。うーむ、一度に頭に入れる型や型クラスが多くなってくると結構辛いものがありますが、型合わせのパズルゲームだと思って色々試して感覚をつかみましょう。えっと、つらつら書きましたが、実はちゃんとマニュアル(http://hackage.haskell.org/package/random-1.1/docs/System-Random.html)を見れば使い方が書いてあります。StdGen))をそのまま渡しましょう。random147028643959194987ランダムな数値が得られました。もらった数を剰余(mod)すれば丸められますが、もう一方の関数、randomR関数を調べてみましょう。この関数は引数に(a,a)をもらうところがrandom関数と異なるところです。マニュアルを見ると値の範囲(range)を渡すようです。(0,6)6OK、これを使っていきましょう。7種類のテトリミノからランダムに選択する関数は以下のようになります。[i_Mino,o_Mino,s_Mino,z_Mino,j_Mino,l_Mino,t_Mino]!!iでは先程のautoDown関数を修正して、T-ミノ固定ではなくランダムなテトリミノが出現するようにしましょう。}今回はここまでにします。参考画像のように大分テトリスっぽくなりました。あとはライン消し処理を実装するだけです。新年おめでとうございます。今年も皆様にとって実り多い良き一年でありますように。昨年に引き続きテトリスの実装を行います。今回はキーボードでテトリミノを操作できるようにしたいと思います。キーボードの入力もイベントモナドで取得します。Hackageサイトでそれっぽいイベントを探してみるとkeyPressEventというイベントがありました。Bool)このイベントモナドの中でeventKeyVal関数を使用してキーの値を取得します。KeyValモナド関数の使い方を覚えていますか?EventMモナドに包まれたKeyVal型の値を取り出すにはdo構文を使います。eventKeyVal...取り出したkeyvalは16bitの数値です。キーボードの上下左右キーを押した時の数値と、自作のMove型をマッピングするとこんな感じです。NONE...KeyVal型にEnumのような命名的な値コンストラクタがあればいいのですが、無いので仕方なく数値によるパターンマッチを行っています。諦めずにマニュアルを漁っていると16bitのキー値を文字列に変換する方法がありました。TextText型からString型に変換する関数も一緒に使います。String型への変換をghciで試してみましょう。unpack関数を使うためにData.Textモジュールをimportしておく必要があります。65364)"Down"65364という値から分かりやすい文字列に変換できました。これで以下のように数値ではなく文字列でパターンマッチができますね。NONE...では取得したmoveを以下のようにコンソールに表示してみましょう。コンソール出力はprint関数で行いますが、これはIOモナド関数ですので、liftIO関数を使ってIOモナドをEventMモナドに持ち上げて使います。True(省略)上下左右キーを押すとコンソールに"DOWN"や"ROTATE"と表示され、キーの値が取れることを確認できました。この時点で少しリファクタリングしましょう。上記のmoveを取り出す部分をgetMove関数として切り出してみようと思います。move切り出した関数はイベントモナド関数になっています。最後にmoveをモナドに包み込んで返しています。ところでgetMove関数の型は分かりますか?イベントモナドにMove型を包んで返すので、答えは以下のようになります。Moveでは、元の処理からgetMove関数を呼び出すようにしましょう。TrueOK、すっきりしました。次の課題、今回の山場です。getMoveでmoveを取得した後のことを考えましょう。今はコンソールに表示していますが、moveMino関数でテトリミノを動かすように直しましょう。mino...これはNG。右辺で現在のminoを参照し、左辺でminoを上書きしていますが、純粋関数型言語であるHaskellでは変数の書き換えができません。oldMino...確かにnewMinoは書き換えられていません。いづれどこかでnewMinoからoldMinoに反映させなければなりませんね。いつまでたってもoldMinoが同じ値ではテトリミノは連続して動きません。ところがoldMinoの書き換えもNGなので、ほんと困りマックスです。他の言語では当たり前に行っている変数の書き換えが、Haskellでは簡単にはできません。変数の書き換え(状態の更新)は、ファイルI/Oやコンソール入出力と同じ副作用です。副作用ということはIOモナドの出番ですね。この後紹介する3つのIOモナド関数を覚えて、書き換え可能な変数を扱えるようになりましょう。書き換え可能な変数を作るnewIORef関数です。Data.IORefモジュールのインポートが必要です。a)モナド関数の見方は慣れてきましたか?これはIOモナド関数ですね。a型)が包まれています。使用例を見てみましょう。String)型の値を作れたので、参照を剥がして(?)さらに中身の値を取り出してみましょう。readIORef関数を使います。aこれもIOモナド関数です。使用例を見てみましょう。String)型の値を書き換えてみましょう。writeIORef関数を使います。()変数を書き換えるという副作用を起こしてみましょう。Year!"と表示されます。writeIORefでstrref変数を書き換えることができました。Stringを束縛しているstr変数やstr'変数は相変わらず書き換えられないので注意してください。それではテトリスに戻りましょう。ボードとテトリミノのIORefをバラバラに作っても良いですが、ボードとテトリミノを合わせて「ゲーム状態」という型を作り、まとめて書き換えられるようにしようと思います。今回のテトリス作成では実装しませんが、ゲームスコア機能を追加しようとした場合、スコアも書き換えが必要な状態変数になりますので、そのときは以下のゲーム状態型に加えてもいいかもしれません。GameStatus)型はmain関数の最初にでも作ってしまいましょう。windowNew...次にmain関数内の描画イベント処理を修正します。これまではいつも同じ状態を描画していましたが、現在のボード+テトリミノを描画するようにしましょう。board前半で作ったキーボードイベント処理も直します。getMove関数で取り出した操作に応じて、現在のテトリミノ状態を書き換えましょう。それと処理の最後に描画イベントが発生するようにキックしておきます。これを行わないとゲーム状態変数が変わるだけで画面に反映されません。Trueさあ、動かしてみましょう。上下左右キーを操作するとテトリミノが移動したり、回転するようになりました。(参考画像1)HaskellでもIOモナド(IORef)を使えば状態変数が割と簡単に書けます。IORefはここぞというところで使うべきでしょう。参考画像12015-01-10キーボード操作時のスクショ第38回目:Haskellでテトリス(Part6)2014-12-27筆者:村田こんにちは。今年も残すところあと僅かとなりました。振り返ってみてどんな一年でしたか?こんなとき日記を付けていればな〜と年始に買った日記帳を開いては、驚きの白さにそっ閉じしてしまいます。図書館に行くときなどに持っていたA4ノートをめくってみたところ、こちらは書き込まれていて味が出ていました。試験勉強の跡、買い物リスト、地図、棒人間(ユースケース図っぽいけど、ただの落書き)、謎のページ数メモ(どの本だよ)・・・。皆さんも普段プライベートに書き込んでいる物、例えばスケジュール帳や手帳などを読み返してみると面白いかもです:-)さて、前回はボード上のブロックを置き換える関数を作りました。テトリミノを画面に表示できそうですので頑張りたいと思います。テトリミノの値から各ブロックの位置を計算する関数を作りましょう。minowhere句でminoの中心位置を(x,y)に、相対位置リストをshapeに束縛します。getPosやgetShape関数は前回登場したレコード構文で作った関数ですね。相対位置に中心位置を足すことで、各ブロックの実際の位置を計算することができます。相対位置はリストになっていますので、またまたいつものパターンですね。リストをなめて「リスト」を生成するので、map関数の出番です。ちなみに前回の復習になりますが、リストをなめて「一つの値」に畳み込む場合はfoldr関数を使うのでした。getPosList関数をテストしてみましょう。z_Mino[(5,2),(4,2),(5,3),(6,3)]初期位置にあるI-ミノやZ−ミノの値から各ブロックの実際の位置を計算できました。これでようやくputMino関数が書けそうです。board左辺、右辺の最後の引数(board)が同じ場合は以下のように省略できます。mino)へ〜省略できるんだ〜というテクニックとして覚えても良いのですが、これは正しい説明ではありません。putMinoはMinoとBoardをもらって新しいBoardを返す関数、という考え方から、putMinoはMinoを与えると、「BoardをもらってBoardを返す関数」を返す関数であると解釈するテクニック(カリー化)です。右辺のputBlocksも第1引数、第2引数を与えると「BoardをもらってBoardを返す関数」を返します。それぞれ関数の型にカッコを足してみましょう。Board)となり、どちらも「関数を返す」関数なんだということがイメージしやすくなります。関数型プログラミングでは引数に関数を与えたり、今回のように戻り値として関数を返すといった、関数を値のように扱うことが日常的に行われます。それではputMino関数をテストしてみましょう。initBoard[[G,E,E,E,E,E,E,E,E,E,E,G],[G,E,E,E,E,E,E,E,E,E,E,G],[G,E,E,E,Z,Z,E,E,E,E,E,G],[G,E,E,E,E,Z,Z,E,E,E,E,G]・・・(省略)ghciの出力結果を整形して見やすく並べてみました。I-ミノは縦に並び、Z-ミノもZ型に並んでいるのが分かると思います。これからPart4で書いたコードを一部リファクタリングしたいと思います。main関数内に描画イベントハンドラをベタ〜っと書いてしまっているので長いんですよね。この部分を別関数に切り出してmain関数をすっきりしたいと思います。Part4のコードは以下の部分で固定ボード値を参照しており、このままでは初期状態のボードしか表示できません。(-—;)}この部分も直しましょう。引数でboardデータをもらうようにすれば良いですね。描画イベントハンドラをそのまま持ってきて新しい関数(updateWindow)に仕立て上げましょう。型はBoardをもらってイベントモナドを返す関数となります。(^^)/}(省略)main関数側は上記updateWindow関数を呼ぶようにします。引数boardにはZ-ミノを置いた後のボードを渡してみました。boardではghciからmain関数を呼び出してみましょう。Z型のテトリミノが表示されました(参考画像1)。テトリスっぽくなってきました。ふ〜無事に新しい関数を抜き出すことができました。リファクタリングをうまく行うコツは一度に行う作業を極力一つに絞ることです。コードを移動しつつ、変数を直して、処理も奇麗にして、インデントも直して・・・というのは失敗の元です。極端ですが、イベントハンドラのコードをそのまま別の場所にカット&ペーストしてコンパイルを試します。関数名がなくて怒られるので、そこで初めて関数名を与えます。その後で固定化していたinitBoardの替わりにboardを引数でもらうように修正します。まあ今回のようにリファクタリング工程が少なければいっぺんにやることも可能ですが、自分の中の作業モードを機能開発モードからリファクタリングモードに切り替えるために、あえて機械的な作業手順を踏んだりします。リファクタリング中は機能追加や偶然見つけたバグ修正をしないほうがいいですからね。次の目標はどうしましょう。この辺りから実装の結果がGUI画面に表示されるので楽しいです。う〜ん、よし、テトリミノの移動や回転を行う関数を考えてみることにしましょう。なると、操作を表す型があった方が良さそうです。1ならば左移動、2ならば右移動と数値パラメータでも実装できますが、やはりきちんと型を作るべきでしょう。(Show,Eq)Move型を作りました。値コンストラクタはDOWNからROTATE、それとNONEは移動しないことを表す値です。テトリミノを移動する関数を型から考えます。Minoこんな感じですね。次に実装を考えましょう。Z-ミノを右に一つ移動したい場合どうしましょうか。関数型プログラミングでは値を書き換えられませんので、Z−ミノの位置データを直接書き換えることはできません。したがって、パターンマッチ、またはレコード構文のgetPos関数を使って現在位置(x,y)を取り出し、xに1を足して、新しいZ-ミノを構築することになります。ghciでアイデアを練りましょう。[(0,0),(-1,0),(0,1),(1,1)]}元のミノの位置を取り出す。[(0,0),(-1,0),(0,1),(1,1)]}これで右に移動したmino’を作ることができました。最後の部分はもう少しだけ簡単に書けます。レコード構文で作られたアクセサを通して位置(getPos)の部分だけ新しい値を設定することができます。[(0,0),(-1,0),(0,1),(1,1)]}上記をふまえてmoveMino関数を実装してみます。minoテストしてみます。Prelude.undefinedDOWNもLEFTも問題ないようですね。ROTATEはundefinedで未実装にしているので当然エラーです。回転実装はテトリスの山場の一つです。テトリスの回転は少し癖があります。どのテトリミノも4回転して元の形状に戻りそうですが、そうではありません。四角形のO-Minoは1回転で元に戻ります(回転しないとも言えます)。I-Minoは2回転で元に戻ります。縦棒の状態から1回転すると横棒の状態になり、2回転すると元の縦棒の状態に戻るということです。Z−ミノもよく観察すると2回転で戻ります。L-ミノやT-ミノは4回転で戻ります。このことは各テトリミノの属性と見なせるので、Part5で作ったMino型の属性に加えたいと思います。今説明した各ミノの最大回転数と、現在の回転数を加えました。設定するように修正しましょう。T-ミノそして先ほど作ったmoveMinoのROTATE部分は現在の回転数を1増やすようにします。minoテストしてみます。現在の回転数が0から1に増えていますね。回転数を設定できるようになっただけです。実際にブロックを回転をするのはブロックをボードに置く際に行います。本日最初に作ったgetPosList関数を思い出してください。テトリミノの各ブロックの位置を計算してくれる関数でした。ここで回転数から回転済み位置を計算するように改造すればうまくいきそうですね。さあ、回転の山場を乗り越えましょう。回転行列やcos/sinを考えて回転方法を考えても良いですが、ブロックを時計回りに90°回転させるだけならば、紙でお絵描きすれば思いつくと思います。頭にグラフ用紙を思い描いてください。Y座標は今回のボードに合わせて上がマイナスです。(1,-2)にある点を(0,0)を中心に時計回り90°回転するとどこに移動するでしょうか。いうことは、(x,y)の点を回転すると(-y,x)に移動するということです。アイデアをghciで試してみましょう。i_shape'[(0,0),(1,0),(2,0),(-1,0)]元の縦棒(i_shape)をmap関数を使って回転させてみました。i_shape’は横棒になっていますね。今は1回転しただけですが、関数にする場合は今の処理を繰り返せるようにしましょう。繰り返して一つの値を求める・・・foldrの出番のようですが今回は再帰呼び出しで書こうと思います。foldrでも書いてみましたが、少し読みにくかったので。興味のある方は、練習を兼ねてfoldrで書いてみるのもいいと思います。いうわけでこんな関数になりました。xsではrotate関数をgetPosList関数に組み込みましょう。前述の通りテトリミノによって最大回転数が決まっていますので、回転数(r)は剰余(mod)で求める必要があります。rではここまでの成果を画面に出してみましょう。T-ミノをDOWN->DOWN->RIGHT->ROTATEした状態を表示してみます。main関数の描画イベント処理を以下のように書き換えます。moveMinoは2引数関数なのでfoldrで畳み込みしやすいですね。boardmain関数を呼び出して画面を表示するとT-ミノが回転していることがわかると思います。(参考画像2)う〜む、今年中にテトリスを完成させることはできませんでしたが、来年はゲーム状態の更新(IOモナド)、キーボードによる操作、ライン消し、タイマーによる落下、などを作ってテトリスを完成させたいと思います。来年もよろしくお願いいたします。