Island Life

2016/12/29

100 Rejections

今年は、学生映画も商業も全部ひっくるめてだけど、14回オーディションを受けた。 LAやNYでやってれば週2回とか受けるそうだから、 それに比べれば全然たいしたことはないけれど、 自分的には年間最多記録である。


こちらでオーディションを受け始めて落ちつづけてた頃、 「100回落ちるまでは諦めずに続けてみよう」と思った。 幸い数回に1回くらい拾ってもらえるようになって、 じゃあ年数回くらいのペースで受けてれば年1回くらい何かやれるかな、 てな感じで気が緩んでいたのだが、 「年間100回落ちることを目指せ」という記事を読んで考えを改めた。

In late 2011, a writer friend was sharing her experiences of having months of uninterrupted writing time at her residencies at the Millay Colony, Ragdale, and Yaddo. I was staggered by her impressive rates of acceptance.

[...]

I asked her what her secret was, and she said something that would change my professional life as a writer: "Collect rejections. Set rejection goals. I know someone who shoots for one hundred rejections in a year, because if you work that hard to get so many rejections, you’re sure to get a few acceptances, too."

これは作家の話なんだけど、すごく成功しているように見える作家の友人に秘訣を聞いてみたら、 「不合格/不採択/ボツ通知を集めるの。目標数を決めてね。年間100回落ちることを目指してる人を知っているけれど、それだけ頑張って落ちようと思ったら、そのうちいくつかは受かっちゃうわけ。」

生涯で100回なんてぬるかったっすね。毎年100回かあ。 あらゆるチャンスを、どんなに可能性が低くても、捕まえるつもりでいないと難しいだろう。

でも、落ちることで目標に近づく、と考えると、挑戦へのハードルはちょっと下がる。

落ちてもいいからと何も準備をしないで受けまくる、というわけじゃない。 少なくともオーディションについて言えば、 いい加減な準備で受ける方が万全の準備で臨むよりもうんと恐ろしいことなので、 必然的に準備はするようになる。


で、 エージェント経由でのオーディションは時の運だし増やしたくて増えるもんでもないので、 オープンオーディションで必ずしも自分のタイプと一致しなくても、 あるいはリモートでのプロダクションへのビデオオーディションとかでも、 とりあえず受けるようにしてみた。

100回にははるかに及ばないが、 オーディションの頻度が上がることで、変わってきたことがある。

オーディションは自己顕示ではなく作品発表の場、と割りきれるようになり、 何を準備してゆくべきかが明確になったことだ。

映像系のオーディションでは、1〜2シーンをあらかじめ受け取り、 それを演じるのが一般的だ。1次審査では相手役の台詞をADさんとかが読んでくれて 一人で演じる。2次審査(callback)がある時は、他の役者との絡んで演じることもある。 併せてモノローグを演じることもある。

どっちにせよ、シーンを演じるわけだから、 普段のアクティングクラスと同様に、シーンを創ってゆけば良いのだ。 アクティングクラスの場合は事前に相手役とリハーサルできることがあるから若干事情は異なるが、 リハーサルをやる直前の状態までは自分で持っていけるわけで、 その手順は普段の準備と何ら変わらない。 台詞を入れて、シーン分析を行い (superobjective・objective・obstacle・"the moment before"の同定、 substitution・inner object・actionの選択)、 シーンを成立させるところまで作っておく。 この準備過程は、やればやるだけ上手くなってゆくので、 オーディションもその機会だと思えばいい。

それを見て向こうがどう判断するかは向こう次第で、 自分の力の及ぶところではないから気にしても仕方ない。 たとえ素晴らしい演技が出来たとしても、 それが向こうの考える作品にフィットするかどうかは別問題だから。 ただ、自分が作って行ったシーンを人前で演じてみせる、という経験は確実にひとつ積み重ねられる。

こう思い始めたら、オーディション後の振り返りも別の意味を持ってきた。 以前、オーディション後に「あそこはああすれば良かった」などとくよくよ考えてもダメと書いたことがある。 それは変わっていない。 オーディションの場で起きたことについて後から別案を考えることに意味はない。 けれども、準備は足りていたか、結果としてシーン中にどこまでtruthfulになれたか、 という点について振り返り、次の準備へと反映することはできる。 substitutionは自分の中ではっきりしていたか、 the moment beforeを忘れていなかったか、 相手にreactできていたか、このへんの振り返りは、 アクティングクラスでシーンを演じた後の振り返りと全く同様だ。

舞台系のオーディションでは脚本全体が対象でどこを演じるかを事前に知らされないので、 完璧に準備してゆくのは難しい(未出版作品だと、契約の都合上脚本の持ち帰りも出来なくて、 劇場に行って目を通すだけ、ということもある)けれど、やれるところまで分析しておくことはできる。

最近は自分でシーンを録画して送る、self-tape auditionも増えてきた印象がある。 今年は4つやったかな。 オーディションの話が来てシーンの脚本を受け取ってから締切りまで36〜48時間しかないこともある。

脚本を受け取ったらまず、相手役を確保する。写るのは自分だけだけど、 相手の台詞へのリアクションは非常に重要なので、 カメラの向こうで生身に人間に読んでもらうのが極めて有効だ。 役者仲間には、 仕事の昼休みに手伝ってもらったりとか、色々世話になった。 感情的なシーンだと、静かな場所の確保も必要になる。 段取りをつけて、自分の演技の準備もして、効率的に撮影し、編集して送る。 これも回数をこなせばそれだけ練習になる。

10月にハワイ島で撮影した映画もこの形式のオーディションだった。 10/9の夜にエージェントから「明後日の午前中までにオーディションビデオ出して」と言われて大急ぎで段取りつけて翌朝撮影。 1週間後の10/17に「あなたを使いたいんだけど5日後から10日間ハワイ島に来れる?」と連絡があって、 慌てて仕事のスケジュールを調整した。 映画は来年公開予定。 3シーンくらい出演してる。カットされてなければ。


さて明日も撮影なので準備せねば。

Tag: 芝居

2016/12/28

批評の受け止め方

ネットに初心者が公開したプログラム等の成果物にあんまりツッコミが入ると萎縮しちゃうんじゃないか、という話が上がってた。

何かを表現することの初期段階に、 安全な環境で批評を受けるのは重要だ。たとえ Don't take it personallyと言われても、 意識的に自分と自分の作ったものを分けて考えるのは(最初のうちは)難しい。

ただ、ツッコミにもいくつか種類があって、 それを全部「不寛容社会」のせいにするのも乱暴すぎる。


成果物(ここではプログラム等)に対するツッコミの目的は、いくつかに分類できる。 複数の目的が混じっている場合もあるが、ツッコミを読み解くことで、そこに込められた 情報を目的別に分離できる場合が多い。

  1. 成果物自体の改善を目的とするもの: 最終的に、指摘が成果物に反映されることを期待するツッコミ。 バグの指摘、アルゴリズム上の問題、運用上の落とし穴の解消、等。 具体的な修正案やパッチの形を取ることが多い。
  2. 読者への情報提供を目的とするもの: 検索して訪れた読者が情報をそのまま流用するとまずいことが起きる可能性があるため、 そういう読者への付加的な情報を提供しようとするもの。 作者が本文を変更して注意書きを足してくれるに越したことはないが、 コメント等で読者の目に触れるようになっていればいい。
  3. 作者の行動を変えることを目的とするもの: 作者に一歩進んでほしいという建設的なものも、 その程度の意識で出てくるなといった非建設的なものもある。
  4. 突っ込む側の自己満足: 自分の優位性を誇示するマウンティング目的とか、 自分が押すフレームワークやら言語やらの布教目的とか。

個人的な印象では、エンジニア界の手斧の投げ合いの結構な割合は1や2だと思う。 コードが良くなることや、誤った情報が広まらないことが重要であって、 誰が書いたかとか誰が指摘したかはどうでもいい。 とはいえ、3や4も混ざってなくはない。

一方、作者の方の目的も色々だ。 成果物を晒してブラッシュアップする、というのが目的とは限らない。 初めて書いてみてうまく動いたからやったー!っていう気持ちをシェアしたいだけかもしれないし、 勉強を続けるモチベーション維持のために経過報告をしているだけかもしれない。

作者の目的と指摘者の目的が食い違っている時に、摩擦が生じる。


公開する以上、不特定多数からのフィードバックが集まるのは仕方ない。 作者の側として、次のことをまず前提にするといい。

ツッコミを受けとるか流すかは、作者の一存で決めることである

こう書くと当たり前のようだけど、何か言われたら影響されちゃうのも人間の性である。 けれど、「影響されるかどうかを決めるのも、自分の一存」なのだ。 明後日の方向に突っ込んでる無価値な批評はいくらでもあり、 そんなのをいちいち相手にする必要はない。 自分の目的にとって役に立つ批評だけより分けて使い、後は捨てればいい。

その前提を置いた上で、寄せられたツッコミのカテゴリにより対応を変える。 (混ざっている場合は要素ごとに分離して考えれば良い)

  • 突っ込む側の自己満足 - 無視する。
  • 作者の行動を変えようとするもの - 「この人の言うことなら聞いていいかも」と思える場合は取り入れる。それ以外は無視。
  • 成果物の改善 - 成果物を今後も使うつもりでメンテしていきたいなら取り入れよう。 でも、自分ではサンプルや練習のつもりで、それ以上改善する意図が無い時にこれが来たら、 その旨説明してお引き取り願うのが良かろう。 最初からそう説明してたのに指摘してくるのは無視でいい。
  • 読者への情報 - これは自分の問題ではなく、自分が書いたものを読む不特定多数の利益に関することである。 間違ったり誤解されやすい情報を広めることが本意でなければ、 その事実が読者の目に触れるようにしておくのが良いだろう。

だいたいにおいて、突っ込む側には「何かを変えたい」というモチベーションがある。 その変えたいものが、自分が変えたいものと一致するなら、指摘を利用すればいいし、 そうでないなら受け流せばいい。 相手のゲームに乗る必要はない。 自分のサイトで発表したなら、ゲームのルールを決めるのは自分だ。

Tags: Programming, 表現

2016/12/23

トランプのヤバさ

珍しく政治ネタ。4年後に読み返して杞憂だったなと思えることを祈りつつ。

大番狂わせの大統領選から1ヶ月半。 今週頭には選挙人投票があってトランプ次期大統領が正式決定した。 ひととおり選挙の分析が出た後、 リベラル勢からは、 ここからどう立て直してゆくかについての戦略や決意表明が次々に出ている。 (例えばBruce Schneier, これからの四年間における私の優先事項, yomoyomoさん訳)。 私の観測範囲にリベラルが多いせいもあり、皆が闘いに備えているような張り詰めた雰囲気を感じている。

でも、日本の報道や反応を見ていると、 「選挙戦では極端なことを言ってたけど、それ以外の公約にはまともなこともあるし、 いざ大統領になれば変わるんじゃないか」 「トランプ一人で何でも出来るわけじゃない、無茶な言動は優秀なスタッフが何とか止めるだろう」 みたいな楽観的な感覚もあるようだ。 トランプが過激な発言をするたびにリベラル系メディアが騒ぐのを、過剰反応しすぎじゃないの、 と感じている人も結構いるんじゃないか。

個人的には、ものすごくヤバい、と感じてる。

でもそれは、トランプの差別的発言とか、表現の自由への脅威だとか、 核武装強化発言だとか、そういう個々の動静ではない。

最近、トランプの側近の一人が 「彼(トランプ)の発言は文字通り(literally)取るんじゃなくて、象徴的に(symbolically)取るべき」と発言した。この一言にヤバさが集約されている。それを無邪気に側近が言ってしまうのがさらにヤバい。

実際、「国境に壁を作り、費用はメキシコに払わせる」とか「不法滞在外国人を全員追放」とか、 熱狂的支持者はともかく、大部分の人は文字通りそれを実行できるとは思ってなくて、 こういうのはキャッチーな煽りだと思ってるだろう。トランプはそういう煽りでメディアをひとしきり 賑わせた後で「やっぱやらないかも」「文字通りそういうことをやるという意味ではない」みたいに トーンダウンしたりする。裏付けの無いニュースや明らかに事実と反する情報を平気で流すこともやる。 要するに具体的な中身よりも、どう周囲が反応するかの方を重視している。 その点に関しては、かなり計算高い人物だと思う。

これが続くとどうなるか。

過去の発言に関して突っ込まれたら、 「あれは象徴的な意味で言ったのだ」といつでも逃げられる、ということだ。 言葉に対して責任を負わなくても良くなる、ということだ。

そして周囲も、毎回毎回の煽り発言に釣られるのにだんだん疲れてきて、 「彼はそういう人だから」で流すようになる。 で、発言が文字通り解釈できないから、 その発言が象徴するところの意味を忖度して周囲が行動するようになる。 トランプの意向に反する、と周囲が思う行動があれば、 先回りしてそういうのを抑圧するようになる。

でもトランプは具体的にそうしろという指示を出したわけではない。 行き過ぎがあれば「部下が勝手にやったこと」でその部下を切ればいい。 黙認していれば部下は承認されたと解釈して同様の行動を強化してゆく。 皆が、明示されない一線を探り合いながら行動するようになる。 その結果生じたことについて、上の人間は誰も責任を取らない。

(追記2016/12/24 23:04:18 UTC: もっと言っちゃうと、これ、 カルト内部で集団心理が形成されるのと全く同じプロセスなんである。 トンデモな教祖になぜ人がついてゆくか。 それをまるきり信じ込む人だけが信者になるのではない。 「あの発言は文字通りの意味じゃなくて象徴だから」 という前提を置くと、大抵のトンデモ発言は正当化できてしまう。 頭の回る人ほどそいういうのが得意だ。 各メンバーに、そのカルトに居つづける個人的な理由がわずかでもあれば、 トンデモ発言はそこを離れる理由にはならない。 そして誰かが発言に対する疑問を呈しても、 周囲はあらゆる正当化を使ってそういう疑問を潰しにかかる。 この罠に落ちた集団を内部からの力のみで変革することは極めて難しい。)

これが企業なら、数字で結果が出るから、 どうやったら市場にうけるかを探りながらうまく手綱を調節して、 最終的に損をしなければビジネスとしては成功であると言えるかもしれない。 しばしばトップに極端な人物を置いて 側近が実務をしっかり締めることでうまく回る場合もあるかもしれない。

損をだれかに押し付けて切り抜けることがあっても、 自分が常により大きな力を集めている限り、 損を被って去る人よりも寄ってくるサポーターの方が多くなる。 マイナスが大きくてもそれを上回るプラスがあればビジネスは回る。

けれど、国家の運営はそうはいかない。

「この州は税収に貢献していないから」とリストラすることは出来ないし、 トップダウンの方針が気に入らない奴は出ていけ、と言うこともできない。 結局うまくいかなかったから国を畳みます、というわけにもいかない。

言葉でもって方針を示し、それを国民から承認されることで、信託された力を行使する。 その原則が崩れてしまったのが、何よりもヤバい。

Tag: 生活

2016/12/22

call/ccはいつ使う?

(この記事はLisp Advent Calendar 2016の23日めの記事です)

昔のTVは、取扱説明書に回路図がついていた。 中学校に上がるか上がらないかの頃、多少電子工作をかじって背伸びしていた私は 家のTVの回路図を眺め、 「ここを映像信号が通ってるはずだから、この信号を取り出してカセットテープに録音したり 再生した信号をここに流し込んだらビデオになるんじゃないか」とか無茶なことを 考えて試したことがある。まだ周波数帯域という概念を知らなかったんだな。


さて。第一級継続とそれを取り出す手続きcall/ccは、 Schemeの特徴としてよく挙げられる。 使い始めの頃は、よくわかんないけどなんだかすごそうな機能に見えて憧れたりする。 いくつかcall/ccを使った例を実装してみるうちに、 安全なsetjmp/longjmpみたいなもんにすぎない、と納得できるんだけど、 いざこの機能をどう使えば良いか、となると難しい。

普段のプログラミングで使えそうな場面は、まさにsetjmp/longjmp相当の、 関数呼び出しの深いところから脱出する時くらいのものだろう。

例えばハッシュテーブルのすべてのキーとその値に与えられた手続きを適用する hash-table-for-each (refj:hash-table-for-each) がある時、 「述語predを満たすキーと値を見つけたら捜索を打ちきってreturnする」 なんてのが書きたいとする。 Schemeにはreturnが無い(「WiLiKi:Scheme:なぜSchemeにはreturnが無いのか」)から、 こういう時は継続の出番だ。 (処理系にlet/cc (refj:let/cc)などの簡略化マクロがあれば、 call/ccよりそっちを使った方が見やすいだろう)

(define (hash-table-any table pred)
  (let/cc return
    (hash-table-for-each table  ; tableを順次調べ、predが真を返したらreturn
       (lambda (key val) (let ((r (pred key val)) (if r (return r))))))
    #f))  ; 見つからなかったら#f

しかしこれ以外の応用となるとどうだろう。 良く例外処理だのコルーチンだの非決定性計算だのが挙げられるけれど、 普通のアプリケーションプログラミングをしていてこういった制御構造を「自分で」 書く必要に迫られることは滅多にない。というより、むしろこれらの機能を 処理系が提供しているなら、自分で書くのは避けた方が良い。

例えば例外処理なら、最近のSchemeにはguardがあるはずだ。 自分でcall/ccを使って例外処理を書いてしまうと、 組み込みのguardを使った他のライブラリと整合性が取れなくなる可能性がある。

コルーチンにしても、目的に特化したものなら例えば gauche.generatorモジュール(refj:generate)、あるいはSRFI:121(refj:make-coroutine-generator)等がある。 これらは内部的には継続を使っているんだけれども、 アプリケーション層でそれを気にする必要はない。

プログラミング言語のユーザに、「言語を使ってアプリケーションを書く人」と 「言語仕様を決めたり実装したりする人」という区別があるとしたら、 call/cc は「アプリケーションを書く人」にとっては ほぼ使う必要がない機能と言ってしまってもいいかもしれない。


もちろん例外処理やコルーチンライブラリを「実装する人」はcall/ccを使うんだけれども、 もしこれが「一つの処理系によって仕様が決められるような言語」であれば、 call/ccみたいな面倒くさい機能をわざわざ 仕様にする意味はあまりない。 新たな制御機能のアイディアを思いついたら、処理系のソースを直接いじって実装して パッチを投げればいいからだ。

call/ccは、原理は単純なんだけれども、 現実の実装に課す制約が多い機能でもある。特に関数のcall/returnを主流の言語の実行モデルに 合わせたい場合に色々面倒が多い。既存の言語の多くが、やろうと思えばcall/ccを 提供できるとしても、それをやらずに目的の制御構造を直接実装してしまう理由のひとつは それだろう。実装を複雑にしてまでcall/ccを実装するメリットが見えないというわけだ。

それでもSchemeがあくまで call/cc にこだわるのは、 別にそれが伝統だからとかアカデミックに重要だからとかかっこいいから、ではない。


プログラミング言語は、コンピュータで実行するプログラムを書く言語であると同時に、 プログラムについてのアイディアを人とやりとりするための言語でもある。 アルゴリズムを説明するのに、全て自然言語で説明するのはあまりにまどろっこしく 不正確だ。擬似コードでも良いが、ディテイルまで 詰めたければ明確な定義のある言語で書くのが結局は一番手っ取り早かったりする。

そのアイディアの説明であるコードが、直接実行できて動作を確かめられるなら、なお良い。

call/ccは、新たな制御構造のアイディアを議論する際に、 直接制御の流れをいじれて、かつ比較的明確な定義があるプリミティブ、として活躍するのである。

R6RS以降標準となった例外機構のguardは当初SRFI:34で議論された。 その参照実装を見ると、call-with-current-continuationがネストしていて 慣れないといかにも恐ろしげに見える。 でも慣れるとむしろどのコードがどの動的環境で実行されるかが明確に示されているので、 なまじ曖昧に自然言語で説明されるよりもわかりやすいのである。 (なお、実はGaucheのguardにはknown bugがあって srfi-34と完全に同等になっていない… 効率を落とさずにサポートするのが今の実装では面倒なので先延ばしにしている。)

部分継続(refj:gauche.partcont)みたいなトリッキーな制御構造も、効率を考えなければcall/ccで実装できて、それは意味を考える上でも、実装の際の動作確認の比較対象としても、とても有用だ。

このように、「言語の仕様を考えたり実装したりする人」にとって、call/ccというのは新たな制御構造を試すのになかなかに便利なツールなのである。 (ただし、トップレベルの継続のように仕様が曖昧な部分も無くはない。)


先に、プログラミング言語のユーザに、「言語を使ってアプリケーションを書く人」と 「言語仕様を決めたり実装したりする人」という区別があるとしたら、と書いた。 けれども、そういった区別は必要だろうか。

Lisp系言語は、"Programmable programming language" と呼ばれてきた。 その言語を使ってプログラムを書く、だけではなく、 その言語を使って言語自身をいじるための言語でもあるということだ。

もちろん、言語仕様は誰かが便利に決めてくれればいい、それを使って アプリケーションを作ることに関心がある、というユーザもいていい。 でも、「なんかこのパターン毎回書いてるけど言語組み込みになってたら便利じゃない?」とか 「今はこういう仕様になってるけどコーナーケースはこう動くべきじゃ?」とか 「やっぱり言語にはGOTOが必要だ!」とか 思いついちゃった時に、

  • そのアイディアをコードとして明確に書き下して、
  • 自分で実行して確かめられて、
  • さらに他の人とシェアして別の処理系で試してもらったり新しいアイディアをもらったりする

ってことが出来たら、それはそれで楽しい。そうやって練ったアイディアがいずれSRFIに、そしてRnRSへと反映されてゆくかもしれない。


昔のTVについていた回路図は、ほとんどのユーザにとっては無用の長物だったろう。 けれども町の電気屋さんは持ち込まれた近所の家のTVを直すのに重宝しただろうし、 腕に覚えのあるお兄さんは魔改造するのに使ったかもしれないし、 電子工作少年の心をくすぐるのにも十分だった。 その気になれば、中身に手を突っ込んでいじれる、そのための共通のコトバとして、 call/ccをとらえてみるといいかもしれない。

Tags: Programming, Scheme, call/cc

2016/12/15

parameterizeの面倒くさい話

(この記事はLisp Advent Calendar 2016の16日めの記事です)

あらかじめ言っておくと、今日のエントリはやたら長いし、 parameterizeの仕様の重箱の隅をつつくような、面倒くさい話である。 Scheme処理系の実装者でもなければ「細けぇことはいいんだよ!」で流したくなるかもしれない。 でも、Schemeの一機能の話ではなく、 ぱっと見で簡単に見える仕様でも隙無く実装するのが難しいことがあるよ、って話だと 読んでもらえれば。

なお、文中のコードは https://github.com/shirok/parameterize-pitfalls に置いてある。


parameterizeとは

レキシカルスコープを採用したSchemeにおいて、 ダイナミックスコープをライブラリレベルで実現する仕組み。 いくつかの処理系に昔からあったが、2003年に基本部分がSRFI:39で規格化され、 R7RSにも取り込まれた (ただし、R7RSの仕様はsrfi-39とは微妙に違う)。 基本的には、動的束縛のメカニズムをクロージャの中に隠すという発想だ。

Common Lispの以下のようなコードは:

(defvar *special* 1)

(defun get-special ()
  *special*)

(let ((*special* 2))
  (get-special))  ;=> 2

(get-special) ;=> 1

parameterizeを使ってこう書ける。

(define special (make-parameter 1)) ; [1]

(define (get-special)
  (special))                        ; [2]

(parameterize ((special 2))         ; [3]
  (get-special))  ;=> 2

(get-special) ;=> 1

make-parameter は、束縛を包み込んだクロージャを返す[1]。 引数無しで呼べば、現在の動的環境での値が返る[2]。 マクロparameterizeによって、動的スコープで値を変えることができる[3]。 (make-parameterが返すものは単なるクロージャで良いのだが、 以下では説明のために「パラメータオブジェクト」と呼ぶ。)

単なる変数ではなくパラメータオブジェクトにしてあるのは、 実装に柔軟性を持たせるためだ。単なる変数でも、動的束縛マクロの中で グローバルな値を差し替えてやれば動的変数のように振る舞わせることは可能だが、 例えば後から拡張してスレッド安全にしたくなっても、打つ手がひどく限られてしまう。 パラメータオブジェクトとして扱っておけば後からいろいろやりようがある。

さて、これから、パラメータオブジェクトとparameterizeの実装を何通りか 見てゆくけれど、中にはバグのあるものが混ざっている。 Schemeに心得のある読者は、「……ほんとに?」が出てきたら、 バグの解説を見る前に、問題を見抜けるかどうかちょっと考えてみてほしい。


実装例1

最もストレートな実装は、パラメータオブジェクトPを0個または1個の引数を取る クロージャにして、(P)と呼ばれたら現在の値を返し、 (P newval) のように呼ばれたら、newvalを新たなパラメータの値に するとともに以前の値を返す、ということにするものだ。 SRFI:39もこの線で仕様化されている。

(define (make-parameter init)
  (case-lambda
    [() init]                 ; 引数なしの時は現在値を返す
    [(newval)
     (begin0 init             ; 引数ありの時はそれを新たな現在値にして元の値を返す
       (set! init newval))]))

(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let* ([params (list param ...)]   ;paramのリスト
            [vals   (list val ...)]     ;valのリスト
            [saves  (map (^[p v] (p v)) params vals)]) ;入る前の値を保存 [1]
       (dynamic-wind
         (^[] #f)
         (^[] body ...)
         (^[] (for-each (^[p v] (p v)) params saves))))])) ;[2]

parameterizeに入ったら入る前のパラメータの値を保存し、 パラメータを指定された値に書き換える。 パラメータオブジェクトは引数つきで呼ばれると「新しい値の設定と、古い値の返却」を 同時に行うので、[1]の行だけで「各paramに対応するvalの値を設定すると ともに、savesに以前の値を保存する、というアクションが起きる。

bodyの実行中にエラーが起きた時でも確実にparamの値が復元できるように、 dynamic-windを使って保存されていた値を再設定している[2]。

実行してみよう。

gosh> (define special (make-parameter 1))
special
gosh> (define (get-special) (special))
get-special
gosh> (parameterize ((special 2))
        (get-special))
2
gosh> (get-special)
1

parameterizeの実行中に呼び出されたget-special はその時点での動的束縛である2を返し、外側で呼ばれたget-specialは元の値を 返している。 うまくいっているようだ。

……ほんとに?


実装例1の問題

dynamic-windの呼び出しが非対称だからこれはわかりやすかったかも。

問題が出るのはたとえば次のコード。

gosh> (define cc #f)
cc
gosh> (parameterize ((special 3))
        (call/cc (lambda (c) (set! cc c))) ;[1]
        (get-special))
3
gosh> (cc #f)
1

parameterizeの本体で継続を捕捉し、 parameterizeから抜けた後でその継続を再起動している。 つまり、(cc #f)の呼び出しは[1]の時点に戻る(引数#fは [1]の式の返り値になるけれどここでは捨てられるので、何でも良い。) そこで再びget-specialを呼ぶけれど、ここは再びparameterizeの 有効期間の中なので、動的束縛された3が返されなければならない。

(「一旦抜けてしまったら戻さない」という仕様を別途考えることもできる。 けれども、継続というのは「その時点の動的環境を捕まえるもの」なので、 パラメータを動的束縛の仕組みとするなら、値が戻るのが正しい。 例えば継続によるコルーチンを考えれば、値が戻ってくれないと困ることがわかるだろう。)

この問題をfixするのは易しい:


実装例2

make-parameterは実装例1と同じ。 parameterizeで、savesの設定をdynamic-windbeforeサンクで やるようにした。

(define (make-parameter init)
  (case-lambda
    [() init]                 ; 引数なしの時は現在値を返す
    [(newval)
     (begin0 init             ; 引数ありの時はそれを新たな現在値にして元の値を返す
       (set! init newval))]))

(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let ([params (list param ...)]   ;paramのリスト
           [vals   (list val ...)]     ;valのリスト
           [saves  #f])
       (dynamic-wind
         (^[] (set! saves (map (^[p v] (p v)) params vals)))
         (^[] body ...)
         (^[] (for-each (^[p v] (p v)) params saves))))]))

call/ccによる再起動もok。

gosh> (define special (make-parameter 1))
special
gosh> (define (get-special) (special))
get-special
gosh> (define cc #f)
cc
gosh> (parameterize ((special 3))
        (call/cc (lambda (c) (set! cc c)))
        (get-special))
3
gosh> (cc #f)
3
gosh> (get-special)
1

これで無問題だ。

……ほんとに?


実装例2の問題

問題が出るのは次のようなケース。

gosh> (parameterize ((special 3))
        (special 5)          ; [1]
        (call/cc (lambda (c) (set! cc c)))
        (get-special))
5
gosh> (cc #f)
3

[1]でspecialの値が5に変更されているので、継続を再起動したら 5が見えなければならない。

ちなみに、specialの設定を"""call/cc""の後ろに持ってくると 一見動いてるように見えるんだけど:

gosh> (parameterize ((special 3))
        (call/cc (lambda (c) (set! cc c)))
        (special 5)
        (get-special))
5
gosh> (cc #f)
5

こうしてみるとまずいことがわかる:

gosh> (parameterize ((special 3))
        (call/cc (lambda (c) (set! cc c))) ; [1]
        (print "special=" (get-special))
        (special 5) ; [2]
        (get-special))
special=3   ; printによる表示
5
gosh> (cc #f)
special=3   ; ここは5でないとならない
5

クロージャが静的環境(レキシカル環境)を捕捉するように、call/ccは 動的環境を捕捉する。[1]でcall/ccが捕まえるのはspecialの 値そのものではなく、その時点でparameterizeが作っている動的環境だ。

クロージャ内で閉じ込んだ変数にset!すればそのその変更が一旦クロージャから 抜けても残るように、動的変数の変更の効果は永続的でなければならない。 [2]によるspecialの値の変更は、parameterizeが作る動的環境の 変更なので、継続の再起動で[1]に戻ってきた時にはspecialは既に5になっているというわけ。

fixは次のとおり:


実装例3

やはりmake-parameterは同じ。 parameterizeのafterサンクで、「抜ける時点でのパラメータの値」を 再びvalsに保存しておく。bodyに再び戻ってきたら、抜けた時点での 値が復元される。

(define (make-parameter init)
  (case-lambda
    [() init]                 ; 引数なしの時は現在値を返す
    [(newval)
     (begin0 init             ; 引数ありの時はそれを新たな現在値にして元の値を返す
       (set! init newval))]))

(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let* ([params (list param ...)]   ;paramのリスト
            [vals   (list val ...)])    ;valのリスト 兼 現在の値のセーブ
       (dynamic-wind
         (^[] (set! vals (map (^[p v] (p v)) params vals)))
         (^[] body ...)
         (^[] (set! vals (map (^[p v] (p v)) params vals)))))]))

確認。

gosh> (define special (make-parameter 1))
special
gosh> (define (get-special) (special))
get-special
gosh> (define cc #f)
cc
gosh> (parameterize ((special 3))
        (special 5)
        (call/cc (lambda (c) (set! cc c)))
        (get-special))
5
gosh> (cc #f)
5
gosh> (get-special)
1

実装例3は動的束縛を実現しているが、srfi-39およびR7RSのパラメータには追加仕様があって これだけでは不十分だ。その点については後で見て行くが、 その前にもうひとつ、気になる点に触れておく。


実装例4 中間リスト作成を避ける

実装例3では、parameterizeに入って抜けるまでにリストが4本作られる。 性能に敏感なSchemerはそこに眉をひそめるかもしれない。パラメータの数は コンパイル時にわかるのだから、わざわざリストにせずとも、マクロ展開時に 同数の一時変数を用意してそれぞれにセーブしてやれば良い。

;; これは例3までと同じ
(define (make-parameter init)
  (case-lambda
    [() init]                 ; 引数なしの時は現在値を返す
    [(newval)
     (begin0 init             ; 引数ありの時はそれを新たな現在値にして元の値を返す
       (set! init newval))]))

;; 値のセーブをリストにするのではなく、各パラメータごとに一時変数を作る方式
(define-syntax parameterize
  (syntax-rules ()
    [(_ (binding ...) body ...)
     (%parameterize (binding ...) () () () (body ...))]))

;; 補助マクロ。再帰しつつparam、valと同数のtmpを作る。
(define-syntax %parameterize
  (syntax-rules ()
    [(_ () (param ...) (val ...) (tmp ...) (body ...))
     (let ((tmp val) ...)
       (dynamic-wind
         (^[] (set! tmp (param tmp)) ...)
         (^[] body ...)
         (^[] (set! tmp (param tmp)) ...)))]
    [(_ ((param val) . rest) (p ...) (v ...) (t ...) bodies)
     (%parameterize rest (p ... param) (v ... val) (t ... tmp) bodies)]))

syntax-rulesで不定個の一時変数を用意するのはちょっとまどろっこしい。 見慣れてない人は戸惑うかもしれないので1ステップづつ追っておこう。 入力が

(parameterize ((a x) (b y) (c z)) body)

の場合、まず補助マクロが

(%parameterize ((a x) (b y) (c z)) () () () (body))

と呼ばれる。これが%parameterizeの2番目の節にマッチして、再帰的に 束縛がひとつづつばらされて、対応する一時変数が追加されてゆく。

(%parameterize ((b y) (c z)) (a) (x) (tmp) (body))
↓
(%parameterize ((c z)) (a b) (x y) (tmp tmp) (body))
↓
(%parameterize () (a b c) (x y z) (tmp tmp tmp) (body))

束縛が空になったら%parameterizeの1番目の節がマッチして、 letdynamic-windに展開される。 変数名tmpが重なっているように見えるが、衛生マクロによって 再帰の度に内部的には別変数として扱われるので衝突しない。

実際の展開例も示しておこう。macroexpand-allの出力は わかりやすいようにインデントして示す。

gosh> (macroexpand-all '(parameterize ((a 3)(b 5)) (list a b)))
(letrec ((tmp.0 '3)
         (tmp.1 '5))
  (dynamic-wind
    (lambda ()
      (begin (set! tmp.0 (a tmp.0)) (set! tmp.1 (b tmp.1))))
    (lambda ()
      (list a b))
    (lambda ()
      (begin (set! tmp.0 (a tmp.0)) (set! tmp.1 (b tmp.1))))))

展開例でわかるように、この実装例は余分なリストを作らない。めでたしめでたし。

……ほんとに?


実装例4の問題と実装例5

実装例4には非常に微妙な問題がある。

gosh> (define special (make-parameter 1))
special
gosh> (parameterize ((special 3))
        (set! special string->number))

*** ERROR: string required, but got 1
Stack Trace:
_______________________________________
  0  (special tmp)
        [unknown location]
  1  (^ () (set! tmp (special tmp)))
        [unknown location]
  2  (eval expr env)
        at "/usr/share/gauche-0.9/0.9.5/lib/gauche/interactive.scm":269

パラメータの保持する値ではなく、パラメータを指す変数そのものを書き換えてしまった 場合にエラーになる。

もちろんそんなことをする意味はほとんどない。specialset!した後では specialをパラメータとしては使えなくなる。それでも、parameterizeの ボディ中でパラメータspeciallを一切参照していないにもかかわらず、 エラーが出てしまうのは抽象化の破れである。

このfixは簡単だ。parameterizeに与えられたパラメータ式自身も 一時変数に束縛しておけばよい。

;; 実装例5
(define-syntax %parameterize
  (syntax-rules ()
    [(_ () (param ...) (val ...) (ptmp ...) (vtmp ...) (body ...))
     (let ((ptmp param) ...
           (vtmp val) ...)
       (dynamic-wind
         (^[] (set! vtmp (ptmp vtmp)) ...)
         (^[] body ...)
         (^[] (set! vtmp (ptmp vtmp)) ...)))]
    [(_ ((param val) . rest) (p ...) (v ...) (pt ...) (vt ...) bodies)
     (%parameterize rest (p ... param) (v ... val)
                    (pt ... ptmp) (vt ... vtmp) bodies)]))

展開例は次のとおり。

gosh> (macroexpand-all '(parameterize ((a 3)(b 5)) (list a b)))
(letrec ((ptmp.0 a)
         (ptmp.1 b)
         (vtmp.2 '3)
         (vtmp.3 '5))
  (dynamic-wind
    (lambda ()
      (begin (set! vtmp.2 (ptmp.0 vtmp.2)) (set! vtmp.3 (ptmp.1 vtmp.3))))
    (lambda ()
      (list a b))
    (lambda ()
      (begin (set! vtmp.2 (ptmp.0 vtmp.2)) (set! vtmp.3 (ptmp.1 vtmp.3))))))

では次に、srfi-39/r7rsのフル仕様のサポートを考えよう。


converter手続きと実装例6

srfi-39/r7rsのmake-parameterは追加引数を取れる:

    (make-parameter init-value [converter])

converterは1引数の手続きで、パラメータの値が変更される際に、 新たな値としてパラメータに渡された値を受け取る。 そこで適切な型変換をしたり、値の有効性をチェックしたりできる。

例えば、パラメータの値は常に文字列であって欲しいとしよう。次のとおり、 converter手続きで文字列に変換するようにしておけば:

(define prompt
  (make-parameter
   ">"
   (lambda (x)
     (if (string? x)
       x
       (with-output-to-string (lambda () (write x)))))))

こうなる:

gosh> (prompt)
">"
gosh> (parameterize ((prompt '$))
        (prompt))
"$"

では実装例3のmake-parameterを改造してconverter手続きをサポートしてみよう。 要は、newvalが与えられた時にconverterを噛ませてやればいいだけのはずだ。

(define (make-parameter init :optional (converter identity))
  (let1 val (converter init)
    (case-lambda
      [() val]
      [(newval) (begin0 val (set! val (converter newval)))])))

;; これは例3と同じ
(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let* ([params (list param ...)]   ;paramのリスト
            [vals   (list val ...)])    ;valのリスト
       (dynamic-wind
         (^[] (set! vals (map (^[p v] (p v)) params vals)))
         (^[] body ...)
         (^[] (set! vals (map (^[p v] (p v)) params vals)))))]))

これで、上のpromptの例も動く。簡単だね。

……ほんとに?


実装例6の問題

こんなパラメータspecialを作ってみる。

gosh> (define special
        (make-parameter 1 -))
special

動的束縛時に与えられる値の符号を反転したものが値となる。

gosh> (special)
-1
gosh> (parameterize ((special -5))
        (special))
5

動いているように見える。しかし…

gosh> (define cc #f)
cc
gosh> (parameterize ((special -5))
        (call/cc (lambda (c) (set! cc c)))
        (special))
5
gosh> (cc #f)
-5     ;; 5になるべき

継続ccでもってparameterizeの中に戻ったら、specialの 値の符号が反転してしまった。


実装例7

問題は、converterが冪等でない場合、動的束縛時と 値のリストア時で処理を変えなければならない点にある。 動的束縛時にはconverter手続きが適用されるが、値のリストア時はconverter手続きを バイパスしなければならない。 これは、パラメータオブジェクトの保持する値を変更するのに、(P newval)以外の もうひとつ別のチャネルが必要であることを意味する。

SRFI:39の参照実装ではパラメータオブジェクトの値を保持するセルを グローバルに見えるリストにつないでおいて、値の復帰はそちらを直接いじるようにしている。 でもこれまで各パラメータ内に値を保持してきたのだから、 ちょっとAPIをいじることで適合できないだろうか。 (昔のLispの用語を使えば、SRFI:39の方法はグローバルに環境をスタックして そこから探すのでdeep binding的、ここでやっている方法は各変数(パラメータ)が 動的状態を保持するのである意味shallow binding的と言える。)

動的束縛の値を変える二つの方法を区別できれば良いのだから、 例えばパラメータオブジェクトにこっそり、 converter手続きを通さない第3のモードを追加すれば実現できそうだ。

;; parameter手続きのインタフェースを変更
;;  (proc)                  => 現在の値を返す
;;  (proc newval)           => newvalを新しい値とし、以前の値を返す
;;  (proc newval something) => newvalをconverter手続きを通さずに直接セット
;; 3番目は内部的に使う
(define (make-parameter init :optional (converter identity))
  (let1 val (converter init)
    (case-lambda
      [() val]
      [(newval)   (begin0 val (set! val (converter newval)))]
      [(newval _) (begin0 val (set! val newval))])))

(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let* ([params (list param ...)]   ;paramのリスト
            [vals   (list val ...)]     ;valのリスト
            [saves  #f])
       (dynamic-wind
         (^[] (if saves
                (set! saves (map (^[p v] (p v #t)) params saves))
                (set! saves (map (^[p v] (p v)) params vals))))
         (^[] body ...)
         (^[] (set! saves (map (^[p v] (p v #t)) params saves)))))]))

先の例は、今度は動く。

gosh> (define special
        (make-parameter 1 -))
special
gosh> (special)
-1
gosh> (define cc #f)
cc
gosh> (parameterize ((special -5))
        (call/cc (lambda (c) (set! cc c)))
        (special))
5
gosh> (cc #f)
5

……ほんとに?

gosh> (define a (make-parameter 1 (^x (unless (number? x) (error "!!")) x)))
a
gosh> (define b (make-parameter 2 (^x (unless (number? x) (error "!!")) x)))
b
gosh> (parameterize ((a 10) (b 'abc)) (list a b))
*** ERROR: !!
Stack Trace:
_______________________________________
  0  (error "!!")
        at "(standard input)":61
  1  (converter newval)
        at "(standard input)":43
  2  (map (^ (p v) (p v)) params vals)
        [unknown location]
  3  (^ () (if saves (set! saves (map (^ (p v) (p v #t)) params sa ...
        [unknown location]
  4  (eval expr env)
        at "/usr/share/gauche-0.9/0.9.5/lib/gauche/interactive.scm":282
gosh> (a)
10  ; 1に戻ってない

実装例8

実装例7では、動的束縛を逐次処理していたが、converter手続きがエラーを投げた場合、 それまでに変更した束縛を元に戻すチャンスが無かった。 converter手続きがエラーを投げる可能性を考えると、束縛を変更する前に 以前の値を保存しておき、エラーが出たら巻き戻すようにすればいけそうだ。

;; これは例7と同じ
(define (make-parameter init :optional (converter identity))
  (let1 val (converter init)
    (case-lambda
      [() val]
      [(newval)   (begin0 val (set! val (converter newval)))]
      [(newval _) (begin0 val (set! val newval))])))

(define-syntax parameterize
  (syntax-rules ()
    [(_ ((param val) ...) body ...)
     (let* ([params (list param ...)]   ;paramのリスト
            [vals   (list val ...)]     ;valのリスト
            [saves  #f]
            [restarted #f])
       (dynamic-wind
         (^[] (if restarted
                (set! saves (map (^[p v] (p v #t)) params saves))
                (set! saves (map (^[p] (p)) params)))) ; 初回の値を保存
         (^[]
           (unless restarted  ; 初回のみここで値をセット。エラーが起きたら巻き戻される
             (set! saves (map (^[p v] (p v)) params vals)))
           body ...)
         (^[]
           (set! restarted #t)
           (set! saves (map (^[p v] (p v #t)) params saves)))))]))

やってみよう。

gosh> (define a (make-parameter 1 (^x (unless (number? x) (error "!!")) x)))
a
gosh> (define b (make-parameter 2 (^x (unless (number? x) (error "!!")) x)))
b
gosh> (parameterize ((a 10) (b 'abc)) (list a b))
*** ERROR: !!
Stack Trace:
_______________________________________
  0  (error "!!")
        at "(standard input)":78
  1  (converter newval)
        at "(standard input)":43
  2  (map (^ (p v) (p v)) params vals)
        [unknown location]
  3  (eval expr env)
        at "/usr/share/gauche-0.9/0.9.5/lib/gauche/interactive.scm":282
gosh> (a)
1

めでたしめでたし。

……ほんとに?


教訓

ここに上げた経緯は、ほぼGaucheのparameterize実装がたどった経緯である。 パラメータオブジェクトそのものはスレッドローカルな値を保存するように 細工がしてあるが、parameterizeの現時点の実装はロジックとしては 実装例8にあげたものと同等になっている。今のところ問題は見つかっていないが、 これで完璧かどうかはわからない。

SRFI:39の参照実装のようにグローバルな動的環境リストを持つようにしていれば、 そちらの方が落とし穴が少なかったかもしれない。 (パラメータオブジェクトの値の参照に毎回環境リストを探さねばならないという点で deep bindingの欠点を引き継ぐことになるが。) その意味では、最初の方針が悪かったせいで回り道をしたとも言えるが、 ここからいくつか教訓を引き出すことは可能だと思う。

単純であっても落とし穴がないとは限らない

(p)で値の取得、(p v)で値の変更」という仕様は十分に単純に見える。 これを見たら多分真っ先にクロージャ内に現在の値を保持することを思いつくだろうし、 実装例2くらいまでならすぐに思い浮かびそうな気がする。

実際は、parameterizeを使わずに(p v)で直接値を変更できることが 落とし穴になっていた。動的束縛を実現したいのなら、必要なのはオブジェクトの 作成(make-parameter)、束縛された値の参照、および束縛の実現(parameter) の3点であって、どうやって束縛された値を変更するかというのは実装に任せておけば 良かったのである。

実際、R7RSではパラメータオブジェクトの値の変更はparameterizeのみによって 行うことになっていて、パラメータオブジェクトに引数を渡した時の振る舞いは未規定である。

複数の役割を持たせることの危うさ

make-parameterのconverter手続き、および(p v)での値が変更できること、 という仕様は、「制御されたグローバル変数」としての用途を念頭においていたようにも 思える。(p v)でグローバルに見える状態を変更できるが、 converter手続きによって少なくとも変な値がセットされることを防いだり、 いつ変更されたかをトレースすることができる。

ナイーブに考えると、make-parameterにconverter手続きを追加することは ちょっとしたハックに見える。 ちょっと変えればこっちの用途にも使えて便利じゃん、というノリだ。

実際は、converter手続きの存在によって、 パラメータオブジェクトの「値の変更」と「値のリストア」に別の操作が必要になってしまった。 元々、(p)(p v)だけあればその上に抽象を組める、と思っていたのに、 ちょっとしたハックがその前提を壊してしまったわけだ。

「制御されたグローバル変数」としてのパラメータはそれはそれで便利なこともあるので、 このデザインが全く誤りだとい言いたいわけではないが、 一見無害なちょっとしたハックが思わぬ影響を及ぼすことがある、という教訓にはなるだろう。

Tags: Scheme, Gauche, r7rs, srfi-39

More entries ...