アホが書き散らすFactor入門 その8
長らくおまたせしました。
(待ってない?)
クラスに行こうと思ったのですが、最近関数合成のwordがkernelに昇格して
たりするので今回は関数の合成をやりたいと思います。
関数の合成
基本的にFactorはローカル変数を持ちません。
そのため複雑な処理を行う場合には関数の結果をそのまま別の関数に渡してつ
ないでいくスタイルになります。
:f ( x -- y ) 1+ ;
:g ( x -- y ) dup * ;1 f g .
4
あるいは
1 [ 1+ ] call [ dup * ] call
Factorのプログラムは上記のようにwordをひたすら続ける、すなわち大きな合
成関数を作成する事で実現されています。
上記の例はシンプルですがquotの場合だとcallを呼んでやらないといけないし、
複数の引数をケースもあったりと実際にはそう単純ではありません。
そこでFactorでは簡単に合成関数を作成、実行するwordを用意しています。
それらはkernel vocabularyのwordになっています。
(0.91ではcombinators vocabulary)
cleave combinators
cleave combinatorsは複数のquotをひとつの値にまとめる場合に使います。
以下のようなwordがあります。
- bi ( x p q -- )
- 2bi ( x y p q -- )
- 3bi ( x y z p q --)
- tri ( x p q r -- )
- 2tri ( x y p q r -- )
- 3tri ( x y z p q r -- )
代表的なbi wordを例に説明します。
biですが別の書き方をすると以下のようになります。
以下は等価になります。
[ p ] [ q ] bi
dup [ p ] [ q ]
抽象的でわかりにくいかも知れません。
biはobj xを引数にquot p を実行しその後、元のobj xを引数にquot
qを実行します。
ではquot p、qが結果を返すquotである場合はどうなるのでしょう。
quot p 、qのStack Effectが( x -- y )の場合は以下のようになります。
[ p ] [ q ] bi
dup p swap q
わかりやすくすると
1 [ 1+ ] [ 1+ ] bi .s
2
2
このようにp,qに同じ値obj xが渡たされます。
ではこの場合はどうなるでしょうか。
1 [ 1+ ] [ + ] bi .
この場合は最終的に3がスタックに積まれます。
Stack Effectが( x -- y )でない場合 quot pが結果をスタックに積んでいれば
quot qには先にquot pの結果が渡る点に注意してください。
多くの場合、quot qは2つの引数を取る、すなわち元のobj xとquot pの結果を
とる使い方をします。
2biはquotにわたる引数が2つ、3biは3つと増えていくだけです。
2biの場合、以下は等価です。
(2dup ( x y -- x y x y ))
[ p ] [ q ] 2bi
[ p ] 2keep [ q ]
triはquotの数が増えているだけです。
spread combinators
spread combinatorsはcleaveと違い複数の値を返します。
(そもそもスタック指向なので複数の値もクソもないんでしょうけど)
cleaveの別実装の意味もこめて*がついています。
spread combinatorsには以下のようなwordがあります。
- bi* ( x y p q -- )
- 2bi* ( w x y z p q -- )
- tri* ( x y z p q r -- )
ここではbi*を例にして説明していきます。
bi*は以下のように書きなおす事ができます。
[ p ] [ q ] bi*
>r p r> q
1 2 [ 1+ ] [ 1+ ] bi* .s
2
3
bi*はobj xを引数にquot p、obj yを引数にquot pを実行します。
biよりもこちらの方がわかりやすいかも知れません。
こちらも2bi*、tri*は引数の数が増えているだけで原理は同じです。
apply combinators
apply combinatorsは名前から推測できるようにあるquotに複数の値を
適用させるcombinatorsです。
以下のようなwordがあります。
- bi@ ( x y quot -- )
- 2bi@ ( w x y z quot -- )
- tri@ ( x y z quot -- )
またutility的なworに以下のようなものがあります。
- both? ( x y quot -- ? )
- either? ( x y quot -- ? )
ここでは基本的なbi@を見てみます。
bi@は以下のように書き直すことができます。
[ p ] bi@
>r p r> p
あるいは
[ p ] bi@
[ p ] [ p ] bi*
になります。
わかりやすく例を書くと以下のようになります。
1 2 [ 1+ ] bi@ .s
2
3
bi@は引数を2つとり、それぞれ引数で渡したquotに渡しています。
こちらはシンプルですね。
2bi@、tri@も引数が増えただけです。
またboth?、either?はquotの適用結果をand、orで評価してboolで返します。
3 5 [ odd? ] both? .
t
12 7 [ even? ] both? .
f
either?はorなので片方でもtであればtを返します。
slip combinators
スタック指向独自のcombinatorsと言えるでしょう。
これらは死ぬほど使われているので覚えておかないと追いかけるのがしんどいです。
retain stackを使う基本的な部分なのですがこの辺はスタック指向でつまづく点の
ひとつなので慣れておきましょう。
slip系には以下のwordがあります。
- slip ( quot x -- x )
- 2slip ( quot x y -- x y )
- 3slip ( quot x y z -- x y z )
slipの亜種系にはdipがあります。
- dip ( obj quot -- obj )
keep系には以下のwordがあります。
- keep ( x quot -- x )
- 2keep ( x y quot -- x y )
- 3keep ( x y z quot -- x y z )
まずはslip系から説明します。
ここではslipを例に説明します。
slip wordはスタックのtopの値を隠してから実行します。
slip wordは以下のように定義されています。
: slip ( quot x -- x ) >r call r> ; inline
わかりにくいので簡単な例を見てみます。
4 [ 1+ ] 2 slip .s
5
2
パッと見なんのこっちゃなので、スタック状態を順にみてみます。
slipが実行される直前の状態は以下のようになります。
4
[ 1+ ]
2
stackの一番上が2です。
このスタックを見るとslipはスタックの一番上の値をkeepしてquotを実行してる
ことがわかります。
2slip、3slipもスタック消費しないobjの引数が増えただけで原理は同じです。
さらによく使われているのがdipです。
dipはslipの亜種と言っても構わないでしょう。
dipは以下のような定義になっています。
: dip ( obj quot -- obj ) swap slip ; inline
dipの引数に注目です。
slipが( quot x -- x )に対し、dipは( obj quot -- obj )になっている点です。
さらに定義を見ます。
swap slip
となっている点です。
要はslipを適用するためにswapでスタックを入れ替えているだけです。
スタック指向上slipよりもdipがよく使われます。
スタック指向では前の処理の結果はスタックに積みあがります。
そのため結果を受けて処理したい場合は引数の先頭がobjになっているwordの方が都合
が良いのです。
(逆にそうなっていないとしんどい)
keepはdipに近いですが値をkeepしています。
keepの定義は以下のようになっています。
: keep ( x quot -- x ) over slip ; inline
over は ( x y -- x y x )でxをコピーします。
keep上ではyはquotなのでy x がslipに渡ります。
yが実行されるときの引数は元々あったxなのでkeepの結果は
x [ y ] call
x
のようなスタック状態になります。
keepはquotを実行しつつ、元のstackの値も保持する(keepする)wordという事が
わかります。
2keepなどはkeepの引数が増えただけです。
とりあえずざっくり説明しました。
今回のcombinatorsはFactorでは空気のように使われているので慣れてないとなかな
かしんどいものがあります。
とりあえずretain stackに突っ込んで評価順を調整するという基本テクニックは身
につけておきましょう。
次回はやっとクラスとかその辺に行きます。