誰得かわからないSqueak Smalltalkを使った関数型プログラミングっぽい話
当初、誰向けかわからないCommon Lispでの関数型プログラミング入門とその未来 - 八発白中 に例にあがっているコードを Squeak Smalltalk ならどう書けるかというような、少しは有益なことを書こうかと思っていたら、のっけからネタに走ってしまって失敗しました。ごめんなさい。
▼関数の定義とコール(ネタ)
Smalltalk ではまずクラスを作り(その際、インスタンス変数などを宣言し)メソッドを登録することで、そのクラスのインスタンスであるオブジェクトの振る舞いを決める、というやりかたをします。しかし「メソッド」などといったところで、クラスに属しているだけでただの関数です。
通常であればこの“関数”は、オブジェクトに「メッセージ」と称される「関数名(セレクターとも言う)と引数」を組にした情報*1を送ることで動的にコールされますが、もし静的コール*2もできればもっと普通に関数として運用可能なはずです。
処理系ごとに方法が異なりますが(可不可も含め)、実は Smalltalk でも関数の静的コールは可能だったりします。たとえば Squeak Smalltalk であれば、メソッドの定義(とクラスへの登録)とその実体の取得、静的コールはそれぞれ次のように書いて実行できます。
"関数の定義とクラスへの登録 = 関数を定義したいクラスにコード文字列をコンパイルさせる" Number compile: 'foo ^self' "<= レシーバーを返す関数 #foo を定義し、仮に Number に登録 ". "関数の取得" (Number >> #foo) "=> コンパイルされた関数の実体の取得。なお #〜 はシンボルのリテラル式 ". "関数の静的なコール" (Number >> #foo) valueWithReceiver: 'string' arguments: #() "=> #foo を静的にコール ".
静的なコールなのでいわゆるオブジェクト指向プログラミングの文脈からは外れ、クラス(この場合Number クラス)は単なる関数の置き場所に過ぎません。実際、Number とは縁もゆかりもない文字列である 'string' をレシーバー(もちろんこの呼び名も今や意味をなしておらず、「第一引数に」と読み替えてよい)にして #foo メソッドをコールできている点に注目してください。お手元に処理系があれば、上の最初の式を評価する(alt/cmd + d か、右クリックメニューから do it )かクラスブラウザを使って Number>>#foo を定義後、最後の式を評価(alt/cmd + p か、右クリックメニューから print it )すると、レシーバーとして与えた 'string'(実行コンテキスト内では self )がちゃんと返ってきます。
もし 'string' に対して foo というメッセージを送って、#foo を通常通り動的にコールしようとしても、Smalltalk で唯一のエラーともいえる「そんなメッセージ投げられても、何すりゃいいのかわかんねーぞ!」エラーを吐きます。そもそも 'string' のクラスやそのスーパークラス群は Number にある関数 #foo のことなど何一つ知るはずもないので、当たり前といえば当たり前ですね。
'string' foo "=> MessageNotUnderstood: ByteString>>foo "
試しに階乗を求める #fact も定義してみましょう。どうせ静的にコールするため、関数の置き場所はどこでもよいので、今度は String に登録してみます。
String compile: 'fact self < 2 ifTrue: [^self]. ^self * (String >> #fact valueWithReceiver: self-1 arguments: #())'. (String >> #fact) valueWithReceiver: 10 arguments: #(). "=> 3628800 "
ちゃんと答えが返ってきますね。ただ、ここまで書いていまさらですが、関数のコールに valueWithReceiver: 10 arguments: #(). とかいちいち長たらしくていけません。
▼関数の定義とコール(仕切りなおし)
そんなわけもあってかなくてか(←ないです)、Smalltalk では通常、関数を単体で定義するにはブロックという無名関数オブジェクトを用います。定義はゼロ個以上の式を [ ] で括るだけです。名前を付けたいときは、別途、変数を宣言してそれを束縛してください。
| func | func := [:a :b | a + b]. func value: 3 value: 4 "=> 7 "
引数がなければそのまま関数本体を記述し、引数があるときは「:引数名」を列挙して最後に | で区切って関数本体の記述を続けます。
引数のない関数では「value」を、引数がある関数では「value: 引数 」を引数の数だけ繰り返して構成したメッセージを関数に送ることでコールできます*3。value とか value: 3 value: 4 とか通常の言語の関数コールの記述より長たらしくていけませんが、(Integer >> #+) valueWithReceiver: 3 arguments: #(4) とかいちいち書くよりは少しはマシなので我慢しましょう。
再帰も書けます。が、再帰的呼び出しのために変数名が未定義であると評価するたびにコンパイラに指摘されるのがウザイです。
| fact | fact := [:n | n < 2 ifTrue: [n] ifFalse: [n * (fact value: n-1)]]. "=> fact appears to be undefined at this point Proceed anyway? " fact value: 10 "=> yes を選べば、普通に 3628800 を返してくる "
表示されたポップアップでメニューで yes を選べば、警告を無視して(他にエラーがなければ)コンパイルは完了し、普通に走らせることはできるのですが、毎回それをするのは面倒なので、再帰関数を定義したいときは、いったん関数名にしたい変数を nil などに明示的に束縛しておき、直後に改めて関数本体を再代入するとよいようです。
| fact | fact := nil. fact := [:n | n < 2 ifTrue: [n] ifFalse: [n * (fact value: n-1)]]. fact value: 10 "=> 3628800 "
なお、末尾再帰最適化とかはないです。が、コールスタックはメモリの許す限り積めるので、メモリを食いつぶしたな…と気付く時点(メモリ不足のアラートが出るか、VM ごと落ちる)で、きっとそれはなにか意図しないことが起こっているか、意図したとおりに動いていたとしても現実的な時間では終わらないコードであることが多いです。
ブロックの返値としては最後の式の結果が使われます。メソッドと違い、ブロックでは ^ を用いて関数の途中で抜けることができません。オーソドックスには、この階乗の例のように条件分岐などをネストする必要があります。
Squeak Smalltalk や VisualWorks(Cincom Smalltalk)では BlockClosure>>#valueWithExit が使えるので、この手のカラクリを活用するのもよいでしょう。条件分岐だけでは対応しにくいネストしたループ内からの脱出とかも可能です。ただデフォルトの #valueWithExit の定義のままでは値を持って抜けることができないので、返値を持って回る変数を用意するなどの工夫が必要です。
| func | func := [:stop | | result | [:exit | 1 to: Float infinity do: [:m | m = stop ifTrue: [result := m. exit value] ] ] valueWithExit. result]. func value: 100 "=> 100 "
▼高階関数
よくある map の役割は Smalltalk では #collect: というメソッドが担当します。
#(1 2 3 4 5) collect: [:x | x + 1] "=> #(2 3 4 5 6) "
[:x | x + 1] は無名関数でこれは第一級オブジェクトなので、変数に代入したり、あらためてそれを関数コール時に引数として渡すこともできます。*4
| plusOne | plusOne := [:x | x + 1]. #(1 2 3 4 5) collect: plusOne "=> #(2 3 4 5 6) "
さらに Squeak や Pharo では、単項メッセージのセレクター(シンボル)を渡すこともできます。
3 squared "=> 9 "
#(1 2 3 4 5) collect: #squared "=> #(1 4 9 16 25) "
内部的にはシンボルを同名の単項メッセージとして各要素に送ってその返値を得ているだけなので、Lisp などでシャープクオートマクロを使って関数を得ているのとは意味が違いますが、見た目はそれっぽくてかっこいいです。
あと、高階関数の話からは逸れてしまいますが、Squeak や Pharo の場合さらに、たとえば #squared のように Collection に定義されているメソッドならそのまま配列などのコレクションオブジェクトに対してメッセージとして送信、同名メソッドを動的にコールすることも可能です。
#(1 2 3 4 5) squared "=> #(1 4 9 16 25) "
参考まで、同様のことができるメソッド名(セレクター)を列挙するコードとその結果を示します。
#('math functions' 'arithmetic') inject: #() into: [:acc :cat | acc, (Collection allMethodsInCategory: cat)]
#(abs arcCos arcSin arcTan average ceiling cos degreeCos degreeSin exp floor ln log max median min minMax negated range reciprocal roundTo: rounded sign sin sqrt squared sum tan truncated * + - / // \\ raisedTo:)
Smalltalk では reduce は #inject:into: を使います。
#(1 2 3) inject: 0 into: [:sum :x | sum + x] "=> 6 "
もちろんこの場合は #+ でもOKです。
#(1 2 3) inject: 0 into: #+ "=> 6 "
最近のバージョンの Squeak や Pharo には、inject: キーワードの引数として初期値を与える代わりに、レシーバーの第一要素を用いるバリエーションとして #reduce: が用意されています。
#(1 2 3) reduce: #+ "=> 6 "
これまた余談ですが、Squeak や Pharo には Collection>>#sum があるので reduce: #+ に限っては無用です。
#(1 2 3) sum "=> 6 "
{Color red. Color green. Color blue} sum "=> Color white "
無名関数や #reduce: を(あえて)使って sum や factorial を定義すると例えばこんなふうに書けます。
| sum | sum := [:xs | xs reduce: #+]. sum value: #(1 2 3 4). "=> 10 " sum value: (1 to: 100). "=> 5050 " sum value: {Color cyan. Color magenta. Color yellow}. "=> Color white "
| factorial | factorial := [:x | (1 to: x) reduce: #*]. factorial value: 10. "=> 3628800 "
ところで Common Lisp の mapcar は複数のリストをとってこんなことができます。
(mapcar #'list '(1 2 3) '(a b c) '(松 竹 梅)) ;=> ((1 A 松) (2 B 竹) (3 C 梅))
が、Smalltalk の #collect: はこうした凝ったことはできません。そもそも、Smalltalk には文法の制約から可変長引数というしくみ自体がありません。
もはや関数型ぜんぜん関係なくなっちゃいますが、上の Common Lisp のコードの動きを想像して Squeak や Pharo で手続き的に書くとこうなります。
(#((1 2 3) (a b c) (松 竹 梅)) collect: #readStream) in: [:ss | ss collect: [:x | ss collect: #next]]
=> #(#(1 #a #'松') #(2 #b #'竹') #(3 #c #'梅'))
▼関数合成
| compose plusOne | compose :=[:fs | fs reduce: [:g :f | [:arg | f value: (g value: arg)]]]. plusOne := [:x | x + 1]. #(1 2 3 4 5) collect: (compose value: {plusOne. #sin})
=> #(0.909297426825682 0.1411200080598672 -0.756802495307928 -0.958924274663138 -0.279415498198926)
▼conjoin & disjoin
| cojoin | cojoin := [:fs | fs reduce: [:g :f | [:arg | (f value: arg) and: [g value: arg]]]]. (cojoin value: #(isZero isInteger)) value: 0 "=> true ". (cojoin value: #(isZero isInteger)) value: 0.0 "=> false ".
| disjoin positive | disjoin := [:fs | fs reduce: [:g :f | [:arg | (f value: arg) or: [g value: arg]]]]. positive := [:x | x > 0]. (disjoin value: {positive. #negative}) value: 100 "=> true ". (disjoin value: {positive. #negative}) value: 0 "=> false ".
| complement | complement := [:f | [:x | (f value: x) not]]. (complement value: #isZero) value: 0 "=> false ".
▼まとめ
- Smalltalk のメソッドは、通常は「メッセージ」を介して動的にコールされるが、静的にもコールできる
- Smalltalk のメソッドの実体を手繰って静的にコールすれば普通に関数として使えなくはないが、いろいろめんどくさい
- Smalltalk で関数を使いたければ、メソッド実体より普通にブロック(無名関数)を使うほうがいい つーか、使うべき
- Smalltalk のブロックリテラル [〜] は Commpn Lisp の (lambda 〜) より lambda と書かなくていいぶん短くすっきり
- でも呼び出しは value とか value: 3 value: 4 とかの長ったらしいメッセージの送信なのでプラマイゼロ、むしろマイ
- 素の Smalltalk のやり方にも、関数型プログラミングっぽい側面はある
- Squeak や Pharo にある、ブロックの代わりにシンボルを渡せる機能は、さらに関数型っぽさを醸し出す
- かなり gdgd になったが気にしない
*1:たとえば 3 + 4 なら 3 へ + 4 というメッセージの送信を意味し、+ がセレクタ、4 が引数です。4 between: 3 and: 5 なら between: 3 and: 5 がメッセージ、between:and: がセレクタ、3 と 5 が引数になります。メソッド名はコロンも含まれ、そのコロンの直後で分断してそこに引数を挿入してメッセージを構成するところが、通常であれば 4.between:and:(3,5) のように記述するであろう他の言語と Smalltalk のメッセージ式とが違うところです。
*2:ここでは呼びたいメソッドを定め、ずばりそれをコールするという意味で使っています。
*3:内部的には、#value: #value:value: #value:value:value: はそれぞれ独立した別個のメソッドとして定義されています。#value:value:value:value: まで用意されていて、それ以上の引数が必要な場合は #valueWithArguments: を使い、引数は配列の要素として渡します。
*4:ただし条件分岐(たとえば #ifTrue:ifFalse:)など一部のメソッドはコンパイル時にインライン展開されるため、変数を介してブロックを間接的に渡すことができないこともあります。