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、というよりブラウザという環境が優れている。

寛容

※この記事は『Asahi クリアセブン』を飲みながら書かれた。

↑まずい。

今日はクソ酷いミスをして人に迷惑をかけてしまったのだが、さらりと許してもらえた。僕もそういう人間でありたいと思う。食堂の給茶機の順番待ちに割り込まれても笑顔でいられるようになりたい。

これまでaptで入れていたChromeを公式debから入れ直したことでバージョンが70になった。タブのデザインが変わり当たり判定が増えたのもそうだが、何より素晴らしいのは拡張機能のSpeedDialが正常に機能するようになったことだ。これまでは4列に設定するとなぜか右にスペースができてしまってとても気持ち悪かったのだが、それがなくなりきちんと中央に配置されるようになった。

Hyperappのプログラムをいじっていたのだが、actionは引数を1つしかとれないということを学んだ。そういうプログラムになっているのはそうとして、そうしている理由はなんなのだろう。

Actions

The only way to change the state is via actions. An action is a unary function (accepts a single argument) expecting a payload. The payload can be anything you want to pass into the action.

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引数のハッシュで、ブラケット記法を用いてキーの方を変更する。バリューはなんでもいい。

生還

※この記事は上野『かっちゃん』で飲んできたあとに書かれた。

今日は研究室のミーティングだった。ろくでもない結果しか出ていないので壮絶な覚悟で望んだが、困っていることを説明したらたくさんのアイデアをもらったディスカッションが弾んだのでよかった。

晴れやかな気分でミーティングを終えられたので友人の招集に応じて上野の立ち飲み屋に行った。『せんべろ』なるジャンルを初めて聞いたのだが、「1000円でべろべろに酔える」ような価格帯の酒場の俗称らしい(wikipedia先生)。そもそも僕は1000円分も飲まずともべろべろに酔えるのでそれほどありがたみは感じないが、今日行った店は1000円ポッキリで生・ウーロン茶・天ぷらセットでとても良かった。昼間は天ぷら定食を売ってる店で本格的な天ぷらだった。

そういえば学科の内定者歓迎会のメールが来ていた。僕の大学は2年生の半ばで3年生以降の学科が決定するので、この時期に学科の内定者の歓迎会が企画される。この会には以前アルコールバトルで敗北して大変なことになった苦い思い出がある。

僕はアルコールが好きだ。強くもなければ詳しくもないが、よく飲む。アルコールを飲むと脳が縮むと言う。気にならないわけではないが、将来の健康を見越して常に最善の行動しかとらないとするとかなり生活が制限されるのであまり考えないようにしている。そもそも人体の耐用年数は30年くらいだろう。その先は高望みしないことだ。

今日のUI心理学

4.3 色の識別能力

目立たせたい色は彩度を上げる。色を比較させたいとき(グラフの凡例など)は眼球運動なしに両方の色が目に入るように近づける。

図1.左眼の網膜位置に対応した桿体・錐体の分布密度(感覚知覚ハンドブック p.924 図18・5・1)

「立命館大学人間科学研究所 人間科学のフロント 色覚障害について考える」より引用

人間の網膜で色を認識できる錐体細胞は視野中心部から両側に10度程度の範囲に密集している。つまりグラフを注視しながら凡例を視野に含めるためには、それらを視角10度以内に配置する必要がある。

視角10度は視距離57cmのときに10cmだ。意外と近い。視角とディスプレイ上の距離(ピクセル)の対応を知るためのツールは以前作った。

(アイコニックメモリーって色はどのくらい正確に保持できるんだろう。教えてエロい人)

over the touge

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

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

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

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

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

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

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

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は書きやすい。楽だ。しかし「どう書くべきか」というノウハウが見つからない。自由は苦痛だ。自由を捨てよ。