8つの質問で、Lazy K業界の現状を知る

Lazy系の会社の隆盛があって、仕様が定められたのが8年ぐらい前だろうか。 コンビネータ産業の人材動向が、今どうなってるかって?

大方の予想より凄惨ですよ。

それが分かる方法がある。Lazy K技術者に技術力を問う8つの質問によってだ。

Lazy K業界のエンジニアの平均レベルを知りたくって、いろんな会社さんのLazy K開発者(経験者)向けに以下のようなつ8の質問をし

その8つの質問というのはこんな問題だ。

Lazy K技術者に技術力を問う8の質問

  1. ラムダ抽象ではなくコンビネータで表現するメリットを一言で表してください。(筆記回答)
  2. 入出力の終端を表現する方法は何ですか?(筆記回答)
  3. チャーチエンコーディングとスコットエンコーディングの違いを端的に説明してください。(筆記回答)
  4. 任意の関数に対して不動点を求めるコードはどれですか?(選択回答)
    1. S I I (S (S (K S) K)) I
    2. S S K (S (K (S S (S (S S K)))) K)
    3. ((S (K S) K) (K (S I I))) ((S (K S) K) (K (S I I)))
    4. S I I (S (K (S S (S (S S K)))) K)
  5. Lazy Kのプログラムにおいて、いきなりスタックがあふれる不具合が発生しました。原因として何が考えられますか?(筆記回答)
  6. 与えられたタプルやリストに対してcarとcdrをそれぞれ適用するのはなぜNGなのかメカニズムを説明してください。
  7. 与えられたチャーチ数の階乗を求める関数をSとKとIの組み合わせで書いてください。
  8. JavaScriptでHTML要素をid属性の指定により取得するメソッドは何ですか?(筆記解答)

過去に実施した平均点

この8問について、私が出会ったエンジニアに解答してもらった平均正解数は、 なんと8点満点中SII(SII)である。

内訳的な話

うろ覚えになってしまってるだろうから満点は無理でも、ちゃんとLazy Kをやってるエンジニアさんであれば4問は解けるだろう。筆記解答については相当甘くつけるから尚更だ。例えば、1問目「コンビネータのメリット」は、「簡約が確実かつ簡単」ならニジュウマルだが、「変数名を考える必要がない」でも「シンプルに書ける」でもマルで、それらに類する説明ならちょっと分かりにくくても、なんでも正解にしてる。

スタックあふれの問題は、経験していなかったから知らないから何も言えなくてもまぁいいだろう。 (10年やっててもスタックオーバーフローの込み入ったトラブルを経験しないなんて運がいい人なのだろうか、悪い人なのだろうか) 「getElementById」なんてほとんど白紙解答。不動点コンビネータだって自分で簡約すればわかる話だが、そんな状況である。1、2日目ではなく3-10年生であり、SKIバリバリとの触れ込みなのだ。 設計やチームリードやコミュニケーションを得意としている人ではない。プログラマー対象なんだぜ。 (ウチの会社では平均8点ほどあった。これは基礎教育をやってれば当たり前の点数だろう)

これぞ現在のLazy K業界の実態

でも私は、ここ数年茨城でLazy Kの護符などの錬成をやっていた経験から納得がいく。

基礎教育なしに、1年目0円でラムダ計算→2年目コンビネータ論理→3年目ちょっと型理論 仕事と割り切った中で、フレームワークに乗っ取ったコードを書き続ける→その後10年経過 という流れのキャリアパスを通ってきた人にまったく出会っていない。

そもそも業界が存在しないのが現実ではないか。

淘汰という概念が存在しない業界

統計としては母数が少なすぎる(-∞桁程度)し、母集団を変えれば別の所感が得られるだろう。 しかしながら、ヤバいのは淘汰という概念がそもそもないことだ 特に東京は仕事量がないから、こういうレベルの人でも危機感なく生き残ってしまう。 このところ景気がいいせいもあって、正解数1問だった人もすぐに稼働現場が決まっていないようだ。 こういうエンジニアたちが大勢集まってLazy K開発をするのだ。

結果として、一部の出来る人には余計に負荷がかかるだろうなと思う。

いままで私は楽観主義者だから、

関数型言語は適用簡約産業だったけど、これからは抽象除去産業になっていく。 必然的にコンビネー汰の時代になり、優秀な簡約器が生き残るだろう。 と思っていた。

しかし現実は、もっと凄惨な世界を経て時代が進んでいくようだ。

元ネタ

8つの質問で、Java SI業界の現状を知る - 山本大@クロノスの日記

Freeモナドでゲームを作ろう!第3回: Haskellは手続き型言語

※最新のfree-gameを想定しているので、古いバージョンを使っている方は今すぐアップグレード(cabal update && cabal install free-game)してください。

7週間以上開きましたが、第3回です。ここからはどんどんプログラムを組み上げていきます。

コードが長くなったのでGitHubに上げました。重要な所に絞り、どのようにしてプログラムを組むかを解説していきたいと思います。

当たり判定について(Collision.hs)

説明が面倒なので省略。円、線分、およびそれらの集合同士がどこで衝突しているかを計算することができます。

Prelude Data.Vect Collision> Primitive (Circle 5 (V2 0 0))
 `collisions` Primitive (LineSegment (V2 5 0) (V2 (-7) 2)) :: Maybe (V2 Float)
Just (V2 (-2.0) 2.0)

世界を作る(Types.hs)

Types.hsでは、世界に存在する物体の型と形状を定義しています。フィールド名が_で始まるような代数的データ型を定義し、makeLensesするというのは前回と変わりませんね。

data Pack = Pack
    { _packPos :: V2 Float
    , _packVel :: V2 Float
    }

makeLenses ''Pack

packShape :: Getter Pack Fig
packShape = packPos . to (Primitive . Circle 15)

前回定義したものに加えて、Getterなるものが出てきていますね。これは文字通りGetterで、Lensの特殊な場合です。to :: (s -> a) -> Getter s aによって関数からGetterを作ることができます。

また、LensやGetterなどはすべて(.)で合成できます。

(.) :: Lens' a b -> Lens' b c -> Lens' a c

メインの処理(main.hs)

プログラムは世界を状態とするStateモナドで統一しています。mainの処理はこれだけ。

main :: IO (Maybe ())
main = runGame def $ evalStateT gameMain $ (世界の初期状態)

そして、本体であるgameMainも非常にシンプルなものです。

gameMain :: TheGame ()
gameMain = do
    addPack $ Pack (V2 320 360) (V2 4 (-3))
    forM_ [60,140,220] $ \y -> do
        addBlock $ Block (V2 60 y)
        addBlock $ Block (V2 160 y)
        addBlock $ Block (V2 480 y)
        addBlock $ Block (V2 580 y)
    forever $ do
        updateBar
        updatePacks
        updateBlocks
        tick

そして、lensをふんだんに活用してupdateBar, updatePack, updateBlocksを実装するだけですね。

さりげなくfree-game 0.3.2.3の新機能、loadBitmaps :: FilePath -> Q [Dec]も登場しています。指定したディレクトリ内の画像をすべて読み込んで定義してくれる関数です。

Lensの技

彼には72通りの型があるから、何て呼べばいいのか…

確か、最初に会ったときは…

zoom :: Monad m => Lens' s t -> StateT t m a -> StateT s m a

そう、指定されたフィールド(Lens' s t)を状態とするアクションを実行する関数だ。

まぁ…いい関数だったよ。

これを使うと、いちいち対象を指定するLensを書かなくて済むので非常に便利です。コンテナのすべての要素に作用するLensであるeachと組み合わせることで、foreachのような処理が簡潔に書けます。

updateBlocks :: TheGame ()
updateBlocks = zoom (blocks . each)
    $ use blockPos >>= (`translate` fromBitmap _block_png)

Lensは一見難解ですが、use, .=, each, toの使い方さえ覚えておけば、あとは手続き型言語の要領で分かりやすいコードを作ることができます。また、以下のような演算子も覚えておくと便利です。

(<~) :: MonadState s m => Lens' s t -> m t -> m () -- l <~ mはm >>= (l .=)と同じ。
(%=) :: MonadState s m => Lens' s t -> (t -> t) -> m () -- 指定した関数を使って更新する。Lens版modify

まとめ

f:id:fumiexcel:20130311220935p:plain

無事それっぽいものができました。勿論、これで終わりではありません!次回からは、ゲームの本質的な部分を記述していきます。

モナド変換子の速さを測ってみる

mtl(実装はtransformers)で提供されているモナド変換子のLazy版とStrict版で、どのくらいパフォーマンスの差が出るか調べてみた。また、私がCPS(継続渡しスタイル)で実装したものとも比較してみた。

比較方法

{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Reader.Class
import Control.Monad.Reader as R
import Control.Monad.Reader.CPS as RC

import Control.Monad.Writer.Lazy as WL
import Control.Monad.Writer.Strict as WS
import Control.Monad.Writer.CPS as WC
import Data.Monoid

import Control.Monad.State.Class
import Control.Monad.State.Lazy as SL
import Control.Monad.State.Strict as SS
import Control.Monad.State.CPS as SC

askTest :: MonadReader Int m => (m () -> Int -> t) -> t
askTest f = f (replicateM_ 10000000 ask) 42

tellTest :: MonadWriter (Sum Int) m => (m () -> t) -> t
tellTest f = f (forM_ [1..1000000] $ tell . Sum)

getTest :: MonadState Int m => (m () -> Int -> t) -> t
getTest f = f (replicateM_ 10000000 get) 42

putTest :: MonadState Int m => (m () -> Int -> t) -> t
putTest f = f (forM_ [1..10000000] put) 42

modifyTest :: MonadState Int m => (m () -> Int -> t) -> t
modifyTest f = f (replicateM_ 1000000 (modify succ)) 0

のそれぞれの関数で3回時間を測り、もっとも短かったものを結果とする。

結果

Target Lazy Strict CPS
askTest runReader 0.90s 0.84s
tellTest runWriter 1.56s 1.34s 1.14s
getTest runState 5.58s 0.70s 0.41s
putTest runState 5.54s 1.14s 0.81s
modifyTest runState 0.61s 0.39s 0.08s

やはり、StrictのほうがLazyよりも数段速い。だが、CPS版はさらに速い。継続の力、恐るべし…

まとめ

モナドはCPSで実装しよう。

ヘッズのための純粋関数型言語「Lazy SLYR」

ドーモ、変数スレイヤーです。

Brainf*ckめいた派生言語が作られる前に、ニンジャスレイヤーを基にしたニンジャヘッズのための言語「Lazy SLYR」を作ってみた。

Brainfuck派生ではない◆ ◆Lazy K派生でもない◆ ◆独自性重点◆ ◆純粋な◆

Hello, world!

イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエ!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエ!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエ!!!!!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエ!!!!!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエエ!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエ!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエ!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエエ!!!!!!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエエ!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエエ!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエ!!!!!!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエエエエエエエエ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエエエ!!!!イヤーッ!イヤーッ!イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!アバーッ!イヤーッ!イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!アイエ!

はじめに

関数の表記としてラムダ抽象を使う。xを引数にとりEを返す関数をλx. Eと表記する(λx y… z. Eはλx.λy.…λz. Eの略記)。gに対するfの適用をf gとする。

カラテ(文法)

  • イヤーッ![X][Y]…YにXを適用する。

コトダマ(組み込みの値)

  • グワーッ!…λx. x (λx y. x) (λx y z. x z (y z)) (λx y. x)に相当する。
    • イヤーッ!イヤーッ!グワーッ!グワーッ!グワーッ!でλx y. xになる。
    • イヤーッ!グワーッ!イヤーッ!グワーッ!グワーッ!でλx y z. x z (y z)になる。
  • アバーッ!…λx. xに相当する。
  • ゴウランガ!…不動点コンビネータ(後述)。
  • サヨナラ!…評価した瞬間に爆発四散してプログラムを終了する。
  • アイエエエ…エ!!!!…!…チャーチ数(後述)。

最低限「イヤーッ!」「グワーッ!」があればプログラムが作れるようになっているため、コードは肥大しがち。インガオホー!

チャーチエンコーディング・ジツ

アロンゾ・チャーチ=サンの発明したジツで、自然数とリストを以下のようにエンコードする。

  • 5 -> λsucc zero. succ (succ (succ (succ (succ zero))))
  • [A, B, C, D, E] -> λcons nil. cons A (cons B (cons C (cons D (cons E nil))))

Lazy SLYRではバイト列をチャーチエンコーディングされた自然数(チャーチ数)チャーチエンコーディングされたリストで表現し、プログラムは入力であるバイト列を受け取りバイト列を返す関数となる。なお、リストにスコットエンコーディングを使うLazy Kとは互換性がない。いいね?

アイエエエエ!!!!と叫ぶことで、(「エ」の数×10) + (「!」の数-1)を表すチャーチ数が作れる。

リカージョン・オン・フィクスド・ポイント

ゴウランガ!は任意の関数の不動点を求めるワザ。これによって再帰が表現できる。スゴイ!

  • イヤーッ!ゴウランガ![F] → イヤーッ![F]イヤーッ!ゴウランガ![F]

フィボナッチ数を列挙するプログラムではゴウランガ!が4つも使われている。

処理系

処理系(Haskell製)はGitHubにある。ゴウランガ!処理系のソースコードはなんと70行未満だ!

Windows向けバイナリはこちら

Freeモナドを使っている方、あるいはこれから使う方へ

FreeモナドのベースとなるFunctorは、GHCのDeriveFunctor拡張を使って、対象のデータ型にderiving Functorをつけることで

自 動 生 成

できます。

{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free

data CharIOBase a = GetCh (Char -> a) | PutCh Char a | EmbedIO (IO a) deriving Functor

type CharIO = Free CharIOBase

とすると、

https://github.com/ghc/ghc/blob/ghc-7.6.2-release/compiler/typecheck/TcGenDeriv.lhs#L1407 の規則に従い、

instance Functor CharIOBase where
    fmap f (GetCh g) = GetCh (f . g)
    fmap f (PutCh c a) = PutCh c (f a)
    fmap f (EmbedIO m) = EmbedIO (fmap f m)

のようなインスタンスが生まれます。

Freeモナドでゲームを作ろう!第2回: 本当に動かす

連載目次

Haskellの力を最大限に引き出す

前回紹介したような関数の組み合わせでももちろんゲームは作れるのですが、今回は、さらにゲームを開発しやすくするための手法をいくつか紹介し、その例を実際に動かしてみます。

Vec2

2Dゲームを作る上でベクトルの演算は必須です。vectというライブラリは、二次元から四次元までの範囲でベクトルや行列の操作ができる、非常に便利なライブラリです。二次元に絞って主要な関数をいくつか紹介しておきます。 Edward Kmettさんから「linearを使った方がいい」といったアドバイスを頂いたので、free-game 0.9ではlinearを採用しました。零次元から四次元までの範囲でベクトルや行列の操作ができる、非常に便利なライブラリです。二次元に絞って主要な関数をいくつか紹介しておきます。

V2 :: a -> a -> V2 a -- コンストラクタ

zero :: V2 a -- 零ベクトル
negated :: V2 a -> V2 a -- 反転
(^+^) :: V2 a -> V2 a -> V2 a -- 加算
(^-^) :: V2 a -> V2 a -> V2 a -- 減算
(*^) :: a -> V2 a -> V2 a -- スカラー倍
(^*) :: V2 a -> a -> V2 a -- スカラー倍

norm :: V2 a -> a -- ベクトルのノルム(長さ)
dot :: V2 a -> V2 a -> a -- ベクトルの内積
normalize :: V2 a -> V2 a -- 方向が同じの単位ベクトル

distance :: V2 a -> V2 a -> V2 a -- 二つのベクトル間の距離を求める

Stateモナド

ご存知の方も多いと思いますが、Stateモナドは状態を扱うためのモナドで、計算の任意のタイミングで「内部状態」を変更することができます。ゲームに状態はつきものなので、そういったプログラムと相性が良いです。

Lens

Lensはlensというライブラリで定義されている型で、これを使うことで、データ型の要素の一つに着目して操作することが可能となります。まさにレンズのように!

実際に見てもらった方が早いでしょう。

{-# LANGUAGE ImplicitParams, TemplateHaskell #-}
import Graphics.UI.FreeGame
import Control.Monad.State
import Control.Lens

data Hoge = Hoge {
    _xOfHoge :: Float
    ,_yOfHoge :: Float
    ,_dxOfHoge :: Float
    ,_dyOfHoge :: Float
    }

makeLenses ''Hoge -- TemplateHaskellを用いて対応するLensを自動生成(!)

main = do
    bmp <- loadBitmapFromFile "HaskellLogoStyPreview-1.png"
    let ?pic = bmp
    _ <- runGame def $ runStateT hoge $ Hoge 80 80 3.7 1.1
    return ()

hoge :: (?pic :: Bitmap) => StateT Hoge Game ()
hoge = do
    x <- use xOfHoge
    y <- use yOfHoge
    dx <- use dxOfHoge
    dy <- use dyOfHoge

    translate (V2 x y) (fromBitmap ?pic)

    xOfHoge += dx
    yOfHoge += dy

    tick
    hoge

Q.これは手続き型言語ですか?

A.はい、関数型言語です

使い方はとっても簡単。

  • フィールド名が_で始まるようなデータ型Tを定義します
  • makeLenses ''Tします
  • Tを状態とするStateモナドの中で、
  • useで値を取得します
  • .=で値を代入します

これだけで手続き型言語の便利さを取り入れた強力なプログラミングができるようになります!すごい!

さっきのコードをVec2を使った形に直し、さらに壁に反射するようにしてみましょう。(pack.pngはhttp://botis.org/shared/pack.pngからダウンロードしてください)

{-# LANGUAGE ImplicitParams, TemplateHaskell #-}
import Graphics.UI.FreeGame
import Control.Monad.State
import Control.Applicative
import System.Random
import Control.Lens

data Pack = Pack {
    _packPos :: V2 Float
    ,_packVel :: V2 Float
    }

makeLenses ''Pack

main = do
    bmp <- loadBitmapFromFile "pack.png"
    let ?picPack = bmp
        ?packSize = 30
    pack <- Pack <$> (V2 <$> randomRIO (?packSize, 640 - ?packSize) <*> randomRIO (?packSize, 480 - ?packSize))
                 <*> ((^*4) <$> unitV2 <$> randomRIO (0, 2*pi))

    runGame def $ runStateT updatePack pack
    return ()

updatePack :: (?picPack :: Bitmap, ?packSize :: Float) => StateT Pack Game ()
updatePack = do
    pos@(V2 x y) <- use packPos
    vel <- use packVel

    let collisions = [V2 0 y | x < ?packSize]
            ++ [V2 640 y | x > 640 - ?packSize]
            ++ [V2 x 0 | y < ?packSize]
            ++ [V2 x 480 | y > 480 - ?packSize]

    translate (V2 240 240) (fromBitmap ?picPack)
    
    packVel += sum [2 *^ n
        | t <- collisions
        ,let u = normalize (t ^-^ pos)
        ,let n = dot u vel *^ negated u]

    use packVel >>= (packPos +=)

さて、壁で反射する物体を見て、あるゲームを想像した方も多いと思いますが、この連載ではそれとは一味違うゲームを作っていきます。続きはまた次回。

まとめ

  • linearは超便利
  • lensは超便利

Freeモナドでゲームを作ろう!第1回: Gameモナドの基本

連載目次

free-gameを使う

さて、みなさん、free-gameはインストールしましたか?まだの方も、もうインストールした方も、本日free-gameを更新したのでcabal update && cabal install free-gameしましょう。 2013/5/6: free-game 0.9.3をリリースし、内容も改訂しました。必ずcabal update && cabal install free-gameしてください。

free-gameは、Gameモナドという独自のモナドによってGUIの抽象化を実現しています。GameはFreeモナドによって生成されたモナドであり、元は単なるFunctorです(これが何を意味するかは、連載の中で明らかにしていきます)。

今回は、実際にプログラムを組みながら、どのようにしてfree-gameを扱うかを学んでいきましょう。

Hello, world!

まずはHello, world!ですね。最初に、https://github.com/fumieval/free-game/blob/master/examples/VL-PGothic-Regular.ttf からフォントをダウンロードしましょう。

helloworld.hs:

{-# LANGUAGE ImplicitParams, OverloadedStrings #-}

import Graphics.UI.FreeGame

main = do
    font <- loadFont "VL-PGothic-Regular.ttf"
    let ?font = font
    runGame def mainLoop

mainLoop :: (?font :: Font) => Game a
mainLoop = do
    translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" 
    tick
    mainLoop
$ ghc helloworld.hs
$ ./helloworld (Windowsの場合は単にhelloworld.exe)

このように表示されれば成功です。FreeType Errorが表示された場合、カレントディレクトリにダウンロードしたフォントがあるかどうかもう一度確認してください。

f:id:fumiexcel:20130110163026p:plain

このプログラムの肝は、Gameモナドを実行するrunGame :: GUIParam -> Game a -> IO (Maybe a)と、Picture型の値を画面に表示する関数、drawPicture :: Picture -> Game ()です。画面を更新するアクション、tick :: Game ()です。

Picture型の定義を見てみましょう。

*** この型は削除されました
data Picture
    -- ビットマップ(後述)をPictureとして使う
    = BitmapPicture Bitmap
    -- 複数のPictureをまとめて一つのPictureにする。
    | Pictures [Picture]
    -- IOモナドに入ったPicture(内部実装のためのもの)。
    | IOPicture (IO Picture)
    -- Pictureを回転する。
    | Rotate Float Picture
    -- Pictureを拡大・縮小する。
    | Scale Vec2 Picture
    -- Pictureを移動する。
    | Translate Vec2 Picture
    -- Pictureに色を付ける。
    | Colored Color Picture

実はこれはglossにインスパイアされたものです。わかりやすいでしょう?*1

free-game 0.9では、Picture2DおよびFigure2Dという型クラスが定義されています。Gameモナドはこれらのインスタンスであるため、以下のような操作が利用できます。

class Picture2D p where
    -- ビットマップから生成する
    fromBitmap :: Bitmap -> p ()
    -- 指定した角度だけ回転させる
    rotate :: Float -> p a -> p a
    -- 拡大・縮小する
    scale :: V2 Float -> p a -> p a
    -- 平行移動する
    translate :: V2 Float -> p a -> p a
    -- 色を付ける
    colored :: Color -> p a -> p a

class Picture2D p => Figure2D p where
    -- 線
    line :: [V2 Float] -> p ()
    -- 多角形
    polygon :: [V2 Float] -> p ()
    -- 多角形(枠のみ)
    polygonOutline :: [V2 Float] -> p ()
    -- 円
    circle :: Float -> p ()
    -- 円(枠のみ) 
    circleOutline :: Float -> p ()
    -- 太さを設定
    thickness :: Float -> p a -> p a

V2は、vectというライブラリで定義されている、二次元のベクトルを表すデータ型です。が、具体的な使い方はまた次回。

ビットマップはloadBitmapFromFile :: FilePath -> IO Bitmapで読み込むことができます。http://www.haskell.org/wikiupload/4/4a/HaskellLogoStyPreview-1.pngを表示してみましょう(カレントディレクトリに保存してください)。当然ながら、loadBitmapFromFileはGameモナドの中では実行できないので注意してください。Gameモナドの中でIOアクションを使いたいときは、embedIO :: IO a -> Game aを適用しましょう。

{-# LANGUAGE ImplicitParams #-}

import Graphics.UI.FreeGame
import System.Random

main = do
    font <- loadFont "VL-PGothic-Regular.ttf"
    bmp <- loadBitmapFromFile "HaskellLogoStyPreview-1.png"
    let ?font = font
    let ?bmp = bmp
    runGame def mainLoop

mainLoop :: (?bmp :: Bitmap, ?font :: Font) => Game a
mainLoop = do
    translate (V2 40 240) $ colored black $ text ?font 30 "Hello, Free World!" 
    translate (V2 240 480)
        $ rotate 45 -- 反時計回りに45°回転
        $ colored red -- 赤色
        $ text ?font 70 "真っ赤な誓いいいいいいいいいい"

    r <- randomness (-40, 40)
    translate (V2 (-40) (360 + r)) -- ランダムに振動させる
        $ scale (V2 0.7 1) -- x方向に0.7倍
        $ colored blue -- 青色
        $ text ?font 100 (replicate 20 'ド')
    translate (V2 300 300) $ colored magenta $ circle 40 -- 円を表示
    translate (V2 240 80) $ fromBitmap ?bmp -- 読み込んだビットマップを(240,80)に表示
    tick
    mainLoop

f:id:fumiexcel:20130110195629p:plain

入力を行う

これだけでは、ゲームを表現するのに不十分です――「入力」ができないからです。free-gameでは、入力を行うためにKeyboardとMouseという二つのインターフェイスを提供しています。Gameモナドはこれらのインスタンスであるため、直に以下のアクションを実行できます。

-- | The class of types that can handle inputs of the keyboard.
class Keyboard t where
    keyChar :: Char -> t Bool
    keySpecial :: SpecialKey -> t Bool

-- | The class of types that can handle inputs of the mouse.
class Mouse t where
    mousePosition :: t (V2 Float)
    mouseWheel :: t Int
    mouseButtonL :: t Bool
    mouseButtonM :: t Bool
    mouseButtonR :: t Bool

引数に指定するキーについては、http://hackage.haskell.org/packages/archive/free-game/0.9.1/doc/html/Graphics-UI-FreeGame-Base.html#t:SpecialKeyを参照してください。

例: Zキーを押している間だけカウントアップを行う。Escキーを押すとプログラムが終了する

{-# LANGUAGE ImplicitParams #-}

import Graphics.UI.FreeGame
import System.Random
import Control.Monad

main = do
    font <- loadFont "VL-PGothic-Regular.ttf"
    let ?font = font
    runGame def (mainLoop 0)

mainLoop n = do
    translate (V2 40 120) $ colored green $ text ?font 20 $ show n

    translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" 

    key <- keySpecial KeyEsc -- Escキーの状態を取得
    when key quit -- Trueならば終了
    
    key <- keyChar 'Z' -- Zキーの状態を取得
    tick
    mainLoop $ if key then succ n else n -- Trueのときだけ1増やす

mousePosition, mouseWheel, mouseButtonL, mouseButtonR, mouseButtonMを使うことで、位置、ホイール、ボタンの状態が取得できます。

例: カウンタがマウスカーソルの右上に表示され、左クリックするたびに値が1ずつ増える

{-# LANGUAGE ImplicitParams #-}

import Graphics.UI.FreeGame
import System.Random
import Control.Monad

main = do
    font <- loadFont "VL-PGothic-Regular.ttf"
    let ?font = font
    runGame def $ mainLoop (0, False)

mainLoop (n, btn) = do
    pos <- mousePosition
    b <- mouseButtonL
    translate pos $ colored green $ text ?font 20 $ show n

    translate (V2 40 240) $ colored black $ text ?font 20 "Hello, Free World!" 

    tick
    mainLoop (if not btn && b then n + 1 else n, b)

drawPicture、askInput、getMouseStateの使い方が分かりましたね。実はfree-gameの基本はこれで全部なのです

ゲームを開発する準備は整いました。次回は、これらを踏まえて実際にゲームを作ってみましょう!

まとめ

  • 基本はdrawPicture、getButtonState、getMouseStateの3つだけ
  • embedIOがあるからIOだって使えるぞ
  • runSimpleですべてが動きだす

Tips

今回の例では、GHC拡張のImplicitParamsを使っています。?のついた変数は、一旦束縛すればスコープ内で呼び出した関数からどこでも参照できるようになるので、フォントや画像データなどの不変なものを扱うのに適しています。

*1:生意気にもnice data typeを標榜するだけある