倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Clojure のマクロって Groovy で書くとこんな感じじゃないの?

Clojure やっててマクロのあたりの構文クォート、アンクォート、スプライシング・アンクォートあたりが全然分かんなかったので、無理矢理 Groovy っぽく解釈してみました。 サンプルの Groovy コードはかなり力ずくで、(動作はするけど)実際に使えるコードではありませんが、Clojure マクロの理解の助けになれば幸いです。

構文クォート、アンクォート


とりあえず簡単なマクロから。 『プログラミング Clojure』に載っているマクロ「unless」を拝借:

(defmacro unless [expr form]
  `(if ~expr nil ~form))    ; マクロの本体

(unless false (println "this should print"))    ; 使用例1
(unless true (println "this should not print"))    ; 使用例2
  • unless は最初の引数を評価して false なら次の引数を実行します(if の逆)
  • 最初の2行が unless マクロの定義です
  • 最後2行は unless の使用例

このコードを実行すると以下のように表示されます:

this should print

これは unless の使用例1の実行によって表示されます。 使用例2は何も表示しません。

さて、マクロを定義する際にややこしいのが構文クォート (syntax-quote `) 、アンクォート (unquote ~)、スプライシング・アンクォート (unquote-splicing ~@)でしょう。 スプライシング・アンクォートは後で見ることにして、まずは最初の2つを見てみましょう。 『プログラミング Clojure』から説明を拝借すると

構文クォート
クォートと似ているが、フォームの一部をアンクォートすることができ、フォームをテンプレートとして使える。
アンクォート
構文クォートの中で使われると、アンクォートのある部分を form で置き換える。

だそうです。 うむ、よく分からん。 とりあえず、上記のサンプルではこの2つを使ってます(構文クォート `(if とアンクォート ~expr, ~form)。

Groovy で似た機能がないかなと考えてみると、GString がなんか使えそうな感じかなぁ、ということで

  • 構文クォート (`) → GString ("...")
  • アンクォート (~) → GString のプレースホルダー (${...})

という対応で Clojure マクロを Groovy のコードに焼き直してみましょう:

shell = new GroovyShell()

def unless(expr, form){
    def code = "if (!$expr) { $form }"    // マクロの本体に対応
    
    println "// $code"
    shell.evaluate(code)
}

unless('false', 'println "this should print"')    // 使用例1
unless('true', 'println "this should not print"')    // 使用例2
  • unless() メソッドの引数は内容が Groovy コードの文字列となってます。 変数やクロージャではありません
  • unless() メソッドはマクロに対応する Groovy コードを文字列として作成し(code 変数)、GroovyShell によってコンパイル & 実行しています
  • マクロの処理に対応する、生成された Groovy コードも表示されるようにしてます(前に // をつけて表示)

Clojure マクロの本体「`(if ~expr nil ~form)」は GString 「"if (!$expr) { $form }"」となっています*1。 こうやって見ると結構きちんと対応してますな。 ただ、悲しいのが unless メソッドに渡しているのが「内容が Groovy コードの文字列」だってところ。 現実に使えそうにねぇ(笑) このあたり、ClojureHomoiconicity が光るところでもあるかな(ちょっと専門用語使ってみたw)。

ちなみにサンプルコードを実行すると

// if (!false) { println "this should print" }
this should print
// if (!true) { println "this should not print" }

と表示されます。 コード確認のための出力(// で始まる)以外は、上記の Clojure マクロと同じ出力が得られます。 ヨシヨシ。

ちょっと修正


ヨシヨシ、うまくいった・・・と思ってたら、早速以下のようなコードが動かない:

x = false
unless('x', 'println "this should print"')

まぁ、当然ですが。 これは unless() メソッドの中で GroovyShell によってコンパイル & 実行する際に変数 x にアクセスできないため。 まぁ、あんまり本筋の話とは関係ないので、限定的だけど簡単な修正で済ませましょう:

shell = new GroovyShell(this.binding)    // 変数を参照可能に

def unless(expr, form){
    def code = "if (!$expr) { $form }"
    
    println "// $code"
    shell.evaluate(code)
}

x = false    // 変数を定義(def をつけると動かないヨ)
unless('x', 'println "this should print"')
  • GroovyShell のインスタンス生成時に、スクリプトのバインディング(参照可能な[変数名:値]のマップ)を渡しています
  • 参照したい変数(上記の例では x)を定義する際は def をつけないでね

これを実行すると

// if (!x) { println "this should print" }
this should print

となって修正完了。

スプライシング・アンクォート


さて、残るはスプライシング・アンクォート。 英語では unquote-splicing らしいけど、何故逆になってんでしょうね? ちなみに splice とは「継ぎ合わせる」という意味だそうで。 『プログラミング Clojure』に載っている説明は

スプライシング・アンクォート
構文クォート中で使われると、form の値であるリストをテンプレートのリストに継ぎ足す形で展開する

ふむふむ、全然分からんw むりやり「継ぎ足す」という語句を使ってるよね(笑) まぁ、とりあえずスプライシング・アンクォートを使った Clojure サンプルを見てみましょう:

(defmacro unless2 [expr & forms]
  `(if ~expr nil (do ~@forms)))    ; マクロ本体

(unless2 false
    (println "1st line.")
    (println "2nd line.")
    (println "3rd line."))
  • unless2 は最初の引数の評価結果が false なら、残りの引数を全て実行します(可変長引数)

これを実行すると

1st line.
2nd line.
3rd line.

と表示されます。

スプライシング・アンクォートは、どうも Groovy でいうと GString のプレースホルダーとリスト展開演算子 (*) を合わせたようなものっぽいですな。 いまいちうまいサンプルが書けませんが、以下のような感じです:

shell = new GroovyShell(this.binding)

def unless2(expr, String... forms){
    def code = "if (!$expr) { ${splice(*forms)} }"    // マクロ本体に対応
    
    println "// $code"
    shell.evaluate(code)
}

def splice(form){ forms }
def splice(form0, form1){ "$form0; $form1" }
def splice(form0, form1, form2){ "$form0; $form1; $form2" }
//def splice(String... forms){ forms.join('; ') } // でいいんだけどね

unless2('false',
    'println "1st line."',
    'println "2nd line."',
    'println "3rd line."')
  • splice() メソッドでは引数の文字列(内容が Groovy コード)を ('; ') で連結しています

「マクロの本体に対応」する部分でリスト展開演算子を無理矢理使おうとして、splice() メソッドを3つも定義してますが、実際のところはコメントアウトしている splice() メソッドだけで済みます*2。 上記サンプルを実行すると

// if (!y) { println "1st line."; println "2nd line."; println "3rd line." }
1st line.
2nd line.
3rd line.

と表示されます。 うーむ、ちょっとこのサンプルじゃスプライシング・アンクォートの使い方がイマイチわからんなぁ。 とは言っても、あれこれサンプル書いててちょっとは Clojure のマクロも分かるようになってキタ気がする。

参考

プログラミングClojure 第2版

プログラミングClojure 第2版


プログラミングClojure

プログラミングClojure

*1:完全に対応させるには「"if($expr){}else{ $form }"」とした方がいいかもしれませんね。

*2:その場合、「マクロ本体に対応」する部分のリスト展開演算子も不必要。