“関数合成のススメ”の関数合成を Squeak Smalltalk で


メソッドを合成して新しいメソッドを生成するのはちょっと手ごわそうなのであとまわしにして、まずは簡単な無名関数を使ったバージョンから試します。

Smalltalk では無名関数はブロックと呼ばれ、式を [ ] で括ることで定義できます。

f := [3 + 4]


これを評価する際には value というメッセージを送ります。

f value   "=> 7 "


引数をとるブロックの場合は、次のようにコロンに続けて仮引数(変数名)を列挙し、| で手続き本体と区切ります。

add := [:x :y | x + y]


引数をとるブロックの評価には value ではなく、引数の数だけ value: を重ねたメッセージを送ります。

add value: 3 value: 4   "=> 7 "


余談ですが、この式でコールされるメソッドは #value:value: という名前の、(先の引数無しブロックの評価時にコールされる)#value とも #value: とも違う独立したメソッドである点は他の言語のユーザーはハマリポイントかもしれません。Smalltalk ではコロンも含めてメソッド名で(つまり #value と #value: 、もちろん #value:value: はそれぞれ別のメソッドで区別される)、それをコールするメッセージ式では引数をコロンの直後に挿入あるいは追加する特殊な書き方をする(block.value:value:(3,4) ではなく block value: 3 value: 4 と書く)ということはちょっと変わっているので Smalltalkうんちくとしてセットでまとめて覚えておくとよいでしょう。


さて本題ですが、リンク元Scala で書かれた in 、unlines 、putStr 、sort 、lines はそれぞれ(少々無理矢理)Smalltalk のブロックで記述すると次のようになります。

| in unlines putStr sort lines |

in := UIManager default
   multiLineRequest: ''
   centerAt: Display center
   initialAnswer: '3\2\1' withCRs
   answerHeight: 200.

unlines := [:seq | String streamContents: [:ss | seq do: [:str | ss nextPutAll: str] separatedBy: [ss cr]]].

putStr := [:str | World findATranscript: nil. Transcript cr; show: str].

sort := [:seq | seq sort].

lines := [:str | Array streamContents: [:ss | str linesDo: [:line | ss nextPut: line]]].

putStr value: (unlines value: (sort value: (lines value: in))).
1
2
3


ここで Scala の compose のようなことをするには、次のようにブロック(BlockClosure)に #<< というメソッドを定義します。

BlockClosure >> << other
   ^[:x | self value: (other value: x)]


メソッド名がたまたま #<< なのでちょっと読みにくくなってしまっていますが、冒頭の BlockClosure >> というのは BlockClosure クラスに以降に続くメソッドを定義するよ、という意味です。念のため、これはメソッドの所在を明確にするためのたんなる慣習にすぎず、文法とかの類ではありません。さらに余談ですが、Smalltalk にはメソッドを定義する構文がないので、クラスにメソッド本体(この場合 BlockClosure >> より後)の文字列を何らかの方法で(たとえばコンパイルをするためのメソッドの引数として、あるいはクラスブラウザのコード表示枠に入力し accept するなどして)渡してやる必要があります。


さて。この #<< を使うことで関数の合成が可能になり、(記号のみからなり、通常の言語の二項演算を真似た二項メッセージが value: in のような通常のキーワードメッセージより優先して評価されるルールも手伝って)括弧もなくなり、#value: のコールも一回で済むようになります。

putStr << unlines << sort << lines value: in


ただ Smalltalk では関数をブロックとして定義してこれに引数を与えて評価するという通常の言語の方法よりは、オブジェクトにメッセージを送って、オブジェクト自身(つまりメッセージのレシーバー)にそれが内包する適切な関数(メソッド)をコールさせるという方法をとります。

今回の例でなら、レシーバーは in に代入された文字列なので、それにまず lines というメッセージを投げ、その返値に sort、その返値に…というふうにしてそれぞれの返値にメッセージを順に送って結果を得るということをします。つまり適切なメソッドが定義されていれば、次のように書くのが通常ということになります。

in lines sort unlines putStr


そこでこんなふうにつぶやいたわけですが、

Smalltalkのメッセージ式だとstr lines sort unlines putStrと書ける(Rubyも同様)ので「括弧を減らすための関数合成」というとっかかりがつかめないでいるhttp://d.hatena.ne.jp/yuroyoro/20120203/1328248662 via https://twitter.com/#!/ukstudio/status/165325836357087232

@sumim


これに対して(だと思うのですが)次のようなサジェスチョンをいただきましたので、なるほどなとちょっと考え直してみることにしました。

関数合成と関数適用を分離して考える、ということを言いたかった

@yuroyoro


上にも書きましたが、in lines sort … では #lines 、#sort というメソッドが順次コールされてしまうので、まずこれらを合成してから in を引数としてコールするにはどうしたらよいかをひねり出してみます。

とりあえず in と #lines 、#sort 、#unlines の返値が属するクラスにその次にコールされるメソッドを定義しておきます。

SequenceableCollection >> unlines
   ^String streamContents: [:ss | self do: [:str | ss nextPutAll: str] separatedBy: [ss cr]]
String >> putStr
   World findATranscript: nil.
   Transcript cr; show: self
String >> lines
   ^Array streamContents: [:ss | self linesDo: [:line | ss nextPut: line]]


#sort は ArrayedCollection>>#sort としてすでにあるのでそれをそのまま使うことにしました。ちなみにこの「クラス名>>#メソッド名」という表記は当該メソッドオブジェクトを得るのにも使える Smalltalk式としても機能します(ただし Squeak Smalltalk とその影響を受けた一部の処理系限定。#compiledMethodAt: のほうが一般的)。

このメソッドオブジェクトを関数に見立て、通常(10 factorial)と違ってメッセージ送信によるメソッドサーチを介さずに静的にコールしたい場合には、Squeak Smalltalk では #valueWithReceiver:arguments: というメソッドを使うことができます。

| fact |
fact := Integer >> #factorial.
fact valueWithReceiver: 10 arguments: #()   "=> 3628800 "


この #valueWithReceiver:arguments: というメソッドが、すでに書いたブロック版における評価のための #value: と同じ役割を果たすので、ブロック版で f と g の合成である f value: (g value: x) は、メソッド版では次のように書けます。

f valueWithReceiver: (g valueWithReceiver: x arguments: #()) arguments: #()


#valueWithReceiver:arguments: のように長いメソッド名が二度出てくるとちょっと鬱陶しいので、#inject:into: でまとめてしまうのもよいかもしれません。

{g. f} inject: x into: [:val :func | func valueWithReceiver: val arguments: #()]


ただこういう処理をする(つまり合成した)無名のメソッドを返すメソッドをどう書くか、となるとなかなか難しいものがあります。まじめに書くと大変なので、ここは Smalltalk変態性 メリットを活かし、ダミーで生成したメソッドのノードをいじって目的のメソッドを生成させることでお茶を濁すことにします。

CompiledMethod >> << other
   | sourceString methodNode literalNode |
   sourceString := 'AnonMethod ^#(g f) inject: self into: [:x :func | func valueWithReceiver: x arguments: #()]'.
   methodNode := Compiler new
      compile: sourceString
      in: UndefinedObject
      notifying: nil
      ifFail: [].
   literalNode := (methodNode encoder instVarNamed: #litSet) at: #(g f).
   {other. self} doWithIndex: [:cm :idx | literalNode key at: idx put: cm].
   ^methodNode generate


具体的には、'AnonMethod #(g f) inject: self into: [:x :func | func valueWithReceiver: x arguments: #()]' という文字列をコンパイラに渡して生成したメソッドノード(抽象構文木)から、#(g f) に相当するリテラルノードを手繰って、本来シンボルの #g と #f であるこのノードが参照している先を強制的に other と self(つまり f << g なら g と f )に置き換えてしまいます。それからその改変メソッドノードを使ってバイトコード列(すなわちメソッドオブジェクト)を生成してそれを返しています。


こんな #<< を定義することで、たとえば 10 factorial negated という式でコールされる #factorial と #negated という二つのメソッドに対して次のような合成後適用ができるようになります。

| fact nega |
fact := Integer >> #factorial.
nega := Number >> #negated.
(nega << fact) valueWithReceiver: 10 arguments: #()   "=> -3628800 "


わかりやすいように nega << fact は括弧でくくっていますが、前述の通り Smaltalk では二項メッセージ式は通常のキーワードメッセージ式より優先順位が高いので、この括弧は無しでも問題ありません。


ついでに今は #valueWithReceiver:arguments: の第二引数は無用なので #valueWithReceiver: というちょっと短いバージョンも定義しておきましょう。

CompiledMethod >> valueWithReceiver: rcvr
   ^self valueWithReceiver: rcvr arguments: #()
| fact nega |
fact := Integer >> #factorial.
nega := Number >> #negated.
(nega << fact) valueWithReceiver: 10   "=> -3628800 "


ということでお膳立てがすべて整いましたので、あとは各々のメソッドをブロックの代わりに同名のテンポラリ変数に代入し、#value: の代わりに #valueWithReceiver: を使って評価するように変えれば完成です。

| in unlines putStr sort lines |

in := UIManager default
   multiLineRequest: ''
   centerAt: Display center
   initialAnswer: '3\2\1' withCRs
   answerHeight: 200.

unlines := SequenceableCollection >> #unlines.

putStr := String >> #putStr.

sort := ArrayedCollection >> #sort.

lines := String >> #lines.

(putStr << unlines << sort << lines) valueWithReceiver: in
1
2
3