CLOJURE for the BRAVE and TRUEでClojure学習 part1

はじめに

Clojure の勉強をはじめた。
最近出版された Clojure 書籍がないため、Clojure/ClojureScript 関連リンク集で入門者向けとして紹介されてる
Clojure for the Brave and Trueを翻訳?(読み)ながら自分が重要だとおもったところメモしていくスタイルで Clojure への理解を深めようと思う。

今回は、Chapter3 Do Things: A Clojure Crash Course の学習記録。
Chapter3 ではあるが、1, 2 が環境構築の説明なので、実質この章が文法学習のスタートになる。

Syntax

Form

Clojure で書かれたコードはみな統一された構造をもっており、構造には2つの種類がある。

  • データ構造をそのまま記述(たとえば、数値や、文字列、ハッシュマップ、ベクタ)
  • オペレーション

データ構造をそのまま記述する例は次の通り

1
"a string"
["a" "vector" "of" "strings"]

オペレーションは、開括弧、オペレータ、オペランド、閉括弧で構成される。

(operator oprand1 oprand2 ... operandn)

カンマではなく、空白でオペランドを区別する。

たとえば以下のとおり

(+ 1 2 3)
; => 6
(str "It was the panda " "in the library " "with a dust buster")
; => It was the panda in the library with a dust buster

このような Clojure の構造は、今まで使用してきた言語と異なるので奇妙に思うかもしれない。
他の言語では、オペレーションが異なれば、オペレータとオペランドに応じて構造も変わる。
たとえば、javascript ではオペレータや括弧など様々なものを取り入れている。

1 + 2 + 3;
"It was the panda ".concat("in the library ", "with a dust buster");

一方 Clojure の構造は一貫してシンプル。 どんなオペレータをつかおうとも、またどんな種類のデータを処理しようとも構造は同じである。

Control Form

if

書き方

(if boolean-form
  then-form
  optional-else-form)

判別式が true の場合は、はじめのフォーム(then-form)が評価され、
判別式が false の場合は、つぎのフォーム(optional-else-form)フォームが評価される。

判別式が true の場合

(if true
  "By Zeus' hammer!"
  "By Aquamans's trident!")

判別式が fale の場合

(if false
  "By Zeus' hammer!"
  "By Aquamans's trident!")

else は書かなくてもよい。

(if false
  "By Odin's Elbow")
do

しかし以下の Ruby のように、条件みたしたときに複数の評価をしたい場合どうすればよいのだろうか?

if true
    doer.do_thing(1)
    doer.do_thing(2)
else
    other_doer.do_thing(1)
    other_doer.do_thing(2)
end

do を使用すれば実現できる

(if true
  (do (println "Success")
      "By Zeus' hammer!")
  (do (println "Failure")
      "By Aquamans's trident"))
; => Success!
; => "By Zeus's hammer!"
when

when は、 ifdo をあわせたオペレータであり、 else がない。

(when true
  (println "Success!")
  "abra cadabra")
; => Success!
; => "abra cadabra"

nil, true, false, Truthiness, Equality, and Boolean Expression

Clojure は、true と false を真偽値としてもつ。 nil は、値をもたないものとして Clojure で扱われる。nil かどうかを判別するには、 nil? 関数を使用する。

(nil? 1)
; => false
(nil? nil)
; => true

nil と false は、論理的な偽として扱われ、それ以外はすべて論理的に真として扱われる。

(if "bears eat bears"
  "bears beets Battlestar Galatica")
; => bears beets Battlestar Galatica
(if nil
  "This won't be the result because nil is falsey"
  "nil is falsey")
; => nil is falsey

Namging values with def

def をつかうと名前に値をバインド(固定)することができる。

(def failed-protagonist-names
  [["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"]])
failed-protagonist-names
; => ["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"]

bind する理由は再代入できないようにするためである。

Data Structures

Clojure には便利なデータ構造があり、それに大半な時間を使用するだろう。
オブジェクト指向バックグラウンドの人は、ここで紹介する基本的なデータ構造で多くのことがができることに驚くだろう。

Clojure のデータ構造はイミュータブルであり、変更できない。

1. Numbers

Clojure では、整数、小数に加えて 1/5 のように比率のまま扱える。

93
1.2
1/5

2. Strings

文字列はダブルクォートでくくる。シングルクォートは使用できない。

Clojure では変数に文字列を追加することを許可していないので、 str 関数を使用する。

(def name "Chewbacca")
(str  "\"Uggllglglglglglglglll\" - " name)

3. Maps

他の言語でいう、ハッシュマップやディクショナリと似たものである。ある値を他の何かと関連づける。

空の map はこの通り

{}

:first-name, :last-name はキーワードと呼ばれる(後述する)

{:first-name "Charlie"
:last-name "McFishwich"}

関数を value に指定できる

{"string-key" +}

ネストできる

{:name {:first "John" :middle "Jacob" :last "Jingleheimerschmidt"}}

hash-map 関数を使って map を作成できる

(hash-map :a 1 :b 2)
; => {:a 1 :b 2}

get 関数を使って値を取得できる

(get {:a 1 :b 1} :b)
; => 1

key がなければ、get 関数は nil を返す

(get {:a 1 :b 1} :c)
; => nil

get-in 関数でネストした map から値を取得できる

(get-in {:a 0 :b {:c "ho hum"}} [:b :c])
; => ho hum

map の引数に key を指定すると値を取得できる

({:a 0 :b {:c "ho hum"}} :b)
; => {:c "ho hum"}

4. Keywords

主に map の key として使用される。

関数として使用すると、key に一致する value を取得できる

(:a {:a 1 :b 2 :c 3})
; => 1

get 関数を使えば、default value を設定できる

(:d {:a 1 :b 2 :c 3} "No gnome knows homes like Noah knows")
; => "No gnome knows homes like Noah knows"

5. Vectors

配列のように、インデックスアクセスできる

(get [3 2 1] 0)
; => 3

vector 関数を利用して、ベクタを生成できる

(vector 1 2 3)
; => [1 2 3]

conj 関数で、要素を後ろに追加できる

(conj [1 2 3] 4)
; => [1 2 3 4]

6. Lists

Lists は Vector と同様リニアなコレクションであるが、get で要素を取得できない。 また、List であることを表現するには、シングルクォートを前につける必要がある。

'(1 2 3 4)
; => (1 2 3 4)

リストから要素を取得するには、nth を使う。ただしリストよりベクタの要素アクセスのほうが、パフォーマンスはよい。

(nth '(1 2 3) 0)
; => 1

conj 関数でリストの先頭に要素を追加する

(conj '(1 2 3 ) 4)
; => (4 1 2 3)

リストとベクタの使い分けは、 先頭に要素を追加するとき、マクロを作成するときにリストを使い、 それ以外は、ベクタを利用するのがよい。

7. Sets

Sets は、一意のコレクションである。 同じ要素が複数存在することがない。

#{"kurt vonnegut" 20 :icicle}
; => #{20 :icicle "kurt vonnegut"}

hash-set をつかって Set を作成できる

(hash-set 1 1 2 2)
; => #{1 2}

同じ要素を追加しても、変化しない。

(conj #{1 2} 2)
; => #{1 2}

set 関数でベクタから Sets を作成できる

(set [3 3 3 4 4])
; => #{4 3}

contains?で要素が含まれてるかどうか確認できる

(contains? #{:a :b} :a)
; => true

(contains? #{:a :b} 3)
; => false

(contains? #{nil} nil)
; => true

8. Simplicity

Clojure は、オブジェクト指向であつかう独自のクラスを考えない。組み込みのデータ構造を扱うことが、Clojure のシンプルさである。

10 個のデータ構造が 10 の関数をもつより、1 個データ構造で 100 の関数持つほうがよい。 Alan Perlis

基本的なデータ構造を利用してコードの再利用性を得られる方法を意識するがよい

Functions

1. Caling Functions

今まで見てきたように関数呼び出しはこんな感じ。

(+ 1 2 3 4)
(* 1 2 3 4)
(first [1 2 3 4])

関数呼び出しは、演算子が関数あるいは関数を返す関数式として処理される操作のことである。

関数式としての例

((and (= 1 1) +) 1 2 3)

関数の柔軟性は、関数をファーストクラスとして Number や Vecotor と同じように引数に使えることである。

(map inc [1 3])
; => [2 4]

map の返り値が、ベクタではあくリストであることに注意しよう。引数として渡したときはベクタであったのにである。

(+ (inc 199) (/ 100 (- 7 2)))
(+ 200 (/ 100 (- 7 2))) ; evaluated "(inc 199)"
(+ 200 (/ 100 5)) ; evaluated (- 7 2)
(+ 200 20) ; evaluated (/ 100 5)
220 ; final evaluation

すべてのフォームが評価されてから、はじめの + が評価される。

2. Function Calls, Macro Calls, Special Forms

関数呼び出しは、関数式が演算子として処理される式である。式には他に二種類あって、マクロ呼び出しと、スペシャルフォームである。

スペシャルフォームの special の意味というのは、関数呼び出しと異なり、必ずしもすべてのオペランドを評価するわけではないという点にある。

スペシャルフォームの例として if がある。

(if good-mood
    (tweet walking-on-sunshine-lyrics) ;; 1
    (tweet mopey-country-song-lyrics)) ;; 2

この場合、全てのオペランドが評価されず、条件文に応じて 1,2 のいずれかが評価される。

また他の特徴として、関数の引数に指定できないことである。一般的に、 special form は、中心となる Clojure の関数性を実現しており、関数として実装できない。

3. Defineing Functions

関数定義は、5 つのパーツで構成される。

  • defn
  • 名前
  • 関数の説明(オプション)
  • パラメータ
  • 関数内容
(defn too-enthusiastic                                                ;; 関数名
  "Return a cheer that might be a bit too enthusiastic"               ;; 関数の説明
  [name]                                                              ;; パラメータ
  (str "OH. MY. GOD! " name " YOU ARE MOST DEFINITELY LIKE THE BEST " ;; 関数内容
    "MAN SLASH WOMAN EVER I LOVE YOU AND WE SHOULD RUN AWAY SOMEWHERE"))
1. The Docstring

REPL で (doc fn-name) を実行すると、関数の説明を見ることができる。

2. Pramater and Arity

関数に渡す値を引数とよび、引数の数をアリティ(arity)と呼ぶ。
0 個以上の引数をわたすことができ、どんな種類でもよい。

(defn no-parms
  []
  "I take no parameters!")

(defn one-param
  [x]
  (str "I take one parmeter:" x))

(defn two-params
  [x y]
  (str "Two parameters! That's nothing! Pah! I will smoosh them "
  "together to spite you! " x y))

Arity を使えばオーバーロードを実現でき、
ひとつの関数定義に、Arity に応じて異なる処理を実行できる。

Arity ごとに()でくくられて、それぞれ引数のリストをもつ。

(defn multi-arity
  ;; 3 arity
  ([first second third]
    (do-things first second third))
  ;; 2 arity
  ([first second]
    (do-things first second))
  ;; 1 arity
  ([first]
    (do-things first)))

Arity のオーバーロードを使えば、デフォルト引数が使用できる

(defn x-chop
  "Describe the kind of chop you're inflicting on someone"
  ([name chop-type]
    (str "I " chop-type " chop " name "! Take that!"))
  ([name]
    (x-chop name "karate")))
(x-chop "Kanye West" "slap")
; => "I slap chop Kanye West! Take that!"
(x-chop "Kanye East")
; => "I karate chop Kanye East! Take that!"

& を使えば rest パラメータを使用できる。

(defn codger-communication
  [whippersnapper]
  (str "Get off my lawn, " whippersnapper "!!!"))

(defn codger
  [& whippersnappers]
  (map codger-communication whippersnappers))

(codger "Billy" "Anne-Marie" "The Incredible Bulk")
; => ("Get off my lawn, Billy!!!"
;      "Get off my lawn, Anne-Marie!!!"
;      "Get off my lawn, The Incredible Bulk!!!")
(defn favorite-things
  [name & things]
  (str "Hi, " name ", here are my favorite things: "
  (clojure.string/join ", " things)))

(favorite-things "Doreen" "gum" "shoes" "kara-te")
3. Destructing

collection で使う

受け取ったベクタの引数のはじめの要素を first-thing にバインドする

(defn my-first
  [[first-thing]]
  first-thing)
  (my-first ["oven" "bike" "war-exe"])

rest パラメータも使用できる

(defn chooser
  [[first-choice second-choice & unimportant-choices]]
  (println (str "Your first choice is: " first-choice))
  (println (str "Your second choice is: " second-choice))
  (println (str "We're ignoring the rest of your choices. "
    "Here they are in case you need to cry over them:"
  (clojure.string/join "," unimportant-choices))))

(chooser ["Mamalade" "Handome Jack" "Pigpen" "Aquaman"])

Map で使う

{変数 :key}という表現で、変数名に key に対応する value をバインドする

(defn announce-treasure-location
  [{lat :lat lng :lng}]
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})

他のやりかたもある。

(defn announce-treasure-location
  [{:keys [lat lng]}] ;; ここが違う
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})

:as を使えば、もともとの map にもアクセスできる

(defn receive-treasure-location
  [{:keys [lat lng] :as treasure-location}]
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng))
  (steer-ship! treasure-location))
4. Function body

Function body にはどんな種類のフォームでも含めることができる。
返り値は、最後のフォームの評価結果になる。

(defn illustrative-function
  []
  (+ 1 304)
  30
  "joe")
(illustrative-function)
5. All Functions Are Created Equal

自分で定義した関数であれ、inc、map などの組み込み関数であれ、Clojure は区別せず括弧のはじめを関数として愚直に処理する。これが Clojure の Simple さである。

6. Anonymous Functions

fn を使用すれば匿名関数を作れる。匿名関数でも、通常通り、destructing や rest パラメータを使用できる。

(map (fn [name] (str "Hi, " name))
  ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

またもう一つ簡潔な匿名関数の作成方法がある。

(map #(str "Hi, " %) ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

リーダマクロという機能によって、この書き方でも匿名関数として利用される。
% は、引数を意味しており、複数の引数を受け取る場合、%1 %2 %3…となる。
%、%1 と同じ意味である。

%& で rest パラメータを利用できる。

(#(identity %&) 1 "blarg" :yip)

identity 関数は、自身を返す関数である。
rest パラメータは、リストに格納され、それぞれに対して identity 関数が適用された結果が返ってきてる。

7. Returning Functions

関数を返す関数を、Closure と呼び、関数定義されたスコープ内のどの変数にもアクセスできる。

(defn inc-maker
  "Create a custom incrmeter"
  [inc-by]
  #(+ % inc-by))

(def inc3 (inc-maker 3))

(inc3 7)

Pulling it All Together

問題: ホビットの非対称な体の情報から完全な情報をつくる以下定義されるように、体の部位(name)とサイズ(size)が記載されたホビットの体の情報がある。しかし、左目(left-eye)のように左のパーツしか記述できていない不完全な状態なので、これを完全な状態にするのがゴール。

(def asym-hobbit-body-parts [{:name "head" :size 3}
                              {:name "left-eye" :size 1}
                              {:name "left-ear" :size 1}
                              {:name "mouth" :size 1}
                              {:name "nose" :size 1}
                              {:name "neck" :size 2}
                              {:name "left-shoulder" :size 3}
                              {:name "left-upper-arm" :size 3}
                              {:name "chest" :size 10}
                              {:name "back" :size 10}
                              {:name "left-forearm" :size 3}
                              {:name "abdomen" :size 6}
                              {:name "left-kidney" :size 1}
                              {:name "left-hand" :size 2}
                              {:name "left-knee" :size 2}
                              {:name "left-thigh" :size 4}
                              {:name "left-lower-leg" :size 3}
                              {:name "left-achilles" :size 1}
                              {:name "left-foot" :size 2}])

最終的に生成するコードは以下の通り

(defn matching-part
  [part]
  {:name (clojure.string/replace (:name part) #"^left-" "right-")
    :size (:size part)})

(defn symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (loop [remaining-asym-parts asym-body-parts
          final-body-parts []]
    (if (empty? remaining-asym-parts)
      final-body-parts
      (let [[part & remaining] remaining-asym-parts]
        (recur remaining
                (into final-body-parts
                      (set [part (matching-part part)])))))))

実行すると次の結果を得る。

(symmetrize-body-parts asym-hobbit-body-parts)
; => [{:name "head", :size 3}
;     {:name "left-eye", :size 1}
;     {:name "right-eye", :size 1}
;     {:name "left-ear", :size 1}
;     {:name "right-ear", :size 1}
;     {:name "mouth", :size 1}
;     {:name "nose", :size 1}
;     {:name "neck", :size 2}
;     {:name "left-shoulder", :size 3}
;     {:name "right-shoulder", :size 3}
;     {:name "left-upper-arm", :size 3}
;     {:name "right-upper-arm", :size 3}
;     {:name "chest", :size 10}
;     {:name "back", :size 10}
;     {:name "left-forearm", :size 3}
;     {:name "right-forearm", :size 3}
;     {:name "abdomen", :size 6}
;     {:name "left-kidney", :size 1}
;     {:name "right-kidney", :size 1}
;     {:name "left-hand", :size 2}
;     {:name "right-hand", :size 2}
;     {:name "left-knee", :size 2}
;     {:name "right-knee", :size 2}
;     {:name "left-thigh", :size 4}
;     {:name "right-thigh", :size 4}
;     {:name "left-lower-leg", :size 3}
;     {:name "right-lower-leg", :size 3}
;     {:name "left-achilles", :size 1}
;     {:name "right-achilles", :size 1}
;     {:name "left-foot", :size 2}
;     {:name "right-foot", :size 2}]

以下、コードの説明をしていく

1. let

let は名前に値を bind(固定)する機能がある。

次は、x という変数名に 3 という値を固定している。

(let [x 3]
  x)

次の例は、dalmatian-list の先頭から2要素を取得して dalmatians という変数にバインドしてる

(def dalmatian-list
  ["Pongo" "Perdita" "Puppy 1" "Puppy 2"])
(let [dalmatians (take 2 dalmatian-list)]
  dalmatians)

また let には新しいスコープを作成する機能がある。

(def x 0)
(let [x 1] x)

はじめに変数 x に 0 をバインドしているが、let 内では x を 1 でバインドしてる。

(def x 0)
(let [x (inc x)] x)

let ないで rest パラメータを利用できる。

(def dalmatian-list
  ["Pongo" "Perdita" "Puppy 1" "Puppy 2"])

(let [[pongo & dalmatians] dalmatian-list]
  [pongo dalmatians])

では、問題の該当箇所にもどって説明する。

(let [[part & remaining] remaining-asym-parts]
  (recur remaining
          (into final-body-parts
                (set [part (matching-part part)]))))

let によって、remaining-asym-parts の最初の要素を part に、残りの要素を remaing にバインドしてる。

その後、body 内で recur フォームを評価している。

2. loop

loop フォームのサンプルをみてみる。

(loop [iteration 0]
  (println (str "Iteration " iteration))
  (if (> iteration 3)
    (println "Goodbye!")
    (recur (inc iteration))))

はじめの行では、変数名 iteration を初期値 0 でバインドしている。loop 開始時 iteration を 0 にしている。

次の println を呼び出し、iteration が 3 を超えていたら、Goodbye を出力する。超えていないならば、recur を行う。

recur のオペランドが、次のループの iteration の値になる。

loop 関数を再帰関数をつかって表現できる

(defn recursive-printer
  ;; デフォルトvalueを指定してる
  ([]
    (recursive-printer 0))
  ([iteration]
    (println (str "Iteration " iteration))
    (if (> iteration 3)
      (println "GoodBye!")
      (recursive-printer (inc iteration)))))

(recursive-printer)
; => Iteration 0
; => Iteration 1
; => Iteration 2
; => Iteration 3
; => Iteration 4
; => Goodbye!

再帰関数でかけると言われて、自分で書いてみたら次のようなコードになった。
出力は同じだが、2つことなる。

  • 関数呼び出し時に、引数を渡たしてる
  • do 関数を利用してる
(defn iteration
  "自分で書いてみる"
  [param]
  (if (> param 4)
    (println "Goodbye!")
    (do (println (str "Iteration " param))
        (iteration (inc param)))))

(iteration 0)

0 という引数を渡してることから、
デフォルト引数をまだ身に着けれていないことがわかる。

また do 内で複数を呼ぶ必要はない。

3. Regular Expression

正規表現の書き方

#"regular-expression"

clojure.string/replaceを使用すれば、正規表現に合致する文字列を変換できる。
^は、先頭を意味する

正規表現のテストするには、re-find関数が役に立つ。
マッチすればマッチした文字列を返し、そうでなければ nil を返す

(re-find #"^left-" "left-eye")
; => "left-"

(re-find #"^left-" "cleft-chin")
; => nil

(re-find #"^left-" "wongleblart")
; => nil

では、問題のコードでも、left が先頭につく文字列は、right に変換し そうでなければそのまま変換してることがわかる。

(defn matching-part
  [part]
  {:name (clojure.string/replace (:name part) #"^left-" "right-")
   :size (:size part)})
(matching-part {:name "left-eye" :size 1})
; => {:name "right-eye" :size 1}]

(matching-part {:name "head" :size 3})
; => {:name "head" :size 3}]

4. Better Symmetrizer with reduce

loop よりも reduce 関数を使えば、完結に書くことができる。

reduce の書き方

(reduce function initial-value sequence)
(reduce + 15 [1 2 3 4])
(defn better-symmetrize-body-parts
  "Expects a seq of maps that a :name and :size"
  [asym-body-parts]
  (reduce (fn [final-body-parts part]
            (into final-body-parts (set [part (matching-part part)])))
            []
            asym-body-parts))
(def asym-hobbit-body-parts [{:name "head" :size 3}
                        {:name "left-eye" :size 1}
                        {:name "left-ear" :size 1}
                        {:name "mouth" :size 1}
                        {:name "nose" :size 1}
                        {:name "neck" :size 2}
                        {:name "left-shoulder" :size 3}
                        {:name "left-upper-arm" :size 3}
                        {:name "chest" :size 10}
                        {:name "back" :size 10}
                        {:name "left-forearm" :size 3}
                        {:name "abdomen" :size 6}
                        {:name "left-kidney" :size 1}
                        {:name "left-hand" :size 2}
                        {:name "left-knee" :size 2}
                        {:name "left-thigh" :size 4}
                        {:name "left-lower-leg" :size 3}
                        {:name "left-achilles" :size 1}
                        {:name "left-foot" :size 2}])

(defn better-symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (reduce (fn [final-body-parts part]
            (into final-body-parts (set [part (matching-part part)])))
          []
          asym-body-parts))

学んだ destructing と匿名関数を使えって書き直せば、次のとおりだろう。

(defn match-part
  [{name :name size :size}]
  {:name (clojure.string/replace name #"^left-" "right-")
  :size size})

(defn better-symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (reduce #(into %1 (set [%2 (match-part %2)])) [] asym-hobbit-body-parts))

さいごに

hashMap へのアクセス方法や、List と Vector という2つのデータ構造があったりするが、 今のところ他の言語と比べて大きく異るという感じはしない。

次章から Clojure らしさを感じ取れるだろう。 にしても長い。。。

最後までありがとうございました。