東京工業大学 ロボット技術研究会

東京工業大学の公認サークル「ロボット技術研究会」のブログです。 当サークルの日々の活動の様子を皆さんにお伝えしていきます。たくさんの人に気軽に読んでもらえると嬉しいです。
新歓特設ページ        ロボット技術研究会 HP        ロボット技術研究会 twitter公式アカウント

phi16

「ロボット技術研究会」通称「ロ技研」は、その名前の通りロボットの制作や研究はもとより、電子工作や機械工作、プログラミングなどの幅広い分野にわたるものつくり活動を行っています。

カテゴリ一覧: loading

Haskellで始める手続き型プログラミング

こんな煽りがあったので、書いてみることにしました。Haskellが好きなphi16です。この記事はrogy Advent Calendar 15日目の記事です。まぁポエムみたいなものです。

Haskellとは

純粋関数型言語、と良く言われます。いろんなプログラミング言語の中でも結構特殊で、例えば変数が無いとか、間違ったコードはすぐコンパイルエラーになるとかあります。でもHaskellは実はマルチパラダイムみたいなところがあるので手続き型のコードは普通に書けます。

Hello, World!

それはそうって感じ。「=」が気になるかもしれませんけどdoが{}みたいな意味なのでそんなものかなと。関数呼び出しに括弧が無いのはなんだかメッセージパッシングっぽいですね。まぁ括弧なんて要らないよね、慣習でついてるだけで。

FizzBuzz

ちょっと慣れない感じですね?1行目は特殊な便利構文をつかうよーっていう宣言です。GHC(コンパイラ)が許してくれればOKなんです。
forMは所謂for文に近いですが、最近の言語でよくあるRange-based forですね。つまり1から100までの値でforするっていうことです。その値を受け取る方法が $ \x -> です。for内部の手続きで使われる値としてxをつかうよーっていうことですね。
さてifですけど、なんだか条件がいっぱいあります。1行目に書いた命令によってifの条件文を|に続けていっぱい書けるようになったのです。うれしいね! `mod`は%と同じです、割り算のあまり。条件を満たすときはまた -> を使って続きの手続きを書きます。otherwiseはいままでの条件を満たさない場合用の文ですね、defaultみたいな。
後増えたのはprintですか。putStrLnは文字列を出力する命令で、printは出力できそうなものなら出力してくれる命令です。よくあるかんじ。

うんうんわかってきた。

入力と出力

大体の言語での手続きは引数と返り値をとれるようです。Haskellでもやってみましょう。 先程forMで使った $ \i -> の中でも \i -> の部分が再登場しています。要はこれは「引数を持つ手続き」をつくる機能なのです。
$」は実は、「右側全てを括弧で囲んだものとする」という命令です。これがないと手続きがどこで分断されるかわからないのでforMのときは指定する必要がありました。今回は定義なのでだいじょうぶです。
その代わりreturnには$がついています。これは(return i) * iと混乱するかもしれないからつけるのです。勿論括弧で書いてもいいですけど邪魔ですよね。
手続きに値を与えるのはputStrLnと同様にとなりに置けば良いです。そこから返り値を取り出すには <- を使うようです。実行してみると25と、確かにsquareの結果が取り出せていることがわかりますね。

手続きとは?

とりあえず今のところ、「do」が手続きを表していることがわかったとおもいます。いろんな言語で{}で手続きを表していたことと似たような感じです。ですがHaskellには他の言語にはない面白い性質があります : 手続きは代入できます。第一級関数ならぬ第一級手続きってところですね。今まで「=」だったのはそういう理由でした。

別に他の言語でも手続きは代入できる、って言う人はいるとおもいます。けどそれって「無引数関数」ですよね。それは本当にあなたの言語の「手続き」でしょうか。Objective-Cの^{}が一番表記的には近いかもしれませんね、でも結局それを実行する際にproc()と、関数適用の()が必要になります。
Haskellではそれは必要ありません。
確かに無いですね。これが正しい「第一級手続き」ではないでしょうか?手続きを最も気軽に扱えるのは本当はどんな言語なのでしょうね?

おわりと余談

いやまぁジョーク記事ですけど・・・。

私のせいもあるとおもうんですけど、Haskellが(ローカルな界隈だと)偏見で貶される or 怖がられることが多いので 別にHaskellやってほしいわけじゃないですけど普通の1言語として捉えてほしいなーという気持ちです。私はHaskellを「自由にコードを書ける」「バグを早期発見できる」という点で他の言語よりも優秀な言語だと思っていますけど勿論これは別の話です。

あと全てのプログラマに知っていて欲しいと思うこととして、「自分の言語でつらいことが他の言語で簡単にできることがある」というのがあります。C言語を使わないといけない場合でも、「自作言語のコードからC言語のソースを出力するプログラム」は作れるはずです。Syntax Sugarというのは案外言語の本質で、書きたい記法があるなら書けるようにすればよいのです。これをやりまくったのが現在のAltJS文化です。また自分の知っている言語には無いライブラリが他の言語にあることは多々あります。ライブラリを自分で書くより言語を勉強する方が速いことも多いです。目的を達成する方法はいっぱいあるはずなのです。手続き型が良いのか、関数型が良いのか、それとも・・・ 色々選択肢を持っておくことは大事だとおもいます。正しく言えば「自分には選択肢がある」ということを認識するのが大事だとおもいます。

私はこれからも幸せなコードの書き方を追求していきます。

以降、無駄な補足と解説

さすがにまるなげはだめかなっておもって。

間違ったコードはコンパイルエラー

間違ってることを勿論100%検出できるはずがありません。でも大体の言語で発生する実行時エラーは、基本的にはコンパイル時の「型チェック」までで検出できます。ついでにいえばHaskellで起きるエラーは大体型エラーです。型ってすごい。

実はマルチパラダイム

パラダイムってなんですかね。ある意味では「プログラムの読み書き作法」かな?と思います。確かにHaskellは関数型言語でCは手続き型だとおもいます。では「Haskelは手続き型ではない」のでしょうか?
最近の言語は大体自由にコードを書けるようになっているのでパラダイムの明確な区別ができずにマルチパラダイムと呼んでいる場合が多いみたいですね。Haskellではどれくらい自由にコードを書けるのでしょう。
Haskellでは日常的にdo文を使います。というか使わないとコードは書きにくいのです、確かにそれは純粋関数型の制約なのかもしれません。でもHaskellにはdo文があるのです。この「文法」が何をできるか、というのはまぁ数多くあるモナドの例を眺めてみればわかる通りだとおもいます。
大体do文ってもう見た目が手続き型じゃないですか? Haskellは「作法として」do文を使うことが多いです。これは手続き型なんじゃないですかね。

Hello, World!

大体の言語のHello, World!は実は中身ヤバいっていう話がよくあるように(ref:PHP による hello world 入門)、HaskellのHello, World!の全貌を正しく理解するのは勿論困難です。でもどんな言語でもそんなもんですよね。
ちょっと補足するとしたら、実は「1行だけのdo文」は「その文そのもの」と等価です。要は : これで十分です。勿論以降も同じですが、明示的なブロックの導入ということで毎回書いてます。ifで{}を省略できるのと理屈は同じです。
また、とても大切な事実として putStrLnの戻り値は、よくあるvoid型ではなくIO voidです。このIOがとても大事なのです : 副作用の存在を表す型です。

括弧なんて要らない

これができるのは「2引数関数」と「1引数を取って1引数関数を返す関数」を区別しないことによるものです。別に他の言語でもf(x)(y)はできるとおもうんですけどちょっと気持ち悪いですよね。Haskellだと括弧が無いのでこれに特に抵抗もなく、まぁ文化なのかなぁっていう感じです。実際いろいろ便利なんですよね。

特殊な便利構文

Haskellの言語仕様はきっちり定められていてHaskell2010という名前がついていますが、GHCはそれ以外にも拡張をいっぱい定義しています。使うときはコマンドからとかではなくソースに直接記述できるので他の言語よりも気楽に書けるわけです。
色々と世界が高度になりすぎたお陰で言語拡張が無いと困るようなケースが結構あるのです。でもどうせGHCしか使われないのでぽんぽん突っ込んでもOKみたいな感じ。言語仕様は実質GHCそのものみたいなところがある。

$演算子

見た目がわかりにくいんですよね・・・これ無い方が多分初学者には優しいんだとおもいます。
これは事実としてただの関数適用演算子です。左辺に右辺を適用する。ただ、優先順位が最低なのでa + b $ c + dとかやると(a+b) $ (c+d)になります。仕組みがわかる気がする。
さらに右結合なのでf $ g $ xとか書くとf $ (g x)、つまりf (g x)です。便利な気がするじゃないですか。
最近は逆向きの演算子として&が良く使われるようになってしまった(Lensのせい)のでx & g & fとかも増えましたね。まぁ演算子文化が賛否両論なのはめちゃくちゃわかるので何も言わないですけど、Haskellには強い検索ツールがありますし、ドキュメントちゃんとしてるし、型あるし、まぁ、ゆるしてほしい。

出力できそう

なんかObjectにtoStringを生やしている言語とか、<<があるならOKみたいな言語とか、いろいろありますけど。HaskellはShow型クラスのインスタンスを定義しておくと「出力できそう」なことになります。まぁ明確に定義されてるよってこと。
表示できないタイプだと実行時エラーになる言語とか、フォーマットエラーをガン無視する言語とは比べ物にならないですね。

無引数関数と手続き

まぁHaskellには無引数関数はないんですけど・・・ 一般的な言語で「無引数関数」と「手続き」を区別する必要がある理由は、「引数を取らないと手続きの中身を実行してしまうから」だとおもいます。Haskellでは適当に値を作ったとしてもそれが副作用付きだったら評価しません。遅延評価によるものではなく、「副作用付きの値」から値を取るにはdo文に乗せて「<- 」を書く必要があるからです。とはいえ仕組みは無引数関数と全く同じと考えていいとおもいます。
重要な差は、「do文の中で羅列された式は順番に()されていく」という規則なのです。要はdo文の中に書いたら勝手に()されるので、わざわざ引数をとるように記述する必要がないということです。そういうように設計されているのです。
これは手続き型言語、正確に言うと純粋世界と副作用を区別できない言語ではできないことだとおもいます(まぁ一貫性を無視すればできるんですよね・・・UFCSとかの・・・)。

まぁ全体を通して言いたいこととしては「do文はすごい」っていう話です、うん。

定義と代入

勿論違うものです。でも今回のコンテキストでは本質的な違いではないです。事実として「変数」に手続きを代入することはできますし、そこから取り出した手続きを実行するのに括弧はいりません。

Syntax Sugarは本質

do文はただのSyntax Sugarです。

つまりSyntax Sugarがあるだけで純粋関数型言語でも手続き型のようにコードを書けるということ。逆にいえばそういうSyntax Sugarが無いからC/C++はつらいんです。
ちなみに最近は多くの言語にこのdo文と本質的に等価な機能が導入されています : Generator。いっつもGeneratorの宣伝していますけど、これは本当にdo文みたいなものなので好きなパラダイムをGenerator上で展開できます。うれしい!Syntax Sugarとして提供されている場合と言語構造として定義されている場合があるようですけど、前者で十分とは言え大した差はないですね。
みんなもGenerator使って幸せなプログラムを書いていこう。

ねるねるねるねをつくった

こんにちわ。phi16です。このたび謎のゲームを公開したのでブログをかきます。
 02
Google Playにあるのでよかったらあそんでみてください~
 適当に製作話と質疑応答をかきます。
 

どうしてこうなった

最初のおもいつきはm年前(m>1)にn人(n<5)でどうでもいい話をしていたときに「"ね"と"る"には棒の有無の違いしかない」ということに気づいてしまったことでした。
当時暇だった私は家に帰ってから1日でゲームを作り上げました → 初代ねるねるねるね
見ての通りこの時には「ね」と「る」しかありません。シュールゲーのつもりでつくったものです。そのコンセプトは間違いなく達成できてるとおもいますが。
見どころ?としては 棒を刺した時に「る」が綺麗に「ね」に変形するところでしょうか。この辺のおかげでBezier曲線の操作をおぼえたりしました。
なんであれこれで終わったはずのものだったのです。

さらなる"失敗"は、「"ろ"と"る"には丸の有無の違いしかない」ということに気づいたことです。
あとはもうひもづる式というか。
結果的に「ねるわろれそゐゑうえ」まで揃っちゃいました。あとはつくるしかない。

ということでちゃんとしたゲームとしてUI・機構・グラフィック等の再設計をして、できあがったのがこちらです。
自分でもよくやったものだとおもいます。はい。
 

なんでこの文字?

最終的には「流れてくるのは10種のひらがな」になります。他にも候補はあったのですが諸々の理由で却下等されています。

まず、初代ねるねるからベースの考えは変わっておらず、「ねるねるねるね」を作るゲームです。
そういう意味で、「ね」「る」以外をつくる必要性はないのです。「え」の点とかは外したらもう付けられません。
すべてのひらがなからどうにかして「ね」「る」を作ることは考えましたが、明らかに(プレイが)大変すぎることが目に見えているのでやめました。 あくまでちゃんとしたゲームなのです。
勿論初代のようにちょっと要素が少ないかな、っていうのは残念なので ちょうどいいあたりとして10文字を選びました。

ゑの存在意義

このゲームのなかでなかなかにトリッキーな存在であるのが「ゑ」です。正確に言うと「ゑの下を取り外す機構」です。 
実は初期設計では全部の機構にふさわしいアニメーションをつけるつもりでした。「れ→わ」とかは下からぐいっと押し上げる、みたいな。
そうすると「ん部分をとりはずす」というのは「下の方をひっつかんでもぎとる」みたいな感じになります。が、それって「るの下部分」をひっつかんじゃうのでは、とか思ったわけです。そのほうが面白いかもとおもって「下の方全部取る」機構になりました。
これによって本来意味の無い「る→ろ」が実現できることになったわけです。そういう意味で当初の目的と外れているのです。

ですが、これによって今までは「連打でもどうにかなっていた」ゲームが、「連打すると文字がぶっ壊れてどうにもならない」ゲームになりました。やったね! 

おふせぼたん

せっかく?なので何か課金ボタン付けたいわけですよ。こんなシュールゲーに課金要素あるのおもしろいとおもうわけです。
何が良いかなって考えたら まぁ「文字」なわけです。ステージ解放。実装もしやすいしそこそこ意味があるし。 
さらに元々ゲームの順番として「ゐゑ→うえ」だったので、最終ステージを解放、としてちょうどいいかんじでした。
値段に考える余地はないですね。儲けたいわけじゃないので。ゲーム性のひとつです。おかねはほしいけどね。

先ほどの「ゑ」のおかげでそこまででもゲームが完結できますし。うんうん。
でも課金しないと全ての文字をつくることはできないのですね。うん。
これでよかったかなーって思ってます。いまのところ。

質疑応答

まぁ話としてはこれくらいで。よくある(ない)質問でも。

・iOS版は?

MacとAppleに貢ぐおかねをください。あと時間。

・なんでリリース遅れたの?

殆どは8月にほぼ完成していたのだけど 課金部分をつくる気力が湧かなくて・・・
結果的には工大祭Game^3で様々な改良要素を思いつけてよかったかなーという感じです。

・開発環境は?

初期はWin7・Eclipse・JavaでしたがPCを変えてWin8.1・AndroidStudio・Javaです。
AndroidStudioはLogもいい感じだしメモリもちゃんと出るしメソッドコール調べたりできるし良いですね。Eclipse使いこなしてないだけかもだけど。

・どうやって文字を変形してるの?

この記事のもふもふスライドを読んでください : JSでスライドを作るツールを作った話

・重いんだけど

最新版では一応軽くしたつもりではあるのですが・・・古いデバイスだと厳しいかもです・・・

・クラッシュした

クラッシュ時にレポートボタンあるのかなり無視されますよね・・・私もそうでした
開発者的にはクラッシュでレビュー付けられるよりもレポート送ってくれたほうが無限(鉞ポイント)に役立ちます。よろしくおねがいします。
Androidのレポートはスタックトレースまでちゃんと出してくれて優秀なのです。iOSを知ってるわけではないですが。

おわりに

是非あそんでください><
33
Google Play(再掲)

JSでスライドを作るツールを作った話

おひさしぶりです、phi16です。
今回第9回rogyゼミで「もふもふ」というタイトルで発表させていただきました。内容はゲームなどに用いられる「morph」についてです。
資料は ここ にあります。Enterで進めます。本来は画像や動画などがかなり入っていたのですが抜いてあります。
その時に作ってしまったJSでプレゼンもどきを動かすプログラムについて、なかなか放置するにはもったいないので解説等を書いておこうと思った次第です。
JSのプログラムではあるんですけど関数型的知識をかなり利用しているので多少理解が難しいかもしれません。
そのプログラムは ここ にあります。これを用いて書いた、上のスライドの内部コードが これ です。

何故作ってしまったのか

今回の発表ではまさに「morph : 変形」についての話で、ゲーム・アニメなどでの変形表現やモーション、動きなどについて実例とともに解説しました。
実例を出す方法として、自由に「動き」を表現するにはさすがに既存プレゼンツールでは厳しいということと、だからといって自由にプログラムを書いていては大変すぎるということで、JSでEDSLのような形式で記述し、そのままブラウザ上で表示できるようなものがいいと考え実際に実装した次第です。
結果的にはスライド自体は良いものができたと思っていますが、内部EDSLの表記がかなり残念になっているので設計的には失敗だと感じていますが、後述。

基礎

0. Style

まず、私のJSの書き方について。
このコードには、いわゆる「クラス」が存在しません。全てただのObjectとして管理しています。
が、それは何でも入っているというわけではなく、(人間によるチェックとして)全て同じ「型」を持ちます。
元々JSは関数が第一級オブジェクトなのでオブジェクトにクロージャとして生やせば簡単にオブジェクト指向ができます。
全く問題がありません。privateもpublicも自由に作れます。pに予め別オブジェクトを入れておけば継承もできます。
残念なのはブラウザのデバッガで型が表示されないくらいです。thisの闇にとらわれるよりマシです。
オブジェクト指向の本質はカプセル化だと思っています。そういう意味でこういう二次元座標はオブジェクト指向に「向いていない」のだと思ったりもしますが別の話です。
要は、オブジェクトとクロージャを全力で使っていくということです。

1. Color

簡単な例として内部で使っているColor「オブジェクト」について見ていきます。
これは色と色を補間できるようになっています。最初に「コンストラクタ」としてt=0の色、次に「第二コンストラクタ」としてt=1の色を与えるとtを受け取ってCSS的色を返すファンクタ(C++用語)を得ます。
関数だけですがちゃんとそれっぽく動作するのです。クロージャは神。

2. Easing

スライドの最初の方で言っているEasingですが、もちろんスライド自体でも大量に使われています。
Easing「オブジェクト」として利用しているのは、Ease.easingの戻り値です。これはEasingに利用する関数を渡すとそれに沿って動かしてくれるようなものです。
最初にtを渡すのはアニメーション部分との整合性のためです。
valはそのまま[0,1]の値、ファンクタとして呼び出すと線形補間を行います。
メンバのquadはEase-in Quadな補間関数です。これはEasingオブジェクトではありません。
reflect()は「その後に続く命令をIn-OutのInとして解釈」するEasingオブジェクトを返します。
最終行はEase-inOut Quintの[1,2]補間なので1.5というわけです。

設計としては、元々のEase-inな基本関数群を固定してEasingを加工したいという目的の為に、reflectやmirror、invなどの「新しいEasingオブジェクトを返すメンバ」を持たせて自由に関数を組み合わせられるようになっています。
特にIn-OutっていうのはInとInの180度回転をくっつけたものに他ならないので。わざわざ用意するよりも組み合わせとして書けるようにするあたり関数型思想っぽいですかね。
実装ですが、これは単純には実現できるものではありません。quintに渡す値はメンバ関数で好きにいじれるのですが、その結果はいじることができないので「片方を[0.0.5]にする」みたいなことがやりにくいです。つまりquintそのものを線形補間オブジェクトにする前にどうにかしてやる必要があります。
そこで内部的には「継続」を保持しています。分かる人ならもうわかるかもですが。
Ease.easingの第二引数として継続を取り、quintの結果は勝手に継続を通します。Lensにつながるものを感じる。
結果的にEasingオブジェクトのメンバ関数内で「基本関数を通す前」と「基本関数を通した後」に好きに関数を与えられるのでreflectも実装が簡単にできるというわけです。

また、連続的複数Easingのために、indexという関数を持っています。これは位置、全体量、1つに用いる幅を指定することで位置をずらした時にEasingもずれる便利なやつです。
これによって最初の「もふもふ → morph」やEasing一覧などのアニメーションが簡単に描けます。こういうコンビネータ的発想よい。

実装読んでわかるタイプの人はそのほうが速いですね。どうだかわかんないですけど。
関数型やるとわかるようになる気がします。

3. Graphics-1

ふつう、描画するときに座標を指定すると思うのです。
ですが、基本的にこのコードは描画時に座標の直指定を行いません。
その代わりに、「平行移動命令」が別で用意されています。その中で描画すると平行移動された状態になるのです。
同様に「回転命令」「拡大縮小命令」が用意されています。circleの半径は本質的には不要なのですが一応ある。
この方式の便利なところは、「全ての位置が相対的に表示される」ということです。何らかの絵を描く命令組み合わせを頑張って作ったとして、それをちょっとでも移動などしようと思ったら全ての命令の全ての座標を書き換えるしかありません。
この相対方式であれば、外側をg.translateで囲むだけです。
これによってプレゼンツールに「いつでも好きな場所のサムネを表示できる」みたいな機能を一瞬で実装できました。

ちなみに描画は全部HTML5 Canvasで出来ています。video要素を簡単に取り込めるのすごい楽だった。
g.translateはsave → translate → restoreするだけだし。かんたん。

4. Graphics-2

もう一つ面白い特徴として、「全ての描画部品は統一」されています。具体的には
を等しく持ちます。
これによってオブジェクトの色からの分離ができ、構築に時間が掛かるポリゴンデータなどを適切にキャッシュすることが容易になります。
clipではそのオブジェクトの形状でクリッピングを行い、外に描画しないようにできます。これもサムネ機能で使われています。
要は全部書き方がおなじなのです。

特に文字描画についてもそうで、普通はfill(brown)とするところ、stroke(1)(brown)とやれば幅1の線で枠がでます。
ちなみに文字描画はg.text("ぽよ").left.fill(brown)みたいな書き方をします。
原点の指定方法が9種類用意されていて、位置指定に役立つのです。個人的に便利。

なおHTML5 Canvasには文字をPathにする方法が存在しません。なのでopentype.jsでPathを取得して自分で曲線を書いています。
この時最大最小を計算できるのでleftUp~rightDownを適切にoffset付きで計算できるわけです。

アニメーション

1. 設計思想

まず、スライドの状態はfloat1個。
アニメーションを1つ増やすとその状態が1増える。その増え方はアニメーションにかかる時間に依存。
EDSLとしてはアニメーションのタイミングと描画を綺麗に共存できるのが目標。

2. 書き方

pがアニメーション全体の管理を行っているオブジェクトです。
p.in(duration,function(t){f})で、その場所に1イベントを発生させます。
p.inOut(d1,d2,function(t){f})で、その場所と、内部のfが完了した時点に1イベントずつ発生させます。
tは今いくら経ったのかを表すEasingオブジェクトです。durationはアニメーションする時間。
p.inOutのときは0 → 1 → 0のようにtが変化します。
p.outはただ単にtが[1,0]になるだけです。
p.inをネストさせることで連鎖的なアニメーションを書くことができます。
任意のタイミングでp.draw(f)を行ってGraphics系関数を呼ぶようにできます。

3. 内部処理

この書き方自体は非常に直感的だと思っています。ネストさえどうにかなれば。
が、直感的であるということはコンピュータにとって辛いということです。実際このままで目的機能を果たせるとは思えないですよね?
内部では結構面白い処理を行っています。具体的には関数を事前実行してイベントを全部調べるようになっています。

アニメーションに用いるpというオブジェクトは、実はRenderのコンストラクタの引数の関数の引数です。
つまり、本来はここに管理オブジェクトのpがくるはずだけど、そうでないpを入れることもできるということです。Visitorパターンに近いものです。
そして、前処理として、pに「イベント検出オブジェクト」を渡します。これはクロージャになっていて、p.in(d,f)が呼ばれるたびに内部のリストに必要アニメーション時間を記録していきます。
ついでにイベント数カウントも増やしたり、引数として与えるべきEasingオブジェクトの開始時間を記録します。
gはnullだけどp.draw内でしか呼べないようになっているのでだいじょうぶ。
そうすると全てのイベントのタイミングを把握できます。あとはこれに沿って唯一の状態floatを動かしていけばもはやdurationを無視しても良いのです。
こういう「コードの再解釈を行う」という意味でEDSLになっているのです。

実際に描画する際には前処理で検出したイベントタイムラインを平行して追跡して、
「inの引数関数の引数として渡すべきEasingオブジェクト」を追跡によって得られた「Easingオブジェクトの開始時間」にもとづいて計算し与えることで目的の動作を得ることができます。

ちなみに前処理のときにvideoやimageのロードも行うようになっています。通常実行の際は保存しといたDOMを読みだして操作できます。
あとinOutがなければ多分Easingオブジェクトの開始時間を保持する必要はなかったですね。本来はinOutをもっと使うと考えていたので作ったのですが結局全くつかってないです。後述。

4. 状態管理

このプレゼンツール、左/右キーで前と先に移動ができます。が、最初は右に移動できないです。
内部はブラウザの遷移と同じようにhistoryとfutureを記録してあって、進んだことでhistoryが増え、過去に戻れるようになっています。
またそれらは時間がかかるイベントですが、複数回要求があっても全てキューに保持してあるので適切に動作させることができます。
ちなみにキューに要素があるとイベントの速度が0.1秒になるので遷移が速くなります。戻るだけなのに毎回アニメーションついたら邪魔ですし。
その辺はsetTimeoutでいい感じに実装しました。やるだけなんだけど実装量が重いタイプ。

ちなみにこうなっている元の理由は状態をfloat1個じゃなくて分岐もできるようにしようと思っていたからで、過去に戻った状態でEnterですすめると未来が消えます。

その他

p.global

普通のゲームとかでもなんですが、位置を1px単位で調節したいというニーズはかなりあります。
実際スライド書いていて0.142とか0.045とかのいい感じの値を求めるのが大変でした。
そこで、「毎フレーム処理の評価をしなおしている」「ブラウザには便利なデバッグツールがついてる」ことから、デバッグツール内で変数書き換えできればいいなとおもいまして。
書き方は、決めたい定数値の部分にp.global(name,value)って書くだけです。
そうするとvalueを初期値としてnameの名前でObjectに登録されて、ブラウザからはrender.global(name)でアクセスできます。
実装は自明。

でも結局手で書きなおしたほうが慣れてるあたり速かったりしました。

p.configure

Canvasはデフォルトでは左上(0,0)で右下が(w,h)です。でもアニメーションとかするには中心が(0,0)、単位は1なのがありがたいです。
というわけで、それを常に(-r/2,-0.5)から(r/2,0.5)にしたかったのです。でもデフォルトにするには微妙なので自分で書いたほうがいい気がするのですね。
でもg.translateやg.scaleはp.draw内でしか許されていません。なのでアニメーションを描くたびに書きなおすことになります。つらい。
ということでつくったのがp.configureという機能で、
と書くことで、なんとそれ以降の描画実行がsave()の場所で行われるようになります。まぁまさしく求めていたものなのですが。見た目すごい綺麗じゃないですか。
限定継続っぽさを感じるけどちょっと違うのかな。まぁ。

実装には必見の価値があります。うん。

タイムラインの闇関数

下のバーにでてる円の位置を計算してくれる関数です。
クリックした場所を検出するために逆関数をつくるはめになりました :
もっと綺麗ななにかがあった気がしなくもないです・・・

失敗

タイムラインと描画を混ぜるコンセプトはいいとおもうんですけど実現方法が間違っていた気しかしないです。
inOutを使いたいシチュエーションはいっぱいあったけど諸々の制限で使えなかったり。
ネストを防ぐためにわざわざp.preserve.inというイベントを発生させない機能を追加したり。
これについては「スライド1枚」という単位を状態に入れておくべきだったかなといまは考えていますが。

ちなみに私的には「ネストは全く問題ない」派なのですが、今回のイベント管理は本質的にネストするべきではない記述なのでどうにかするべきだったという印象です。
問題は変数のスコープなんですよね。ただこの辺は考えてもあまり解決しないような気がしなくもないです。

あとこれは関係ない話なのですが、自分は完全に理解しているんですけど動作原理を書くのがすごく難しいですね。
関数の引数の関数の引数の関数の引数みたいなことをたまにやるのですけどちょっといろいろと厳しいものを感じます。
でもきっと関数とかクロージャと戯れていれば理解できるとおもうので がんばってください(丸投げ)。

まとめ

HTML5 Canvasを用いてアニメーションが書き放題なプレゼンツールを製作しました。ブラウザ上でものを作るのはデバッガツールが完璧に揃ってるのですごいやりやすいです。
設計的にはEasingやGraphicsなどの最終形にはかなり満足していて、その辺りの知見を得ることができました。
アニメーション管理についてはまだまだ至らない点もありますが、JS上でEDSLのような形式で諸々描けるという点は結構おもしろいとおもっています。
またコード内でクロージャを「乱用」している感じもありますが、むしろこういうのは積極的に使っていいと思っているので、もっと広まっていってほしいと考えています。

あと線形は、ダメです。

万物の創造

こんにちは。15のphi16とかいいます。はじめてrogyブログをかかさせていただきます。
これはrogy Advent Calendar 2015の12日目の記事です。11日目は@cyan_rej55さんでした。
テーマは†万物の創造†ということですが、宗教色は多分ないのです。たぶん。

はじめに

私は一応rogyのほうでゲーム制作とかしてます。謎のゲームつくったのでそろそろ公開したいです。
さて、私はゲーム作るのが好きなのですが、プログラミングにおける「関数型言語」というものも好きです。
そこで(元祖)手続き型にはあんまりない、関数型言語でよく用いられる概念について、文字通り1から解説・創造していきます。
長々とした読み物になってしまいましたが、読んで面白さとかを感じた方がいたら関数型言語をやるなり手続き型にそれらをつっこむなりしたらいいなと思っています。
対象はプログラマっぽくて数学好きっぽい人です。


1

何よりも最初に必要なのは1です。ですがこの1はいわゆる自然数ではありません。言うなれば「自明」と呼ぶようなものです。
そこにただ存在しているということ、はなかなか認識されているものではありません。具体的には : 
  • int main(){}
という関数をC/C++で書くことがあるかと思われますが、これは0引数の関数ではなく、「1を受け取る」関数とみなすことができるのです。1引数ではなく、1です。
実際これはHaskellではユニット型『()』と呼ばれます。これが全てのベースになります。
ちなみに1って呼ぶのは後述の直積の単位元だから(とか)です。

直積 A × B

集合の方でも出てくる直積という概念ですが、これは2つの物があったときにその両方を保持する物が存在することを表します。
つまり、いわゆるPair,Tuple(,Struct)です。戻り値のためにわざわざ構造体を作らなくて良いなど便利なのです。
個人的に必要だとおもうのはPairを生で生成可能なこと、つまりJavaのようにわざわざ自分でnewとかを書かなくていいことかなと思っています。仕方ないですけど。
C++のstd::tieみたいになるのも残念ですし、直積はもっと言語側でサポートしほしいなとか思います。

直和 A + B

この直和ですが、全くと言っていいほど認知されていません。
何かというと、直積の反対で、2つのうち片方のみを保持していることを表す型です。Eitherとかいいます。
Eitherの制限としての「Maybe, Optional, Nullable」の方は最近ようやく流行ってきてる気がします。
これらは「あるかないか」ですが、Eitherは「AかBか」を表現します。一般的ですね。
例えば「本来なら」直和として表現すべきなのに大抵そうなっていないものがあります : Boolです。
Boolは、まさしく1と1の直和として表現できます(つまり1+1=2です。)。これで値が-1なんかになることはなくなるわけです。
Eitherそのものは例えばエラー処理に使うことができます。正常な値を「右」で返して発生したエラーを「左」で保持、みたいなことをしたり。
うまくやれば例外処理機構そのものを「創り上げる」ことができます : C++のExpectとかまさにそんな感じで使われますね(未来的に)。Expectについてはこれが詳しいです。
ちなみに演算子の優先順位として A × B + C = (A × B) + C として解釈してください。

冪 B → A

型に対して冪(二項演算)を定義するというのも変かもですが、集合においてA^BがBからAへの写像の集合であったように、型の冪とは関数のことです。
集合の冪集合といえば2^Aですが、これはAから2 = Boolへの写像、つまりAの指示関数と同じものですね。
指示関数に具体的な値を渡すことでBoolが得られます。即ちBとB → AがあるときAを得ることができます。これが関数適用です。
といってもまぁ細かいことは気にせず関数 B → A を使っていこうかと思います。私は最近冪と関数の対応に感動しました。
関数は様々な解釈を生みます : 「導出」「抽出」「変換」「埋め込み」「割り当て」「計算」などでしょうか。この多様性が便利な所以でもあるのかもしれません。
実際これだけでも殆どを構築できます。直積直和どちらも関数のみで作ることもできますし。だいたい。
ですが更に世界を広げる為にもうちょっと基本概念を創ります。
あと優先順位としては A + B → C → D = (A + B) → (C → D) となります。最弱で右結合。

多相 F A

ちょっと話は変わりますが「一定の構造」を「任意の型」に適用したいニーズはかなりあります : template,Genericsなど。
これは「型をとって型を返すモノ」Fとして考えることができます。Fは型じゃないです。
これによって関数型ではよく出てくるリストを作ることができます。要素の型をAとしたリストLは
  • L A = 1 + A × L A
として表せます。直和の左は「空」、右は「要素とリスト」なので、「要素がない、または要素があってその後続も存在する」ものがリストと考えられるわけです。この構造はAの中身には依存していません。
例えば、リストという構造には一般的に「長さ」が定義され、これは要素の型に不変です。
つまりlength : L A → Intです。
多相型にしないとlengthを全ての型Aについて作ることになるわけです。これを防ぐためにC言語では暗黙の型キャストがあったのかなぁ、と思わなくもないですがダメです。
ちなみに今までの例もみんな多相型です。例えば直積は左・右の型に依存せず定まるので。

型クラス

先ほどの例では「任意の型」の適用でしたが、実際は「乗算のできる型」に対して適用したい、みたいなことがあると思います。
これを実現するのが型クラスで、C++ではconcept(無い)、Javaでいうinterfaceみたいなものです。ゆるいことを言えばオーバーロードっぽい感じです。
例えば「A × A → A」ができる型を「乗算のできる型」みたいに扱います。すると「全ての乗算可能型」について同名で掛け算を適用できるわけです。
Haskellでは「数値」「除算」「実数」「比較」「順序」「最大最小」「自然数対応」「表示」「読み込み」「畳み込み」などたくさんの型クラスがあり、演算を一般化しています。
例えば比較に関しては compare : A × A → 3 が必要とされており(ここで3はLessとEqualとGreaterの直和になる)、これを満たす型は x < y や x >= y などの比較演算ができるようになります。


具体的な構成

これで†万物の創造†への準備は整いました。実際に例を書いていきます。
細かく解説しようと思ったんですけど長くなりすぎるので列挙していくのです。

1. よくあるもの

  • Bool = 1 + 1 ― 二値論理
  • Nat = 1 + Nat ― 自然数 (ペアノの公理)
  • List A = 1 + A × List A ― Consリスト
  • Stream A = A × Stream A ― 無限リスト
  • Tree A = A + Tree A × Tree A ― 葉に値を持つ二分木

2. 計算 Arrow

通常の計算A → Bの「→」を一般化すると(二項演算としてみると)、P A Bです。ここで計算Pに求める制限は、
  • P A A (何もしないでそのまま出力する計算)
  • P A B × P B C → P A C (順序結合可能)
  • (A → B) → P A B (通常の計算を一般化する)
  • P A B → P (A × C) (B × C) (一部だけを計算させる)
というもので、これを満たすとArrowという型クラスになります。
  • P A B = A → B ―― 通常の計算(関数)
  • P A B = Stream A → Stream B ―― ストリーム処理
  • P A B = A × S → B × S ―― Sを状態としてもつ計算
  • P A B = A → B + E ―― 例外処理
  • P A B = A → B × P A B ―― オートマトン

3. 結果 Monad

これは完全に私の個人的思想なのですが、Arrowが入力についても考えるのに対しMonadというのは結果のみを保持するようなもの、と思っています。まぁ順序実行でいいんですけど。
そうすると入力がなくなって、値Aを表現するM Aとして表わせ、必要な法則(型クラス)は単純になって、
  • A → M A (ただの値を計算結果にする)
  • M A × (A → M B) → M B (前回の計算結果と、その値を使った計算をつなげる)
です。Arrowの特殊版なので実際にP A B = A → M Bと対応がとれます(KleisliArrow)。
  • M A = A ―― ただの値
  • M A = 1 + A ―― 失敗
  • M A = S → A × S ―― 状態
  • M A = List A ―― 非決定性計算
  • M A = (A → R) → R ―― CallBack, 継続

4. 環境 Comonad

あんまり理解していないので細かいことは言えませんが、Monadの「逆」としてP A B = W A → Bを考えます。
そうすると法則が逆転して、
  • W A → A (環境から抽出)
  • W A × (W A → B) → W B (環境を移し替える)
これだと結果を返すのではなく環境を受け取ることになるわけです。
  • W A = List A × Int ―― 現在地、近傍
  • W A = Tree A ―― 部分木畳み込み
  • W A = (S → A) × S ―― 環境更新

5. アクセス Lens

いわゆるオブジェクト指向でのGetter/Setterは、A → B(抽出), A → (B → A) (書き込んだ結果を返す)と書くことができます。
これを直積で合成するとA → B × (B → A)となり、軽量Lensと呼ばれます。これもこれでおもしろい。
そして、これをいじると(B → F B) → A → F Aが出てきます(ここでFはFunctorという型クラスに含まれる型)。 出てくるらしいです。
このときGetterとSetterはそれぞれ
  • (A → R) → B → R ―― Aでの読み出しをBでの読み出しに変換
  • (A → A) → B → B ―― Aの変換をBに書き込み
と考えることができ、それぞれが
  • Getter A = Const R A ―― 常にR
  • Setter A = Identity A ―― 常にA
というFunctorに対応します。もっとやっちゃうと
  • (A → F A') → B → F B' ―― Lens
となります。これは本当に様々な「アクセス」を一般化することができます。
  • F A = A ―― Setter
  • F A = R ―― Getter
  • Contravariant F, Applicative F ―― Fold, 畳み込み
  • Applicative F ―― Traversal, 走査
  • Functor F ―― Lens
さらに「→」を一般化してP A (F A') → P B (F B')とすると
  • Choice P, Applicative F ―― Prism, 部分的全単射
  • Profunctor P, Functor F ―― Iso, 同型
  • Forall P, Forall F ―― Equality, 等価
とまでなります。一般化された方は一般化する前のものとして使えます。つまり「同型」があるとそこから「Getter」「Setter」ができます。
説明的にはとりあえず様々な「アクセス」を一般化することができた、と考えればいいかなぐらいです。
これを使うと複素数で点P中心にθ回転する関数は subtracting P . _phase +~ θ で終わりです。すごい。
ちなみにsubtractingはIso、_phaseはLensです。
正直Lensを一番紹介したかった。 

6. 世界 IO

せっかく万物の話をしているので世界をつくりたいとおもいます。
Haskellで用いられている「世界」は単純に、RealWorldという型を状態として持つMonadとして扱われます。これがIOモナドです。
IO A = RealWorld → RealWorld × A ―― 世界。実際には細かい違いがある。
このRealWorldはまさに世界そのもので、これを使うことで任意の操作(システムコールとか)ができる、という「仮想的な対象」です。
Monad内部の値を取り出すにはMonad以外の何らかの方法が必要、という制約によってRealWorldの整合性が保たれるのです。たぶん。
というわけで、「世界」とはMonadにより閉じ込められた状態、ということになります。
ちなみになんと「世界を過去に書き換える」みたいなことができるみたいです。過去の書き換えに永遠に時間がかかるみたいですけどね :(
 

終わりに

解説ほとんど無い上に省略・適当な部分が多く申し訳ないです。間違えてる部分などあれば教えて下さい。
とりあえずは「1」から「世界」まで、いろいろなものが型で表せることがちょっとでもわかっていただければとおもいます。
C言語とかではハード的制限が強かった上に型は殆ど自明ですが、言語にきちんとした型をいれることでいろんなものができるようになるのです。
もっと関数と型を大事にしてあげてください、よろしくおねがいします。
あともっと面白い話を見たければ型理論とか圏論とかみるといいんだとおもいます。はい。
実際の実用っぽいです。おもしろい。 : dynamorphism
ギャラリー
  • 自作キーボードを作ろうver1.0
  • 自作キーボードを作ろうver1.0
  • 自作キーボードを作ろうver1.0
  • 自作キーボードを作ろうver1.0
  • 第14回ROBO-ONE Light 結果報告
  • ロボット技術研究会紹介
  • ロボット技術研究会紹介
  • rogy2016冬合宿 in 戸狩
  • rogyサバゲ:†革命†をもっと
記事検索
最新コメント