On this page:
4.1 What’s pipe  R?
4.2 Syntax:   %>%
4.3 pipe  R.inc
4.4 require & include
4.5 Syntax Transformer
4.6 syntax-case
6.8

4 pipeR on Racket

4.1 What’s pipeR?

プログラミングをしていると、時に黒魔術的なことをしたくなるものです。元来、Lisperは黒魔術が好きであり、自由度の高い言語に「ハッカー」が集まるのは自然な流れです。

一般的にはソースコードが読みにくいとされるPerl言語なども黒魔術に適した言語の一つであり、私が気に入っているのは特殊変数の$_です。これは、変数への代入式を記述すべき箇所で代入を記述していない場合、自動的にこの変数に代入される、というものです。

一般には$_のスコープの問題などから「使うべきではない」とするWebサイトなども多いですが、もし$_で済むならあえて他の変数をわざわざ記述する必要はなく、まさに「ハッカー」向きの黒魔術の一つだと思います。

また、統計専門のDSLであるR言語にはmagrittrpipeRというライブラリがあり、R言語にUnixのパイプのような動作を導入するという大変便利な機能を提供してくれます。これは、パイプ演算子%>%を導入し、これで繋がれた関数の第一引数に前の式の結果を代入していく、というものです。つまり、関数の評価結果を標準出力に出力し、関数の第一引数を標準入力に接続して、%>%が標準出力と標準入力を繋ぐパイプの役割を果たす、というものです。

これは使ってみると大変便利で、処理が左から右へ、上から下へと流れるように記述できます。

Lispを使っていて一番不便に思うのがこの「左から右へ、上から下へ」という自然な目の動きと実際の式が大きく異なるという点です。今回は、この問題に対して、一般にはあまり推奨されないような方法で、堅牢な言語環境であるRacketに黒魔術的な風穴を開けてみたいと思います。

4.2 Syntax: %>%

例えば、((1 + 3) * 2) / 5を計算することを思い浮かべてください。これを日本語で表すと、以下のようになります。

  1. 13を加算する

  2. 「その結果」に2を乗算する

  3. 「その結果」を5で除算する

計算には常に「継続」が存在します。「継続」とは、この計算例における「その結果」を得たあとの全ての計算であり、複雑な計算式も「その結果」を受け渡しながら処理していきます。

しかし、この計算式をLISPで表すと、以下のようになります。

(/ (* (+ 1 3) 2) 5)

計算の順序と演算子(関数)の順序が、全く逆になってしまいます。これは、前置記法の最大のデメリットと言ってもよく、左から右へという流れる計算をうまく表現できません。

そこで、黒魔術です。

これを、以下のように書けるようにします。

(%>% (+ 1 3)
     (* $_ 2)
     (/ $_ 5))

言葉で説明したものと全く同じようになります。R言語のパイプ演算子%>%と、Perl言語の特殊変数$_を組み合わせて、%>%の中では手前の計算結果を$_で参照できるようにします。

4.3 pipeR.inc

まず、この擬似パイプを実装しますが、実はたった5行で実装できます。

(define $_ 0)
(define-syntax %>%
  (syntax-rules ()
    ([_ x] (begin (set! $_ x) $_))
    ([_ x y ...] (begin (set! $_ x) (%>% y ...)))))

このデータを記述したファイルを"pipeR.inc"として保存してください。このファイルにはシャープラング(#lang)を付けず、本当にこの5行だけにしてください。

そして、このパイプを使う側ではこの2行を挿入してください。

(require racket/include)
(include "pipeR.inc")

これだけで、上記のパイプが使えます。

4.4 require & include

Racketではモジュールのロードを一般にrequireで行います。Racketではファイルがモジュールとして扱われ、名前空間もモジュール単位で独立します。これは、一般には名前の衝突を防ぐための良い方法です。

しかし、今回のように名前空間を汚したい時もあります。この例では$_というシンボルを参照したいので、モジュールを分けてしまうと、容易には参照できません(参照する方法もあります)。

このような時のために、C言語のincludeと同じようにソースコードの単純な挿入だけを行うincludeが用意されています。includeを使うと分割したファイルから一つのモジュールを作ることができるので、実際には名前空間を汚している訳ではなく、そのモジュール内で$_という変数と%>%という構文を定義していることになります。

Common Lispでは「アナフォリックマクロ」(前方参照型マクロ)を簡単に作れますが、パッケージを分けるとアナフォリックマクロも少し難しくなります。includeのイメージとしては、Common Lispのloadと同じように、とてもシンプルな仕組みで、黒魔術というほどのものもありません。

R言語のパイプ演算子が活躍しているように、統計では一連の流れの中で計算を行うことがよくあります。ぜひ、色々と試してみてください。

4.5 Syntax Transformer

(追記)この例は、構文トランスフォーマーを自作することでも定義できます。(この記述の場合はシャープラングをつけてください。)

(require (for-syntax racket/base))
(define-syntax %>%
  (lambda (stx)
    (datum->syntax
     stx
     `(let ([$_ 0])
        ,@(map (lambda (x) `(set! $_ ,x)) (cdr (syntax->datum stx)))
        $_))))

Syntax Transformerの実体はlambdaであり、define-syntaxは構文オブジェクトを引数に取るlambdaを直接指定することで構文を指定することもできます。

4.6 syntax-case

(追記)今回の例ではパターンマッチングによる構文の分解を行いませんでしたので、前述の記述例が一番スマートかと思いますが、パターンマッチングを行うsyntax-caseを使って記述する場合は、以下のようになります。

(define-syntax %>%
  (lambda (stx)
    (syntax-case stx ()
      ([k x ...]
       (datum->syntax
         #'k
         `(let ([$_ 0])
            ,@(map (lambda (x) `(set! $_ ,x)) (cdr (syntax->datum stx)))
            $_))))))

なお、Racketの場合はdefineと同じようにdefine-syntaxもトランスフォーマーであるlambdaを省略するシンタックスシュガーが用意されていますので、最終的に一番綺麗な実装は以下のようになるのではないかと思います。

(define-syntax (%>% stx)
  (datum->syntax
   stx
   `(let ([$_ 0])
      ,@(map (lambda (x) `(set! $_ ,x)) (cdr (syntax->datum stx)))
      $_)))