(Racket) composeマクロ
composeマクロを作る
関数合成を行うcomposeは通常関数で提供されます。それをマクロで作るとどうなるかという話。昨日即興で書いた物が全然ダメダメだったので再挑戦です。使用するのは syntax-rules。
compose1 と compose
Racketには2つのcompose関数があります。違いは合成対象関数が多値を返す事を許可するかしないかです。
compose1
合成対象の関数間で多値を受け渡すことはできません。compose1の1は対象関数の返却値が1つであることを意味します。返却値が1つという事はその後に続くすべての関数は必ず1つの引数へ適用され1つの値を返す事になります。ただし最初に評価される関数だけは複数の引数を渡すことができ、最後に評価される関数だけは多値を返すことができます。
compose
合成対象の関数間で多値を受け渡すことができます。関数が多値を返すとその多値を可変長の引数として次の関数に渡します。call-with-valuesを挟むような動作になります。
書いたマクロ
https://gist.github.com/mnzk/6b785fd80c129c20b237#file-mycompose-rkt
それぞれ再帰部分をヘルパに切り出し、ヘルパの結果をラムダ式で包んでいます。ラムダの引数リストはリストのまま再帰の最深部まで送っています。
(define-syntax mycompose1 (syntax-rules () ((_) values) ((_ f1 f2 ...) (lambda arglist (mycompose1$ arglist f1 f2 ...))))) ;; helper (define-syntax mycompose1$ (syntax-rules () ((_ arglist f) (apply f arglist)) ((_ arglist f1 f2 ...) (f1 (mycompose1$ arglist f2 ...)))))
(define-syntax mycompose (syntax-rules () ((_) values) ((_ f1 f2 ...) (lambda arglist (mycompose$ arglist f1 f2 ...))))) ;; helper (define-syntax mycompose$ (syntax-rules () ((_ arglist f) (apply f arglist)) ((_ arglist f1 f2 ...) (call-with-values (thunk (mycompose$ arglist f2 ...)) f1))))
これを見ると何故二つに分けているかが分かる気がします。compose1で済む場合はこちらを使った方がcall-with-valuesのオーバーヘッドを省略できます。*1
lambdaの引数リスト
lambdaの引数はリストの形ではなく丸ごと仮引数にすることができるんですね。
((lambda lis lis) 42) ;=> (42)
普通(?)可変長引数はドットリストの形で書くと思います。
(lambda (x . more) <body>)
でも、これだと引数ゼロ個が表現できません。composeでは(実用上意味はありませんが)引数ゼロ個の関数へ適用することもできますのでこのドットリスト形式では都合が悪いのです。引数リストを丸ごとシンボルにすることでゼロ個を含む可変長を表現することができました。
マクロだからマクロに適用できる?
composeをマクロ化すると関数だけではなくマクロや特殊形式オペレータの合成にも使えるのではないかと期待しました。が、この実装ではapplyを使っているので関数しか対象にできません。
もっとも applyが適用されるのは最初に評価される関数だけなのでそれ以外の場所ならばマクロや特殊形式を挿入することは可能です。
(define-syntax inc (syntax-rules() ((_ x) (+ 1 x)))) ((mycompose1 inc inc inc identity) 1) ;=> 4
新たな制限が加わりますが一応はマクロの合成もできています。ただしこれはmycompose1だけ可能な事で、mycomposeのほうはcall-with-valuesを再帰の度に挿入しているのでそう簡単にはいきません。いろいろ試行錯誤してみると、今度は逆に lambdaの引数リストをひとまとめにしていることがネックになって行き詰まってしまいます。
こんな風にパターンマッチできればあるいは・・・
(define-syntax mycompose (syntax-rules () ((_) values) ((_ f1 f2 ...) (lambda (a ...) (mycompose$ (a ...) f1 f2 ...)))))
そもそもこれが無理ですが。
*1:実際のRacketの実装は読み解いていませんが