2017/12/19

Binding: Implicit parameters with Clojure

この記事は、Clojureアドベントカレンダーの19日目の記事です。今日は、Clojureの動的スコープの話です。

ScalaのImplicit parameter

ここ最近、業務でScalaがメインなのでScalaしか書いてないのですが、たまに、仕事から帰ってきて家でClojureのコードを書いてると、ClojureにはImplicit parametersの機能が無い事に気づきます。というか、そもそもScala以外だと、Implicit parameterは、HaskellのGHC拡張くらいにしかない?(ちゃんと調べてないですが)と思われますが。。。Implicit parametersとは、関数の引数を暗黙的(implicit)に受け渡す機能です。

例えば、DBConnectionのような関数呼び出し毎に同じ変数を渡すようなシチュエーションが発生した時、Implicit parameterでは、関数間で受け渡す、共有したいパラメータ(ここだとConnectionのような変数)について、呼び出し元と呼び出し先のメソッドでそれぞれimplicitであることを宣言すると、暗黙的に変数の受け渡しをやってくれます。例えば、以下のコード。このコードではcallerという関数からcallee1を呼び出し、callee1からcallee2を呼び出しています。

def caller():Unit = {
  implicit val message = "sample message"
  callee1("from caller")
}

def callee1(text1: String)(implicit message: String):Unit ={
  println(text1)
  callee2(text1 + "2")
}

def callee2(text2: String)(implicit message: String):Unit ={
  println(text2)
  println(message)
}
この時、callee2で使用される変数messageは暗黙的に、caller->callee1->callee2へと受け渡されていきます。明示的に引数を宣言していませんが、callee1及びcallee2の関数呼び出しは、暗黙な引数となったmessageを補完されて呼び出されています。この時、補完される値は、implicitキーワードにより定義された暗黙のパラメータであり、caller内ではimplicitキーワードにより宣言された変数(implicitキーワードのないval messageは普通の変数宣言です。)で、callee1呼び出しで不足しているパラメータを補います。さらに、callee1内では、暗黙的に渡されたパラメータがさらに暗黙的にcallee2に渡されています。

implicit parameterのより正確で詳しい説明は、以下にあります。


実行時のコンテクストなど、種々のパラメータを明示的に記述しないことで、不要な(つまり、例えば、その変数に代入されたオブジェクトが直接使用されない)箇所での余計な記述が減り、その関数で記述すべきロジックが明確になるというメリットがあります。特に、関数の引数で変数引きずり回しプログラミングをやっていると、必須の機能になってくるのではないでしょうか(?)
Implicit parameterも一長一短の機能で手放しで喜べる機能ではないですが、その機能を意識してコントロールしている限りでは、便利な機能と言えます。

ClojureでImplicit parameter的なものを考える

そんな、機能がClojureにあったら良いなと度々思うようになりました。Clojureでは、Lexical Closureのように、環境を捕縛することは出来ても、一々引数として指定せずに別のコンテクストから、値を渡す、といったことはできません。マクロで無理矢理implicitを表す、implicit-letのような構文を作って代入する、みたいなことも出来そうではあります。例えば以下のような感じに。

(implicit-let
 [param1 (get-context1)]
 (function4
    (function1 arg1 arg2)
    (function2 arg1 arg2) ;; 本当はfunction3に3番目のパラメータとしてparam1が入る
    (function3 arg1 arg2))) ;; 本当はfunction3に3番目のパラメータとしてparam1が入る
implicit-letマクロ拡張時に、scopeにバインドされているfunction2とfunction3のスコープを調べ(調べるならdocかsourceマクロあたりでしょうか)、引数が不足していてかつ、パラメータ名(上記で言う所のparam1)が同一であれば、(function2 arg1 arg2)を(function2 arg1 arg2 aram1)に拡張するようなS式変換を行うことで、静的に暗黙のパラメータを受け渡すことが出来そうです。ここに型チェックでも入ればScalaが提供しているImplicit parameterとほぼ同様のものになると思われます。
ただ、これには、マルチメソッドはどうするのか、とか、マクロ内マクロはどのような扱いにするのか、などletのbody部の各名前や構造の静的解決が簡単ではありません。

Binding(Clojureの動的スコープ)

そんなことを考えながら仕事をしていたある日、Implicit Parametersって動的スコープとよく似ているなあ、と思いググったところ、まさにこれ、といった文献がヒットしました。


上記の文献によると、"Implicit Parameters"とは、"Static Types"で"Dynamic Scoping"をするためのものであるようです。タイトルそのまんまですが。だとすると、ClojureでImplicit parameter使いたい問題は、Clojureで動的スコープ使いたい問題と言い換える事ができ、なんとも、都合の良いことにClojureには動的スコープを操作するためのマクロが存在します。
^:dynamicキーワードとbindingです。以下のように動的に値を束縛します。

(def ^:dynamic x "α")
(def ^:dynamic y "β")

(println (format "point1: x = %s, y = %s" x y))

(defn call-xy []
  (println (format "in call-xy: x = %s, y = %s" x y)))

(binding [x "a" y "b"]
  (println (format "after binding: x = %s, y = %s" x y))
  (call-xy))

(println (format "point2: x = %s, y = %s" x y))

(call-xy)
そして、これに対して以下のような出力結果が得られます。

point1: x = α, y = β
after binding: x = a, y = b
in call-xy: x = a, y = b
point2: x = α, y = β
in call-xy: x = α, y = β
bindingにより束縛した、後と束縛中に呼び出した関数呼び出しの中のみ、束縛した値に変更されます。そして、bindingのスコープ外では束縛した値が束縛前のもとの値に戻っています。bindingは、bindingスコープの外側に出ると、もとの値、束縛前の値を再度束縛するという機能があります。これにより引数よる明示的な値渡し以外の方法で値を渡すことを実現します。bindingされた時の呼び出しから呼び出される関数内でのdynamicパラメータを持つ変数のみに動的に値が設定されます。そして、さらに都合の良いことに、スレッドセーフでもあるのです。。。このあたりが、ref(transactionalなclojureのグローバル変数)との使い分けのポイントになると思います。

そして、気になる静的スコープとの競合ですが、以下のようになります。

user=> (binding [x "2"] x)
"2"
user=> (binding [x "2"] (let [x "3"] x))
"3"
user=> (let [x "3"] (binding [x "2"] x))
"3"
基本的に、静的スコープにより遮蔽されるようです。しかし以下のようにvar-get関数により取得することは可能です。
(単にletを飛び越えてトップレベルのdefで定義されたvarを取ってきているだけですが。)

user=>  (let [x "3"] (binding [x "2"] (var-get #'x)))
"2"

Binding(動的スコープ)を使う

動的スコープは副作用ありきの機能というか参照透過性を破壊のような概念ではありますが、Clojureで使われているbindingによる動的スコープなら、例えば、DBコネクションを複数の関数に渡したい、とか、コードの実行コンテクストを複数の関数どうしで引き回したいとか、再帰で書く必要があるものの、一々引数指定したくない(けど、代入する必要がある)などに有用だと思われます。このような、bindingの使い方は、例えば、標準ライブラリのcl-formatや、IOのreadなどで使われています。

例えば、以下のようなコードがある時、関数f, g,でそれぞれg, hの呼び出し以外にparam1, param2が使用されない時、hの変数の値を解決しようとすると、呼び出した先の関数の先の関数で使う値を、一旦、呼び出す関数に渡し、さらにその先での呼び出しでもまた同様に値を渡す必要が出てきます。

(defn h [param1 param2]
  param1, param2を使用)

(defn g [b param1 param2]
  ...
  (h param1 param2)
  ...)

(defn f [a param1 param2]
  ...
  (g abc param1 param2)
  ...)

(f param0 param1 param2)
このようなコードだとかなり厳しさがありますが、例えば動的スコープなら、以下のように解決することが出来ます。

(defn h []
  param1, param2を使用)

(defn g [b]
  ...
  (h)
  ...)

(defn f [a]
  ...
  (g abc)
  ...)

(binding [param1 .... param2 ...]
  (f param0))
また、再帰呼出しを行う以下のような関数がある時、再帰呼出し中、自分自身を呼び出す時に、再帰呼出し中では値が変更されない引数を自分自身の中で共有するために再帰中に値を引き回す必要が出てきます。以下の例では、不変なリストpoints (ex. [[10 20] [30 40] ... ]のような値が入ります)に対して、find-minとweightで、不変なリストを複数の関数内で引き回して使っています。
(ちなみに、以下のコードはの前回の記事から。)

(defn weight [points i j]
  (st/euclidean-distance (points i) (points j)))

(defn +m [x y size]
  (mod (+ x y size) size))

(defn min-path [args]
   (first (sort-by second (filter not-empty args))))

(defn find-min [points i j]
  (if (<= (Math/abs (- i j)) 2)
    [[] 0]
    (let [size (count points)
          i+1 (+m i 1 size)
          j-1 (+m j -1 size)
          [lines w] (find-min points i+1 j)
          skip-i [(cons [i+1 j] lines) (+ w (weight points i+1 j))]
          [lines w] (find-min points i j-1)
          skip-j [(cons [i j-1] lines) (+ w (weight points i j-1))]]
      (letfn [(search-inter [k]
                (let [[lines1 w1] (find-min points i k)
                      [lines2 w2] (find-min points k j)]
                  [(concat [[i k] [k j]] lines1 lines2)
                   (+ w1 w2 (weight points i k) (weight points k j))]))]
        (->> (map search-inter (range (+m i 2 size) (+m j -2 size)))
             (cons skip-i)
             (cons skip-j)
             min-path)))))

(defn triangulate [points]
  (find-min points 0 (- (count points) 1)))
勿論、上記のようなコードは、一旦、Closureを作って解決するという方法も取れますが、そうなると、letfnなどを使い、関数中にさらに不変な変数を共有するためだけに、関数を再定義する必要がでてきます。このようなコードは動的スコープにより以下のように書き直せます。

(def ^:dynamic points [])

(defn weight [i j]
  (st/euclidean-distance (points i) (points j)))

(defn +m [x y size]
  (mod (+ x y size) size))

(defn min-path [args]
   (first (sort-by second (filter not-empty args))))

(defn find-min [i j]
  (if (<= (Math/abs (- i j)) 2)
    [[] 0]
    (let [size (count points)
          i+1 (+m i 1 size)
          j-1 (+m j -1 size)
          [lines w] (find-min i+1 j)
          skip-i [(cons [i+1 j] lines) (+ w (weight i+1 j))]
          [lines w] (find-min i j-1)
          skip-j [(cons [i j-1] lines) (+ w (weight i j-1))]]
      (letfn [(search-inter [k]
                (let [[lines1 w1] (find-min i k)
                      [lines2 w2] (find-min k j)]
                  [(concat [[i k] [k j]] lines1 lines2)
                   (+ w1 w2 (weight i k)) (weight k j)]))]
        (->> (map search-inter (range (+m i 2 size) (+m j -2 size)))
             (cons skip-i)
             (cons skip-j)
             min-path)))))

(defn triangulate [points]
  (binding [points points]
    (find-min 0 (- (count points) 1))))
単にグローバル変数に変数を束縛しているだけである事との違いは、呼び出し元(この場合だとtriangulate)の関数内で、pointsの値を書き換えてる点です。これにより、(当然ですが)triangulateに違うリストを渡した場合に違う結果が返ってくることとなり、あたかも副作用のない通常の関数呼び出しを行っているかのような記述が可能になります。

動的スコープのデメリットとしては、静的にはどこの値が束縛されるのか分からないという問題も発生します。上記のような書き方だと、(トップレベルには記述があるとは言え)どうしてもどこにも所属しない自由変数が関数内に登場してしまうことにはなります。しかし、名前の解決のためだけに値を引数で引き回したり、再帰的な関数をletfnなどで再定義するよりは、シンプルなコードが実現できると思います。

おわりに

前述の文献のように、Implicit parametersが静的型付言語でDynamic Scopingの役割を果たすものだとするならば、その逆もまたありえる、つまり、ScalaでImplicit paramtersで記述されている箇所は、Clojureの動的スコープを使用することで値渡しが可能になるのではないでしょうか。

濫用するとコードが読みにくく扱いづらくなりますが、(これに関してはScalaのimplicit Parameterも同様です)適度に使用することで、無用な引数渡しや、煩雑なデータ渡しが簡潔に記述できて、見通しがよいコードが書けそうです。特にScalaのコードにおいてImplicit parametersで値を渡しているような箇所は、Clojureではdynamic bindingで解決するという方法が取れるのかも知れません。

言語の名前こそClojure(Closure)ですが、動的スコープもまた利用可能で、Lexical Closureのみによって束縛されない変数を含む関数を定義できます。Scalaのimplicit Parameterが良いなと思った時、Clojureではbindingを試してみるのはいかがでしょうか。

参考文献

  1. Let vs. Binding in Clojure - Stack Over Flow
  2. Jeffrey R. Lewis, Mark B. Shields, Erik Meijer, John Launchbury, Iplicit Parameters: Dynamic Scoping with Static Types, 2000, [PDF]
  3. On the Perils of Dynamic Scope - Digital Digressions by Stuart Sierra
  4. Clojure勉強日記(その21 varを使用したスレッドローカルな状態管理 - 夢とガラクタの集積場
  5. Scala 言語仕様 Version 2.8 DRAFT July 13, 2010

0 件のコメント :