clojure備忘録[clojureの基本的な特殊形式 その6(let特殊形式)]

これはマクロで実装できるので個人的には特殊形式ではないだろうと思っていたら特殊形式でした。。


というわけで「let特殊形式」です。


一般的な手続き型言語で定義する関数やらメソッドでは、代入を使って値を保持しておき、後からその値を使って処理を実行します。
その場合、代入される変数は関数内部で宣言され、有効なスコープはその関数内のみ、というケースが多いでしょう。
言語によってはif文、for文等のブロック内でのみ有効な変数を持たせるものもあると思います。


clojureで同様の機能を提供するのがlet特殊形式です。表記法は次のようになります。


表記

(let [a value-of-a
      b value-of-b
      ...]
  body
  ...)

let特殊系式の第1引数がベクタのようになっていますが、このベクタの中身は、let内部のbody部分で参照されるローカル変数になります。
aが変数、value-of-aが変数aに束縛される値、bが変数、value-of-bが変数bに束縛される値、以下、複数の変数束縛を表記できます。
body部分で実際にaやbを使用したS式を記述可能です。
また、letで指定された変数a、bは、このlet特殊系式の中でのみ有効になります。


実例

お題は1変数の二次方程式の一般解を計算する関数を作成してみます。
二次方程式の一般形は、


ax^2 + bx + c = 0

であり、この一般解は、


x = \frac{-b\pm\sqrt{b^2-4ac}}{2a}

になります。これを計算するのが次の関数です。

(defn quadratic-equation-answer
  [a b c]
  (let [b2-4ac (- ( * b b) ( * 4 a c))
        root   (Math/sqrt b2-4ac)
        ans0   (/ (+ (- b) root) ( * 2 a))
        ans1   (/ (- (- b) root) ( * 2 a))]
    [ans0 ans1]))

引数が二次方程式の係数に相当します。また、Math/sqrtはjava.lang.Mathクラスのsqrtメソッドをコールしています。
詳細は、javaの呼び出しに関するエントリに回します。とりあえず、staticなメソッドを呼び出したい時は、「(クラス/メソッド 引数1 引数2 ...)」で呼び出せるのでそれを利用しているとだけ認識してください。


ではreplで評価してみます。

user=> (defn quadratic-equation-answer
  [a b c]
  (let [b2-4ac (- (* b b) (* 4 a c))
        root   (Math/sqrt b2-4ac)
        ans0   (/ (+ (- b) root) (* 2 a))
        ans1   (/ (- (- b) root) (* 2 a))]
    [ans0 ans1]))
#'user/quadratic-equation-answer
user=> (quadratic-equation-answer 1 -7 12)
[4.0 3.0]
user=> (quadratic-equation-answer 1 -4 4)
[2.0 2.0]
user=> 

ちゃんと計算されています。


この例で注意したいのは、letの第2引数のベクタについてです。先に束縛されたものはそれ以降の束縛で使用可能になります。
一番最初の束縛である「b2-4ac」は、次のrootの束縛の値を計算するのに使用されており、
その「root」も次の「ans0」、さらにその次の「ans1」を計算するのに使用されています。



もう一つ例を掲載します。今度はスコープについて注目してみます。letを入れ子にして動きを確認してみます。

(defn lets
  [val]
  (println "Depth 0      : val =" val)
  (let [tmp (+ val 1)]
    (println "Depth 1      : tmp =" tmp)
    (let [tmp (+ tmp 2)]
      (println "Depth 2      : tmp =" tmp)
      (let [tmp (+ tmp 3)]
        (println "Depth 3      : tmp =" tmp))
      (println "Depth 2 again: tmp =" tmp))
    (println "Depth 1 again: tmp =" tmp))
  (println "Depth 0 again: val =" val))

「tmp」が入れ子のletで重複して使用されています。これらは、束縛されたlet特殊形式の中でのみ有効になるため、自分の子のletで束縛された値は子から出た後は無効になります。
実際の動きを見てみます。



user=> (defn lets
[val]
(println "Depth 0 : val =" val)
(let [tmp (+ val 1)]
(println "Depth 1 : tmp =" tmp)
(let [tmp (+ tmp 2)]
(println "Depth 2 : tmp =" tmp)
(let [tmp (+ tmp 3)]
(println "Depth 3 : tmp =" tmp))
(println "Depth 2 again: tmp =" tmp))
(println "Depth 1 again: tmp =" tmp))
(println "Depth 0 again: val =" val))
#'user/lets
user=> (lets 1)
Depth 0 : val = 1
Depth 1 : tmp = 2
Depth 2 : tmp = 4
Depth 3 : tmp = 7
Depth 2 again: tmp = 4
Depth 1 again: tmp = 2
Depth 0 again: val = 1
nil
user=>


入れ子のletの中で加算されていたtmpは、入れ子のletの外に出ると無かったことになっていることが分かると思います。
覚えておくのは、「let特殊形式の中で束縛された値は、その外側で束縛された変数と名前が重複していても、let特殊系式の中でしか有効でない」ということです。
個人的には同名の変数を定義することはおすすめはしませんが。。

おまけ

実はletは無名関数でも代用できます。こんな感じで。

(defn lets-by-fn
  [val]
  (println "Depth 0      : val =" val)
  ((fn [tmp]
    (println "Depth 1      : tmp =" tmp)
    ((fn [tmp]
      (println "Depth 2      : tmp =" tmp)
      ((fn [tmp]
        (println "Depth 3      : tmp =" tmp)) (+ tmp 3))
      (println "Depth 2 again: tmp =" tmp)) (+ tmp 2))
    (println "Depth 1 again: tmp =" tmp)) (+ val 1))
  (println "Depth 0 again: val =" val))

束縛する値が、無名関数の引数として処理されています。実際にreplで評価してみます。

user=> (defn lets-by-fn
  [val]
  (println "Depth 0      : val =" val)
  ((fn [tmp]
    (println "Depth 1      : tmp =" tmp)
    ((fn [tmp]
      (println "Depth 2      : tmp =" tmp)
      ((fn [tmp]
        (println "Depth 3      : tmp =" tmp)) (+ tmp 3))
      (println "Depth 2 again: tmp =" tmp)) (+ tmp 2))
    (println "Depth 1 again: tmp =" tmp)) (+ val 1))
  (println "Depth 0 again: val =" val))
#'user/lets-by-fn
user=> (lets-by-fn 1)
Depth 0      : val = 1
Depth 1      : tmp = 2
Depth 2      : tmp = 4
Depth 3      : tmp = 7
Depth 2 again: tmp = 4
Depth 1 again: tmp = 2
Depth 0 again: val = 1
nil
user=> 


ものすご〜く分かりにくいですねw