JavaScript+Hyperappで心理学実験プログラムを作った

これです(github)。

僕の学科では学生が実験に参加しつつその実験についてレポートを書くという授業が多く行われている。そこで行う実験は古典的なものが多いのだが、大昔に書かれた実験プログラムがメンテナンスされずに使われ続けているということがある。

今回書いた記憶実験のプログラムもそうで、誰がいつどうやって作ったかわからないexeファイルが受け継がれ使われ続けてきた。しかし内部の処理がわからないので細かい仕様がわからず、いろいろ試してみて推測することしかできない状態だった。さらにいつ動かなくなるかもわからない。

そこでこのプログラムを移植(ソースコードがないので移植と言うかは微妙だが)しようという話になった。当初はPythonが候補に上がっていたが、多数の学生に授業中に同時に行わせるにあたり、Pythonをexeに固めたものは容量が大きくなりすぎダウンロードが長引く懸念があった(経験的には数十MB)。そこでJavaScriptでの制作を提案した。サンプルを作って披露したところゴーサインが出たので制作した。

動作は単純で

  1. x桁のランダム数列を3秒間表示
  2. 数列を消し5秒間待つ
  3. 参加者が数列を思い出して回答する
  4. 正誤を3秒間表示
  5. 桁数を変更し、1に戻る

というものだ。5の桁数変更のアルゴリズムには上下法や恒常法などがある。

上下法は正解すれば次は1段階難しく、不正解なら次は1段階簡単にするというアルゴリズムだ。これによって参加者の正解できる限界の桁数に収束する。メリットは試行数が少なく済むこと。

恒常法は一定の範囲の桁数をランダムな順番で課し、桁数を横軸に、正解率を縦軸にプロットすることで正解率を桁数を結びつける関数を得る。そして正解率50%(任意の値)に相当する桁数をその人の限界とする。メリットは関数そのものが得られるため多様な分析が可能であること。

心理学の実験は基本的にはwebではやらない。それは参加者の環境が統一できないからだ。MATLAB+Psychtoolboxが主流で、Python+Psychopy(pygame)もある。しかしポータビリティは低い。GUIを使うプログラムはDockerで動かすのは不便で、できてもパフォーマンスに不安がある。

そこまで環境にこだわらない実験や、今回のように同じ部屋で多くのPCで実験を行うときにはポータビリティの高いwebプログラミングによる実験は便利だ。そもそもGUIを扱うノウハウやライブラリという点ではJavaScript、というよりブラウザという環境が優れている。

Hyperappでhtml要素の真偽属性を操作する

※この記事は『金麦 RICH MALT』を飲みながら書かれた。

hyperappのviewはh関数で作る。これは公式サイトのサンプルだ。

const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, state.count),
    h("button", { onclick: () => actions.down(1) }, "-"),
    h("button", { onclick: () => actions.up(1) }, "+")
  ])

h関数は3つの引数をとる。1つ目はタグ名、2つ目は属性を列挙したハッシュ、3つ目は子要素だ。ここで属性はハッシュなので、キーとバリューのペアが必要だ。しかし全ての属性がキーとバリューのセットで用いられるわけではない。たとえばreadonlyは値をセットする必要がなく、存在することによって機能する。

ここでHTML5の真偽属性の仕様を確認する。

checkedおよびdisabledとなるチェックボックスの例を示す。checkedおよびdisabled属性は真偽属性である。

<label><input type=checkbox checked name=cheese disabled> Cheese</label>

これは次に書かれるものと等価であるべきである:

<label><input type=checkbox checked=checked name=cheese disabled=disabled> Cheese</label>

スタイルを混在させることもできる。以下は依然として等価である:

<label><input type='checkbox' checked name=cheese disabled=""> Cheese</label>

どういうことかというと、以下の3つはどれでもいい(参考)。

  • disabled
  • disabled=disabled
  • disabled=””

つまり真偽属性を変化させたいときは以下のように書きわけることができる。

h("input", {readonly=""}, "")
h("input", {not-readonly=""}, "")

これをhyperappの枠組みで考えるとこうなる。

const state = {
  readonly: "readonly"
}

const actions = {
  writable: () => state => ({ readonly: "not-readonly" })
}

const view = state => h("input", { [state.readonly]: "" }, "")

viewのh関数の第2引数のハッシュで、ブラケット記法を用いてキーの方を変更する。バリューはなんでもいい。

Mastodonに自動ハッシュタグ機能をつけた

アニメ実況時は同じハッシュタグで継続的にツイートする。よってハッシュタグを保持し自動的に付加し続けてくれる機能は便利だ。作った。

やたらとトゥートの反映が遅いのはサーバーがイマイチだからかな…

この機能追加はReact+Reduxの処理階層を知っていれば全然難しくない。まずハッシュタグ入力欄と、そこへの入力を処理するaction, reducer, stateを作る(全部compose-formのコピー)。さらにCtrl+Enterによるトゥートを処理するhandleKeyDownにShift+Enterによって発火する別ルートのトゥート投稿処理を追加する。これもトゥートボタンによって発火する処理をほとんどコピー。そのルートの最後にあるのがこれ。

const status = getState().getIn(['compose', 'text'], '') + ' ' + getState().getIn(['compose', 'preservedHashtag']);

これだけが実質的に意味のあるコードだ。興味のある方はコミットログをどうぞ。

1年前のドワンゴインターンではJavaScriptが全然わからず(わからなかったのはJavaScriptだけではないが)チームメンバーに教えてもらっていたが、今では自力で触れるようになった。

over the touge

※この記事は『福島産 あかつき桃直搾り』を飲んで書かれた。

昨夜から今朝にかけて徹夜したので今日は絶対大学に行くまいと思っていたが、いろいろあって行った。今取り組んでる作業はあとちょっと直すだけ。

先週の土日は大学に来ていたので今週はゆっくり休みたい。そういえば台風が来るらしい。休日に家にいるぶんには、天気は荒れてくれたほうがなんとなくワクワクする。

Hyperappでストップウォッチを作りたかったのでググったらこれがヒットしたんだが、app関数にeventsなる引数を渡している。こんなのあったかなと思ったらこのページの公開版では変わっていて

const main = app(state, actions, view, document.body)
setInterval(main.tick, 1000)

となっている。appの戻り値を利用するのはappのactionsを外部から呼び出す正しい方法だ。公式サイトにも書かれている

よく知らないが、昔はHyperappにそういう仕様があったのだろうか。

ニコニコするためにニコニコしてるのでニコニコできないアレはアレしようというアレを作った

niconico-nonpolitical

正味30分くらいで作った。目的と機能は日記タイトルの通り。インターネットにおいて見たくないものを見ないことは重要だ。これが上手くできないと人類は絶滅する。HTMLCollectionを配列に変換して使う(ES2015+)これやろうと思ったんだけどunexpected tokenとか言われてできなかった。なんでだろう。代わりに集合を配列に変換する方法をやった。jQuery使わなかったのはなんとなく、意地で。

今日は3人分実験をした。データは必ずしも予想通りではないが、概ね意図した通りの実験操作は行えているようだ。アドバイスをもらって改善点もはっきりした。調子は悪くない。

未来のミライをもう一度見に行きたい。あと野菜を摂取したい。

be caught in the rain

毎週土曜はコインランドリー、そして日記もいつもコインランドリーのことを書いていてマンネリを感じないでもない。

今日はコインランドリーから帰るときに雨に降られた。といってもコインランドリーは自宅から290m徒歩3分なのでどうということはない。洗濯物も軽く乾燥して自宅で干すので多少濡れても問題ない。

雨に降られて道を走っていると、自分がその場所にいるということを強く感じる。考え事をしながらだとかツイッターを見ながら道を歩いていてもそういうことは感じないのだが、雨に降られていると早く家に帰りたいとか木の下を走ろうとか考えるので、場所や空間のことを強く意識するのだと思う。

僕はときおり、道に寝転がりたいと思う。夏の夜にコンクリートはひんやりして気持ちよさそうだと思ったり、コンクリートはどういう触感なのか気になったり、はたまた単に疲れていたり、いろいろな理由がある。しかし実際にやったことはない。小学生のころは平気で道に座って寝転がったりしただろうが、大学生になってからはない。なんどか本気で試みたのだが、ついにできなかった。誰かが見ているかもしれないとか、道は公共の場所なのでそんなことをすべきでないとか、いろいろと理由をつけてはいるが、事実として僕は道に寝転がることができない。

子供の頃は自分はなんでもわかっているつもりだったし、自分を子供扱いする大人に苛立ったりもした。そして大人になればもっと自由になれると思っていた。しかし法律的・身体的には間違いなく大人になった今、僕は「子供はものをわかっていない」と思っているし、道に寝転がる自由を失った。

ところでHyperappで2つのactionを連鎖的に呼び出したいとき、具体的にはinputへの入力をstate.onePropertyに反映しつつ、さらにその新しいstate.onePropertyに基づいてstate.anotherPropertyを再計算したいとき、どういう風に書くのが美しいのだろうか。

現状こんな風に書いているが、ひとつの関数で2つの作業をしており、美しくない。2つめ(4行目)の操作を他の場所でも使いたいときに、2つの作業を別々の関数にしておいたほうが便利だ。そう素朴に考えたのだが、

これは上手くいかない。値を入力しても即座に元の値に戻ってしまう。多分Hyperappから呼ばれるupdateにはstateが渡されるが、updateから呼ばれるupdateInputやreCalculateにはstateが渡されていない。

こうすると望み通りの動作をするが、actionsの中に形式の違う関数が混ざっていていいのかどうか疑問がある。別の場所にhelperみたいなオブジェクトを作ってそこにしまっておきたい気がする。

Hyperappは書きやすい。楽だ。しかし「どう書くべきか」というノウハウが見つからない。自由は苦痛だ。自由を捨てよ。

二日酔い回避

昨夜は相当飲んで帰宅したのだが、大量に水を飲んで寝たら二日酔いにならずに済んだ。化学には詳しくないが肝臓でアルコールを分解する化学反応のために水が必要らしい。「うおォン 俺はまるで人間火力発電所だ」ではないが、人間の体はまさに化学プラントである。

午後からゆっくり活動開始して、洗濯→松屋の回鍋肉定食→乾燥→サザエ視聴。行きつけのコインランドリーにはジャンプが置いてあったりなかったりするのだが、今日は偶然新しいジャンプが置いてあって運が良かった。ソーマがそろそろ終わりそうで驚いた。

以前からすこしずつ作っていた視覚実験用のツールがとりあえず完成した。視覚実験では呈示する刺激図形の大きさは現実空間におけるサイズではなく網膜に占めるサイズ(視角)で決める。つまりディスプレイが遠ければ刺激図形は大きくしなければならないし、逆もしかり。このツールではディスプレイサイズ、解像度、そして刺激図形の視角を入力することで、実験プログラムを組むときに刺激のサイズを何ピクセルで指定すればいいか計算してくれる。ロジックは単純で割合とtanの計算をしているだけである。

Hyperappはとても便利だった。ユーザーが入力するフォームと、その計算結果が表示されるフォームの色分けをどうしようか考えている。パッと見ただけでわかるようなデザインにしたい。

ダメな日

今日はダメな日でした。なので大学に行かずに自宅でJavaScriptをしてました。JavaScriptは全然わかんないんだけど、たぶん1年後から死ぬほど使うのでちょくちょく触るようにしている。hyperapp以外何もインポートしていないので、いろいろ作りつつそのうちhyperappの中身さえ読めば完全に理解したことになる(?)。

ここ数日あまり人生をしていない。世界から疎外されている気がする。

人生というのは今日と明日と明後日と…からなる。すなわち人生は無数の今日によって構成されているので、今日を生きることは人生そのものである。にもかかわらず人は今日と人生を別のもののように考えてしまう。

書くことない。

0.75春+0.25夏

※この記事はチューハイ気分(レモン)を飲みながら書かれた

僕の住居は家賃が(場所に比して)安い代わりに人権が少し減っている。隣人の炊飯器のアラームが聞こえるし、地震でもないのに揺れる。そして洗濯機置き場がない。だから僕は洗濯物をバッグに詰めて近所のコインランドリーに通う。コインランドリーに洗濯物を放り込み、近所の飲食店で適当に食事をして、コインランドリーに戻って8分間乾燥、自宅に戻って干すというのが土曜か日曜のルーチンとなっている。飲食店はこれまでは家系ラーメンが多かったが、豆板醤じゃなくて辛醤が置いてあることやラー油が置いてないことが気に入らないので最近は足が遠のきつつある。

飲食店に行って戻る間僕は近所を散歩することになるのだが、週に一度という頻度は絶妙で、季節が少しずつ変化していくのが認識できる。今日の季節はタイトルの通り0.75春+0.25夏だ。基本的には爽やかな風が半袖に気持ちいいのだが少し日差しを長く浴びていると暑いという感覚も生まれてくる。

次の休日にはおそらく暑いと感じる頻度がもっと増えているだろう。「夏だなあ」なんてことを考えているうちに夏も終わり、秋も冬も終わり、僕の人生も終わるだろう。

JavaScriptに慣れるため、Hyperappでちょっとしたカスタム検索を手作りしている。なんにしても自分の手でゼロから作ってみるというのが大事だ。Reactもいいのだろうがちょっと環境が複雑過ぎる。Hyperappも完全に理解しているわけではないが、ブラックボックスがまだ小さくて済む感覚だ。

Reactではテキストボックスへの入力はいちいちonchangeで拾ってstateに反映してやらねばならないと聞くが、これは原理主義的ではあっても効率は劣るのではないだろうか。僕のアプリに必要なのは検索ボタンが押されたときに入力されている内容であって、1文字入力されるごとに何かしたいというわけではない。Hyperappではその辺適当にやれる余地があるようだが、どのように実装するのが美しいのかよくわからない。ブラックボックスを小さめにしたいという学習上の理由からjQueryは使いたくないのだが、すくなくともgetElementById(“input1”).valueではないと思う。誰か教えて。

wikipediaの検索のためにMediaWiki APIを使いたいんだが、Access-Control-Allow-Originの制約によってAPIを呼び出せない。遊びで書いたPythonからは呼び出せたのに、ブラウザから呼び出すとなると禁止とは、なんともケチな話だ。

じゃあウマ娘見るね。