CLOJURE for the BRAVE and TRUEでClojure学習 part7

はじめに

Clojure の学習備忘録。今回はChapter 8: Writing Macrosでマクロの書き方について学ぶ。
これでようやく1章の全Chapter完了である(^o^)

前章がマクロをメカニズムを紹介するのに対して、今回はマクロのより実践的な話。

専門用語をどう訳すが適切なのかわからず進めているので、誤訳あるかもしれませんが、あしからず。

目次

Macros Are Essential

マクロは魅力的なものであるが、コードをド派手にする難解なツールと考えるべきではない。 実際、マクロは、少数のコアな関数やスペシャルフォームから多くの組み込み機能を提供してくれるもの。 when を例にみてみよう。

(when boolean-expression
  expression-1
  expression-2
  expression-3
  ...
  expression-x)

他の多くの言語では、特殊なキーワードを使ってしか条件式を作成できず独自の条件演算子を作る方法もないので、 whenif のようなスペシャルフォームかと思うかもしれないが実はそうではない。 when はマクロである。

マクロ展開すれば、 whenif, do で実装されていることがわかる。

(macroexpand '(when boolean-expression
                expression-1
                expression-2
                expression-3))
; => (if boolean-expression
;       (do expression-1
;           expression-2
;           expression-3))

Anatomy of a Macro

マクロ定義は関数定義とよく似ており、マクロ名や、オプションの説明、引数、処理内容を記述する。 マクロはほぼ必ずリストを返すが、これはClojureが評価できるようにデータ構造を変換するのがマクロの機能であることからわかるだろう。 マクロ定義では、関数や、マクロ、スペシャルフォームが使用でき、関数やスペシャルフォームと同じようにマクロを呼び出すことができる。

もうお馴染みの infix マクロを例にみてみよう。

(defmacro infix
  "Use this macro when you pine for the notation of your childhood"
  [infixed]
  (list (second infixed) (first infixed) (last infixed)))

このマクロは中間記法で書かれたリストを正しい並びに再編成する。

(infix (1 + 1))
; => 2

関数とマクロが異なる点として、関数の場合引数は評価されてから関数内に渡されるのに対して、マクロの場合評価されずにマクロ内に渡されるという違いがある。 これを上記の例でみてみよう。 (1 + 1) を評価しようとすれば、エラーをうけとるであろう。しかし、マクロで呼び出してるので (1 + 1) は評価されずに infix に渡される。 そして、マクロは Clojureが評価できるように first, second, last を使ってリストを再編成している。 マクロ展開をつかえば、、次の通り再編成されていることがわかる。

(macroexpand '(infix (1 + 1)))
; => (+ 1 1)

また、関数のようにマクロ定義のときに分割代入(destructuring)を使うことができる。

(defmacro infix-2
  [[operand1 op operand2]]
  (list op operand1 operand2))

分割代入された引数は、渡されたシーケンス引数の順番に対応してバインドされる。 infix-2 でも引数にシーケンスを受け取り、はじめの値は operand1 という名前に、 2番めの値は、 op という名前に、3つ目の値は、 operand2 という名前にというように 順番に分割代入されていく

またマクロでは、arityも使うことができる。 and, or といった基本的な演算子も実はこれを利用したマクロになっている。 and マクロをみてみよう。

(defmacro and
  "Evaluates exprs one at a time, from left to right. If a form
  returns logical false (nil or false), and returns that value and
  doesn't evaluate any of the other expressions, otherwise it returns
  the value of the last expr. (and) returns true."
  {:added "1.0"}
  ([] true)
  ([x] x)
  ([x & next]
   `(let [and# ~x]
      (if and# (and ~@next) and#))))

これ理解するには、 `, ~@ といったシンボル(後で学習する)を理解しないといけないが、重要なのは3つのマクロ内容があるということである。 0アリティの場合常に true を返し、1アリティの場合は,引数それ自身を返し、nアリティの場合再帰的に自身を呼び出している。 関数と同じようにマクロにも再帰が利用でき、残引数(& next)が使える。

Building Lists for Evaluation

マクロを書くためには、Clojureが評価できるリストを構築しないといけないが、これには通常とは異なる考え方が必要になってくる。 そのひとつとして、クォート式をつかって最終的に構築されるリストが評価されないようにしないといけない。 もっと一般的にいえば, シンボル の違いに対してより深い注意が必要になってくる。

Distinguishing Symbols and Values

たとえば、式を引数に受け取り、その評価結果をコンソール出力させ、さらにその結果を返すマクロをつくりたいとしよう。 マクロに次のようなことをしてほしいわけだ。

(let [result expression]
  (println result)
  result)

評価のため list 関数を使って、そのままこれを次のようにマクロにするかもしれない。

(defmacro my-print-whoopsie
 [expression]
 (list let [result expression]
       (list println result)
       result))

だがこの試みは失敗し、 Can't take the value of a macro: #'clojure.core/let という例外を受け取る。 なぜだろうか?

理由は、letをシンボルのまま返してほしかったのに、 let の参照先を取得しようとしてるからである。 ほかにも、 result, println もシンボルではなくそれの参照先を取得しようとしている。 よって、次のように書けばうまくいく。

(defmacro my-print
  [expression]
  (list 'let ['result expression]
        (list 'println 'result)
        'result))

シンボルのまま扱いたい場合、シングルクォートを先頭につける。これでシンボルを評価させずそのまま扱うことができる。 そして評価させないようクォートをつかうことが、マクロを書くことの重要な考え方となる。 もう少し細かくみていこう。

Simple Quoting

マクロを書くためにはシンボルの評価をさけるためクォートを使用するが、ここでクォートについて簡単におさらいして、マクロ内でどのように使用されるのかを確認しよう。

まずはじめに、シンプルな関数をクォートなしで使うと次の通り。

(+ 1 2)
; => 3

これにクォートつけると、評価前のデータ構造そのまま取得できる

(quote (+ 1 2))
; => (+ 1 2)

このリスト内の + はシンボルである。もしこのシンボルを評価すると、加算関数が得られる

+
; => #function[clojure.core/+]

一方これをクォートすると、加算シンボルが得られる

(quote +)
; => +

バインドされていないシンボルを評価すると例外が発生する

sweating-to-the-oldies
; => Unable to resolve symbol: sweating-to-the-oldies in this context

一方クォートすると、たとえバインドされていなくても、シンボルそれ自身を返す。

(quote sweating-to-the-oldies)
; => sweating-to-the-oldies

シングルクォートは、 (quote x) のリーダーマクロである。

'(+ 1 2)
; => 3

'dr-jekyll-and-richard-simmons
; => dr-jekyll-and-richard-simmons

実際 when マクロでもこれを確認できる。

(defmacro when
  "Evaluates test. If logical true, evaluates body in an implicit do."
  {:added "1.0"}
  [test & body]
  (list 'if test (cons 'do body)))

注目すべきは、マクロ定義で、 if, do がクォートされていることである。 これによりシンボルをシンボルのままとして扱うことができる。 実際にマクロ展開してみてみよう。

(macroexpand '(when (the-cows-come :home)
                (call me :pappy)
                (slap me :silly)))
; => (if (the-cows-come :home) (do (call me :pappy) (slap me :silly)))

もうひとつ別の組み込みマクロ unless をみてみよう。

(defmacro unless
  "Invert 'if'"
  [test & branches]
  (conj (reverse branches) test 'if))

ここでも if はクォートされるので評価されず最終的に次のようなリストを返す。

(macroexpand '(unless (done-been slapped? me)
                      (slap me :silly)
                      (say "I reckon that'll learn me")))
; => (if (done-been slapped? me) (say "I reckon that'll learn me") (slap me :silly))

(conj '(1 2) 3 4) ; => (4 3 1 2) であることを忘れず。

マクロを書くならばこのようにクォートを利用することが多いが、syntax quoteというもっと強力なツールもある。

Syntax Quoting

Syntax Quote(以下準クォートと呼ぶ)は、通常のクォートと同様、データ構造を評価せずに返すが、大きくことなる2つの点がある。 ひとつめは、準クォートは名前空間を含んだフルパスのシンボルを返す。実際に比較してみよう。

名前空間を含めずにクォートをつかったときは、名前空間は含まない。

'+
; => +

名前空間を含めると、出力も名前空間が含まれる。

'clojure.core/+
; => clojure.core/+

準クォートをつかえば、必ずフルパスのシンボルになる。

`+
; => clojure.core/+

リストをクォートすれば、再帰的に働く。

'(+ 1 2)
; => (+ 1 2)

準クォートを使うと、全要素にその効果が働く

`(+ 1 2)

準クォートが名前空間を含むのは、名前衝突を回避するためである。

準クォートのもうひとつの異なる点というのは、チルダ ~ をつかってクォートを解除できることだ。 準クォートされたフォーム内でチルダが登場すると、評価しない、フルパスにするという機能が失われる。

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

チルダのあとに (inc 1) があるので、クォートされるのではなく、評価される。 チルダをつけなかったらフルパスのシンボルにして返す。

`(+ 1 (inc 1))
; => (clojure.core/+ 1 (clojure.core/inc 1))

文字列補間を知っているならば、準クォート・解除も同じようなものと考えられる。 どちらも、構造化された中にいくつかの変数をくみこんだテンプレートを作成する。 Rubyならば、文字列結合をつかって、 "Churn your butter, Jebedian!" を作成できる。

name = "Jebedian"
"Churn your butter, " + name + "!"

これを文字列補間を使って書けば、次のようにかける

"Churn your butter, #{name}!"

文字列補間のほうが、見やすいだろう。 これと同じで準クォート・解除で書いたほうが見やすくなる。

(list '+ 1 (inc 1))
; => (+ 1 2)

`(+ 1 ~(inc 1))
; => (clojure.core/+ 1 2)

準クォートを使ったほうが簡潔であり、また最終的に生成されるフォームに近いので理解しやすいのだ。

Using Syntax Qutoing in a Macro

準クォートのメカニズムを理解できたので、 code-critic マクロについて見てこう。 このマクロを準クォートを使ってより簡潔なものに書き直す。

(defmacro code-critic
  "Phrases are courtesy Hermes Conrad from Futurama"
  [bad good]
  (list 'do
        (list 'println
              "Great squid of Madrid, this is bad code:"
              (list 'quote bad))
        (list 'println
              "Sweet goriilla of Manila, this is good code:"
              (list 'quote good))))

(code-critic (1 + 1) (+ 1 1))
; => Great squid of Madrid, this is bad code: (1 + 1)
; => Sweet goriilla of Manila, this is good code: (+ 1 1)    

これを準クォートをつかって書き直すと、非常に見やすくなる。

(defmacro code-critic
  [bad good]
  `(do (println "Great squid of Madrid, this is bad code:"
                (quote ~bad))
       (println "Sweet goriilla of Manila, this is good code:"
                (quote ~good))))

ここでは、 good, bad シンボル以外をすべてクォートしてる。 はじめのコードでは個々の要素ごとにクォートしたりリスト関数をつかって明示的にその要素として記述するのに対して、 準クォートをつかった場合、 do 式をクォートして評価したい2つのシンボルだけクォート解除するだけでよい。

以上をまとめると、マクロは任意の未評価のデータ構造を引数に受けとり、Clojureが評価できるデータ構造を返すものである。 マクロ定義するとき、関数と同様、引数の分割代入や、 let を使用することができる。さらに、アリティや再帰的にマクロを呼び出すこともできる。

マクロはリストを返す。リスト構築するためには、 list 関数または準クォートが使用できるが後者をつかったほうが 簡潔なコードになる。そして、もし複数のフォームを返したい場合は、 do で囲ってやればよい。

Refacoring a Macro and Unquote Splicing

前節で扱った code-critic にはまだ改良の余地がある。 println が2回呼ばれていることである。これをきれいにしていこう。 まずはじめに、この println リストを作成するヘルパー関数を作成する。

(defn criticize-code
  [criticism code]
  `(println ~criticism (quote ~code)))

(defmacro code-critic
  [bad good]
  `(do ~(criticize-code "Cursed bacteria of Liberia, this is bad code: " bad)
       ~(criticize-code "Sweet sacred boa of Western and Eastern Samoa, this is good code:" good)))

注目すべきは、ヘルパー関数の criticize-code の戻り値が、準クォートのリストということである。 これでマクロが適切なリストを返してくれるようになる。

まだ改良の余地はあり、同じ関数を複数回呼び出している。これを回避するために、 map をつかって改善してみる。

(defmacro code-critic
  [bad good]
  `(do ~(map #(apply criticize-code %)
        [["Great squid of Madrid, this is bad code:" bad]
         ["Sweet gorila of Manila, this is good code:" good]])))

良さそうに見えるので、実行してみよう。

(code-critic (1 + 1) (+ 1 1))
; => (NullPointerException) at user/eval6154

残念だがうまくいかない。なぜだろうか? 問題は、 map が返す値がリストであること、この場合、 println 式をリストで囲ったものを返すことにある。 println の呼び出し結果を得たいのだが、それをリストの中にいれて評価しようとしてしまうのである。

次のようなコードが実行されてしまっているのである。

(do 
  ((clojure.core/println "criticism" '(1 + 1))
   (clojure.core/println "criticism" '(+ 1 1))))

println はリスト内にあるので評価されてしまう。

(do 
  (nil
   (clojure.core/println "criticism" '(+ 1 1))))

最終的に次のようになってしまう。

(do 
  (nil nil))

こういう仕組みで例外が発生してしまった。 printlnnil に評価されて、最終的に (nil nil) になり、 NullPointerException が発生する。

どういうことかわかりにくが、要するに返す値がリストになってることが問題。 printlnを括弧で囲わないでほしいのに、囲われてるので評価されてアウトというわけだ。 *1 => (nil) => error という流れになってる。

clojure (macroexpand '(code-critic (1 + 1) (+ 1 1)))

この問題を解決してくれるのが、unquote splicing(以下スプライシングと呼ぶ)である。 スプライシングは、 ~@ を使う。通常のクォート解除は次の通りになる。

`(+ ~(list 1 2 3))
; => (clojure.core/+ (1 2 3))

一方、スプライシングを使用すると、次のようになる。

`(+ ~@(list 1 2 3))
; => (clojure.core/+ 1 2 3)

スプライシングを使用すると、シーケンスデータの括弧がなくなり、準クォート内に配置される。 これをさきほどのマクロに使用すると以下の通り。

(defmacro code-critic
  [good bad]
  `(do ~@(map #(apply criticize-code %)
              [["Sweet lion on Zion, this is bad code:" bad]
               ["Great cow of Moscow, this is good code:" good]])))
(code-critic (1 + 1) (+ 1 1))

Things to Watch Out For

ここでは、マクロ使用時につまずくポイントとその回避方法について説明する。

variable Capture

Variable Captureはマクロ利用側が知らないうちに既存のバインディングを上書いてしまう現象である。 次のコードは、マクロ内の let で混乱した結果を招くものである。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (concat (list 'let ['message "Oh, big deal!"])
           stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))
; => Here's how I feel about that thing you did: Oh, big deal!

println がシンボル message を参照するとき、 "Good Job!" がバインドされていると思うかもしれない。 しかし、 with-mischief マクロが message に新たなバインディングを作成してしまうので期待通りにはいかない。

準クォートを使うと、例外が発生する。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  `(let [message "Oh, big deal!"]
        ~@stuff-to-do))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

この例外はありがたい。というのも、準クォートはマクロ内のVariable Captureの発生を防いでくれてるのからである。 もしマクロ内で let を使いたいならば、 gensym を使えばよい。 gensym は呼び出すたびにユニークなシンボルを生成してくれる。

(gensym)
; => G__6145

(gensym)
; => G__6148

(gensym) #+ENDSRC

G__6145G__6148

シンボルにプレフィックスをつけることもできる。

(gensym 'message)
; => message6151

(gensym 'message)
; => message6154

gensym をつかってマクロを書き直すと、次のとおりになる。

(def message "Good job!")
(defmacro with-mischief
  [& stuff-to-do]
  (let [macro-message (gensym 'message)]
    `(let [~macro-message "Oh, big deal!"]
        ~@stuff-to-do
        (println "I stil need to say:" ~macro-message))))

(with-mischief
  (println "Here's how I feel about that thing you did:" message))

gensym で新しいユニークなシンボルを生成して、それを macro-message にバインドする。 準クォート内の let では、 macro-message はクォート解除されて、gensymで生成されたシンボルとなる。 このシンボルは stuff-to-do とは異なるシンボルになるため、variable captureを回避できる。

こういったケースはよくあるので、 auto-gensym というもっと便利なものが使える

`(blarg blarg#)
; => blarg blarg__6168__auto__

`(let [name# "Larry Potter"] name#)
; => (clojure.core/let [name__6171__auto__ "Larry Potter"] name__6171__auto__)

順クォート内のシンボルにハッシュマーク(#)を付け加えることで、auto-gensymが作成される。 このように gensym やauto-gensymはマクロを書いてるときに陥るvariable captureを回避するときに必ず使用される。

Double Evaluation

variable captureの他にも double evaluation(二回評価) というトラップもマクロを書くときにある。 これは、マクロの引数のフォームが複数評価されてしまうものだ。

(defmacro report
  [to-try]
  `(if ~to-try
       (println (quote ~to-try) "was successful:" ~to-try)
       (println (quote ~to-try) "was not successful:" ~to-try)))

(report (do (Thread/sleep 1000) (+ 1 1)))

このマクロは、引数の真偽判定に応じて、成功・失敗どちらかの出力を行うものである。 (Thread/sleep 1000) は2回評価されてしまうので、2秒待たされてしまう。 1回目はifの直後、2回目は println が呼ばれたときである。 これが起きるのは、マクロ展開で (do (Thread/sleep 1000) (+ 1 1)) が複数現れてしまうからで、次のようなコードが実行されている。

(if (do (Thread/sleep 1000) (+ 1 1))
  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was successful:"
           (do (Thread/sleep 1000) (+ 1 1)))

  (println '(do (Thread/sleep 1000) (+ 1 1))
           "was not successful:"
           (do (Thread/sleep 1000) (+ 1 1))))

この問題を回避するには、次のようになる。

(defmacro report
  [to-try]
  `(let [result# ~to-try]
      (println result#)
      (if result#
      (println (quote ~to-try) "was successful:" result#)
      (println (quote ~to-try) "was not successful:" result#))))

(report (do (Thread/sleep 1000) (+ 1 1)))

to-trylet 内に配置することで、1回だけ評価されるようにして、その結果をauto-gensymで生成されたシンボル(result#)にバインドする。 これで2回評価されるこはなくなるわけだ。

Macros All the Way Down

マクロ展開が評価の前に行われることで、ハマってしまう問題もある。 たとえば、 report マクロを 単独で複数回呼び出せば問題ないが、 doseq をつかって繰り返し呼び出すときうまくいかないことがある。

(report (= 1 1))
; => (= 1 1) was successful: true

(report (= 1 2))
; => (= 1 2) was successful: false

doseq をつかえば次の通り、期待通りにいかない。

(doseq [code ['(= 1 1) '(= 1 2)]]
  (report code))
; => code was successful: (= 1 1)
; => code was successful: (= 1 2) おかしい

reportマクロを単体で呼び出したときはうまく機能するが、 doseq で繰り返したときは機能していない。 この理由は、 doseq の各イテレーションごとのマクロ展開をみてみるとわかる。

(if
 code
 (clojure.core/println 'code "was successful:" code)
 (clojure.core/println 'code "was not successful:" code))

report マクロの各イテレートの code シンボルは評価されない。 report のマクロ展開時には、評価結果にアクセスできないのだ。

これを解決するためには、次のようなマクロを書けばよい。

(defmacro doseq-macro
  [macroname & args]
  `(do 
      ~@(map (fn [arg] (list macroname arg)) args)))

(doseq-macro report (= 1 1) (= 1 2))
; => (= 1 1) was successful: true
; => (= 1 2) was not successful: false

Brews fro the Brave and True

ここでは、 if-valid というバリデーション機能をもったマクロを書いていく。 自分でマクロを書くのにとってもよい訓練となるだろう。

Validation Functions

シンプルに考えたいので、名前とメールアドレスのバリデーションについて考える。 我々のお店では、次のような注文明細を受け取るようにしたい。

(def order-details
  {:name "Mitchard Blimmons"
   :email "mitchard.blimmonsgmai.com"})

このハッシュマップのメールアドレスは@がないので無効であるが、これをバリデーションでつかまえたいのである。 理想的には、次のようなコードが書きたい。

(validate order-details order-details-validations)
; => {:email ["Your email address doesn't look like an email address."]}

検証対象のデータ(order-details)と、バリデーション定義(order-detail-validations)を引数にとる validate 関数を呼び出せるようにしたいのである。 関数適用結果として、無効なフィールドをkeyに、無効の理由をベクタとしてvalueにもつハッシュマップを受け取りたい。

早速 order-details-validations から実装していこう。

(def order-details-validations
  {:name
    ["Please enter a name" not-empty]
   :email
    ["Please enter an email address" not-empty

     "Your email address doesn't look like an email address"
     #(or (empty? %) (re-seq #"@" %))]})

検証したいkeyに、エラーメッセージと検証関数のペアをベクタが紐付いたハッシュマップである。 たとえば、 :name キーは、ひとつの検証関数(not-empty)をもち、 検証結果がエラーならば、 Please enter a name というエラーメッセージを受け取る

次に validate 関数を実装しよう。 validate 関数は2つの関数で構成され、 ひとつは該当フィールドに検証関数を適用する関数で、 もうひとつは、 {:email ["Your email address doesn't look like an email address."]} のように検証結果のエラーメッセージをひとつにまとめる役割を持つ関数である。

次の error-message-for は前者を実装したものである。

(defn error-messages-for
  "Return a seq of error messsage"
  [to-validate message-validator-pairs]
  (map first (filter #(not ((second %) to-validate))
                    (partition 2 message-validator-pairs))))

第一引数の to-validate は検証したい値である。第二引数の message-validator-pairs は偶数個の要素をもつシーケンスデータである。 (paritition 2 message-validator-pairs) でペアに分割され、第一要素がエラーメッセージ、第二要素が関数となる(order-details-validations と同じ並びになる)。

error-message-for 関数は、エラーメッセージと検証関数のペアごとに、 to-validate を検証してtrueになった場合ものを絞り込む。 そして、 map first をつかってエラーメッセージをのみを返す。

(error-messages-for "" ["Please enter a name" not-empty])
; => ("Please enter a name")

では、つぎにエラーメッセージをハッシュマップにまとめる関数を実装して仕上げよう。

(defn validate
 [to-validate validations]
 (reduce (fn [errors validation]
           (let [[fieldname validation-check-groups] validation
                 value (get to-validate fieldname)
                 error-message (error-message-for value validation-check-groups)]
             (if (empty? error-message)
              errors
             (assoc errors fieldname error-message))))
         {}
         validations))

(validate order-details order-details-validations)
; => {:email ("Your email address doesn't look like an email address")}

if-valid

完成したバリデーション関数は、大抵次のようにつかうだろう。

(let [errors (validate order-details order-details-validations)]
  (if (empty? errors)
    (println :success)
    (println :failure errors)))

共通してつかわれるところは、

  1. レコードを検証して、その結果を errors にバインドする
  2. エラーがあるかどうかチェックする
  3. もしエラーがなければ、それに応じた処理が実行される。ここでは、 (println :success) になる。
  4. エラーがあれば、それに応じた処理が実行される。ここでは、 (println :failure errors) になる。

これを本番コードに導入してると、同じようなコードを繰り返し書いてることに気がついて、抽象化が必要だと気づくようになった。 具体的には、 validate 関数を適用して、結果をあるシンボルにバインドして、バインド結果からエラーがあるかどうかを確認する部分である。 抽象化のために、次のようなコードを書くかもしれない。

(defn if-valid
  [record validations success-code failure-code]
  (let [errors (validate record validations)]
    (if (empty? errors)
      success-code
      failure-code)))

これはうまく動かない。というのも、 success-code, failure-code が評価されてしまうからである。 しかし、評価のコントールできるマクロをつかえば、うまく動作させられる。次のようにマクロを使えるようにしよう。

(if-valid order-details order-details-validations errors
  (render :success)
  (render :faliure errors))

このマクロは、繰り返しの部分をうまく回避し、実現したいことを簡潔に表現してくれる。 早速実装してみよう。

(defmacro if-valid
  "Handle validation more concisely"
  [to-validate validations error-name & then-else]
  `(let [~error-name (validate ~to-validate ~validations)]
     (if (empty? ~error-name)
       ~@then-else)))

このマクロは、 to-validate, validations, errors-name, then-else の4つの引数をもつ。 errors-name を使うのは新しい戦略である。 then-else 内でvalidate関数の結果を使用したいので、errors-nameを用意して検証結果バインドさせている。 マクロ展開すると、次のようになる。

(macroexpand
  '(if-valid order-details order-details-validations my-error-name
       (println :success)
       (println :failure my-error-name)))

; (let*
;   [my-error-name (user/validate order-details order-details-validations)]
;   (if (clojure.core/empty? my-error-name)
;     (println :success)
;     (println :failure my-error-name)))

準クォートで let/validate/if を使ったフォームを抽象化して、 さらにスプライシングif 分岐で使って、残引数の then-else を適切に評価できるようにしている。

Summary

本章では、マクロの書き方について学習してきた。マクロは関数定義とよく似ており、引数、docstring、処理内容がある。 また分割代入、残引数、再帰も使える。マクロはリストを返す。単純なマクロならば、 list, seq を使うときもあるが、 多くの場合、 ` 準クォートで書くことになるだろう。

マクロを書くときには、シンボルと値の違いを意識することが重要である。 マクロ展開されたあとに評価されるので、評価結果にマクロ内ではアクセスできない。 重複評価やvariable captureもトラップになるが、 let 式とgensymsを賢く使用すれば回避できる。

マクロはより自由にプログラムできる楽しいツールである。プログラマが評価をコントロールできるため、 他の言語では不可能な表現を可能にしてくれる。Clojureの学習のなかで、マクロは危険、つかうべきでないといった意見を聞くこともあるだろう。 しかし、少なくともはじめは、これらの意見に耳を傾けてはいけない。率先して使って楽しみなさい。 使ってみないとマクロの最適な箇所はわからないからである。そのうちに反対派の意見もわかってくるようになるからだ。

*1:println …

CLOJURE for the BRAVE and TRUEでClojure学習 part6

はじめに

久しぶりの投稿になったがClojure の学習備忘録。 今回はChapter 7: Clojure Alchemy: Reading, Evaluation, and Macrosで以下を学ぶ

  • Clojureの評価モデル
  • Readerについて
  • Evaluatorについて

評価モデルこそ、Lispが他の言語と大きく異なる特徴的な部分だと理解できる。そしてマクロがどのように振る舞うのかというメカニズムについて理解深まる面白いパートである。

またスペシャルフォームが、なぜスペシャルと呼ばれるのかを理解できたことはうれしい章である。

目次

Clojure Alchemy: Reading, Evaluation, and Macros

賢者の石といえば、錬金術で最も有名なアイテムで、鉛から金を変化できるものだ。 Clojureには、賢者の石のように都合よく変更できるツール、マクロがある。

マクロを使えば、どんな表現でもClojureが実行できるもの変更してくれるので、 必要に応じて言語自体を拡張させることができる。

実際マクロの力をみてみよう。

(defmacro backwards
  [form]
  (reverse form))

(backwards (" backwards" " am" " I" str))

(" backwards" " am" " I" str)Clojureの文法に違反してるので評価できないのだが、 backwards マクロをつかえば、Clojureは正しく評価してくれる。 賢者の石が鉛から金を生成できるように、 backwards で新しい文法を作り出すこと、つまり好きなようにClojureを拡張できる。

本章では、マクロがかけるための概念的な部分について、 Clojureの評価モデル, reader, evaluator, macro expander について説明する。

An Overview of Clojure’s Evaluation Model

他のLisp言語と同様Clojureも、他の多くの言語と異なる評価モデルをもっており、 ソースコードread し、Clojureのデータ構造を作り出すフェーズがある。

その後データ構造は 評価 (evaluate)される。 Clojureはデータ構造を走査して、関数適用だったり、データ構造の種別に応じた変数探索といったアクションを行う。 たとえば、 (+ 1 2) という文字列が読み込まれると(read)、その結果は、第一要素の + シンボルのあとに、数値の1,2がつづくリストと分析される。 リストが評価(evaluate)されると、 + に一致する関数を見つけ出し、1,2が適用される。

このようなソースコードと評価の関係をもつ言語は、同図像性をもつ(ホモイコニック)と呼ばれる。 ホモイコニック言語では、コードはプログラマが操作できるデータ構造として推論される。

プログラミング言語には、コンパイラインタプリタがあり、それはユニコードで構成されたコードをマシン語に変換してくれる。 この変換で、 abstract syntax tree(AST) が構築され、あるデータ構造として表現される。 /evaluator/は、このASTをインプットとして受け取り、その木構造を操作してマシンコードなどをアウトプットとして生成してくれる。

多くの言語では、ASTのデータ構造にプログラミング言語からアクセスすることはできない。 プログラミング言語領域とコンパイラ領域は完全に分かたれ、合流することはありえない。 次の図は、非Lisp言語のコンパイルプロセスを示したものである。

f:id:poppon555:20200827135625j:plain

一方、ClojureLisp言語の場合は違う。 アクセス不可のASTを評価するのではなく、Lisp言語は素のデータ構造を評価する。 Clojure木構造を評価するが、木構造Clojureのリストや値で構成されている。

リストは、木構造を構成するに理想的な表現である。第一要素はルートとして、その後に続く要素はそのブランチとして扱われる。 ネストした木構造をつくりたければ、次の図の通り、ネストしたリストをつくればよい。

f:id:poppon555:20200827135722j:plain

まずはじめに、Clojurereader は 文字列の (+ 1 (* 6 7)) をネストしたリストに変換する。 そして、Clojureevaluator はそれを入力として受け取って、その評価結果を出力する。

f:id:poppon555:20200827135655j:plain

しかし、/evaluator/ はどこからのインプットなのかに関心をもたないので、 インプットが reader である必要はない。 そのため、 eval を使えば、プログラマが書いたコードを evaluator に直接送ることがができる。

(def addition-list (list + 1 2))
(eval addition-list)
; => 3

このとおり、コードからClojureのリストを評価できた。 詳細の説明については追々していくとして、ここでは簡単な説明に留めると、 まずClojureはリストを評価して、 addition-list が参照するリストをみつだす。 そして、 + シンボルに対応する関数をみつけ、 1, 2 を引数としてその関数に適用する。 これは、プログラマのデータ構造であれ、/evaluator/ のデータ構造であれ同じである。 その結果、Clojureの力を最大限を発揮させることができるようになる。

(eval (concat addition-list [10]))
; => 13

(eval (list 'def 'lucky-number (concat addition-list [10])))
; => #'user/lucky-number

lucky-number
; => 13

このとおり直接 evaluator にコードを送ると、実行可能な形式に関数やデータが修正される。

Clojureはホモイコニックな言語である。つまり、コードはリスト形式で記述されるが、それがもうAST表現になっている。 自分が書いたコードが、 evaluator によって評価されるデータ構造と同じ表現であるため、 プログラムがどのように修正されるのか推論するのがたやすくなる。

マクロはこの変換をプログラマから簡単に行えるようにしたものである。 マクロについてより詳細に理解できるようにするため、残りはClojurereader, evaluator について説明する。

The Reader

リーダは、ファイルやREPLに記述されたコードをClojureのデータ構造に変換する。 プログラマが書いたユニコードを文字列を、Clojureの世界のリストや、ベクタ、マップ、シンボルなどに変換してくれる。 ここでは、リーダを直接操作しながら、より簡潔なコードを可能にしてくれる リーダマクロ について学習しよう。

Reading

REPLを起動して、以下の文字列を入力してみよう。

(str "To understand what recursion is," " you must first understand recursion.")

REPLに入力したものはただのユニコードの文字列なのだが、Clojureのデータ構造として判断される。 データ構造を文字列で表記したものは、 reader form と呼ばれる。 この例では、 str とその他2つの文字列からなるフォームを要素に持つリストを表現したフォームということになる。

この文字列がリーダに渡ると、対応するClojureのデータ構造が作られ、さらにそれが評価されると次の通り出力される。

"To understand what recursion is, you must first understand recursion."

読み込み(reading)と評価(evaluation)は、異なるプロセスなので、独立させて実行させることができる。 reader プロセスを直接操作するには、 read-string をつかう。 read-string に文字列を渡すと、リーダと同じようにデータ構造を返す。

(read-string "(+ 1 2)") ; 文字列のリストをわたす
; => (+ 1 2)            ; Clojureのリストに変換される

(list? (read-string "(+ 1 2)"))
; => true  変換したデータ構造がリストであること確認できる

(conj (read-string "(+ 1 2)") :zagglewag) ; Clojureのリストなのでconj関数も適用できる
; => (:zagglewag + 1 2)

そしてreaderの出力結果を評価することもできる。

(eval (read-string "(+ 1 2)"))
; => 3

ここまで紹介してきたものは、リーダフォームとデータ構造が一対一の対応関係があるものになる。 もう少しこの対応関係を紹介する。

  • () はリストのリーダフォーム
  • str はシンボルのリーダフォーム
  • [1 2] は2つの数値リーダフォームからなるベクタリーダフォーム
  • {:sound "hoot"} はキーリーダフォームと文字列リーダーフォームからなるマップリーダフォーム

繰り返しになるが、リーダフォームとは、Clojureのデータ構造を文字列表記したもの

しかしながら、リーダはもうすこし複雑な挙動をするときがある。

(#(+ 1 %) 3)
; => 4

これをリーダに読み込ませてみよう。

(read-string "(#(+ 1 %) 3)")
; => '((fn* (p1__6174#) (+ 1 p1__6174#)) 3)

これが一対一しない場合である。 (#(+ 1 %) 3) を読み込んむとリストを出力するが、 それは、 fn* シンボルと、シンボルを要素に持つベクタ、さらに3要素をもつリストをもっているわけだ。 どういうことだろうか?

Reader Macro

これの答えは、 reader macro によって (#(+ 1 %) 3) は変換されたということになる。 リーダマクロは、テキストをデータ構造に変換する一連のルールをもっている。 リーダマクロのおかげで、省略リーダフォームが展開されて、コードを簡潔にかける。 省略リーダフォームは、 macro character と設計されており、 ', #, @ が代表例である。

たとえば、シングルクォートは、次のように展開される。

(read-string "'(a b c)")
; => (quote (a b c))

deref リーダマクロも同じで、 @ のときに展開される。

(read-string "@var")
; => (clojure.core/deref var)

コメントアウト部分は省略して展開してくれる。

(read-string "; ignore!\n(+ 1 2)")
; => (+ 1 2)

以上が、リーダについてになる。次は、評価(evaluator)についてみていこう。

The Evaluator

f:id:poppon555:20200827135209j:plain

Clojureevaluator は、引数にデータ構造をうけとり、データ構造の種別に応じたルールを適用して、結果を返す関数と考えてよい。 シンボルを評価するときは、Clojureはシンボルの参照先をみつけだす。リストを評価するときは、リストの第一要素をみて関数、マクロ、スペシャルフォームらを呼び出す。 文字列や、数値、キーワードといった他の値は、それ自身として評価される。

たとえば、 (+ 1 2) とREPLに入力したときを考える。 上図に示したデータ構造が、 evaluator の送られる。

リストが送られたので、 evaluator はその第一要素の評価からスタートする。第一要素は+シンボルなので、対応する関数をみつけて次の処理に進む。 第一要素は関数だとわかったので、次にそのオペランドを評価しにいく。オペランドの1, 2は、リストやシンボルではないので、そのまま評価される。 最後に1,2をオペランドとして+関数に適用し、その結果を返す。

ここからは、 evaluator がそれぞれのデータ構造をどのように評価していくのかを詳しく紹介していく。

These Things Evaluate to Themselves

リストやシンボルではないデータ構造を評価するときは、データ構造それ自体が出力される。

true
; => true

false
; => false

{}
; => {}

:huzzah
; => :huzzah

空のリストも同様である。

()
; => ()

Symbols

プログラマの重要タスクの一つは、値と名前の関連づけであり、chapter3では, def, let や関数定義で学習したことである。 シンボル を使えば、関数、マクロ、データ、その他なんであれ名前をつけることがができ、その名前で評価することできる。 シンボルを評価するとき、Clojure名前空間からそれと関連付けられた値を見つけ出す。 最終的にシンボルは、 , スペシャルフォーム のいずれかとして判定される。 スペシャルフォームとは、Clojureに組み込まれた基本的な演算子のことである。

一般的にClojureがシンボルを評価するときは、次の通りになる。

  1. シンボルがスペシャルフォームかどうかを判定する。そうでないならば、
  2. ローカルバインドされたものかどうか判定する。そうでないならば、
  3. def で定義されて名前空間に存在するかどうか判定する。そうでないならば、
  4. 例外をなげる

まずスペシャルフォームの場合についてみてみよう。 if のようなスペシャルフォームは必ず演算子として使用されるため、リストの第一要素に現れる。

(if true :a :b)
; => :a

この例でも、ifはスペシャルフォームであり、演算子として使用されていることがわかる。もしスペシャルフォームをリストなしに使用すると例外が返される。

if
; => Unable to resolve symbol: if in this context

次は、ローカルバインディングについてみてみよう。/ローカルバインディング/ とは、 def を使わないでシンボルと値を関連付けさせることである。 例では、 let を使用してシンボル x に5をバインドしてる。 x を評価すると、シンボルxは、5として現れる。

(let [x 5]
  (+ x 3))
; => 8

名前空間上にxと15を対応付ければ、次の通りになる

(def x 15)
(+ x 3)
; => 18

次の例では、xは15にマッピングされているが、xは let で5にローカルバインディングされてもいる。 この場合、 let が優先される。

(def x 15)
(let [x 5]
  (+ x 3))
; => 8

ネストした場合、直近にあるバインディングが利用される。

(let [x 5]
  (let [x 6]
    (+ x 3)))
; => 9

関数もローカルバインディングを行っており、渡されたパラメータを関数内の引数にバインディングしてる。 次の例では、 exclaim は関数にマッピングされてる。関数内の exclamation パラメータは、関数の引数として渡されたものにバインドされる。

(defn exclaim
  [exclamation]
  (str exclamation "!"))

(exclaim "Hadoken")
; => "Hadoken!"

最後に、 map, inc について見てみよう。

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

Clojureがこのコードを評価するとき、まず map シンボルを評価し、その参照先関数を見つ出し、引数をそれに適用する。 map シンボルはmap関数を参照するが、関数それ自体ではないことに注意しよう。 文字列 fried saled がデータ構造であるのと同様、 map シンボルもデータ構造なのであり、関数それ自体ではないのだ。

(read-string ("+"))
; => +

(type (read-string "+"))
; => clojure.lang.Symbol

(list (read-string "+") 1 2)
; => (+ 1 2)

この例でも、プラスのシンボル + をデータ構造として扱ってのであり、参照する関数として扱ってるわけではない。 評価してはじめて、Clojureは参照する関数を見つけ出し、適用する。

(eval (list (read-string "+") 1 2))
; => 3

Lists

空リストを評価すれば、空のリストが返る

(eval (read-string "()"))

空でなければ、リストの第一要素を呼び出そうとするが、第一要素の性質によって異なってくる。

Function Calls

関数呼び出しならば、全オペランドが評価された後、関数の引数として渡される。 たとえば、 + シンボルの場合をみてみよう。

(+ 1 2)
; => 3

Clojureはリストの先頭が関数だとわかれば、続いて残りの要素を評価する。 オペランドの1,2は、シンボルやリストでないのでそれ自身として評価されて、そしてそれを関数に適用する。

ネストした関数呼び出しの場合はどうだろうか。

(+ 1 (+ 2 3))
; => 6

第2引数がリストになっているが、Clojureの先程と同じプロセスを踏む。 + シンボルを見つけては、その引数を評価する。 (+ 2 3) を評価するときは、第一要素を関数として判定し、残りの要素も評価する。このように再帰的に評価される。

Sepecial Forms

スペシャルフォームは、関数で実装できない機能をもつのでスペシャルと呼ばれる。

(if true 1 2)

この例では、シンボル if で始まるリストを評価している。 if シンボルはスペシャルフォームをつくるので、 true, 1, 2オペランドとして評価する。

スペシャルフォームの評価は、関数評価と異なる。たとえば、関数を呼び出したとき、全オペランドが評価される。 しかし if を使用するときは、特定のオペランドを評価してほしい場合となる。条件の真偽に応じて、特定のオペランドのみ評価させることができるのだ。

quote スペシャルフォームについてもみてみよう。

'(a b c)

これはリーダマクロによって、以下のように表現される。

(quote (a b c))

今まで見てきたとおり普通、 a シンボルがリストの第一要素なので、それを呼び出そうとする。 しかし、 quote スペシャルフォームは、「次のデータ構造を通常通り評価するのではなく、データ構造そのものを返してほしい]と評価方法をかえる。 その結果、シンボル a, b, c からなるリストを受け取る。

def, let, loop, fn, do, recur も同様で、関数と同じやり方で評価しない。 たとえば、 def, let を評価するときも、それの参照先をみつけたりしない。代わりに、シンボルと値の関連付けを作成してくれる。 このように、リーダからデータ構造が送られてくると、シンボルを走査してリストの先頭要素の関数やスペシャルフォームを呼び出す。 またマクロをリスト先頭要素にすると、また新しい評価方法が得られる。

Macros

マクロを理解するには、実際にコードをみてみよう。たとえば、Clojureで、 (+ 1 1) ではなく (1 + 1)中置記法で評価してほしいとする。 これはマクロではない。むしろ、ただ中置記法でかいたら評価できるようによしなに変更してほしいということを示したいだけである。

(read-string "(1 + 1)")
; => (1 + 1)

このリストを評価すれば例外をなげる

(eval (read-string "(1 + 1)"))
; => ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn

しかし read-string が返すリストを評価できるよう適切に変換すれば、うまくいく。

(let [infix (read-string "(1 + 1)")]
  (list (second infix) (first infix) (last infix)))
; => (+ 1 1)     

これを評価すればもちもん2が出力される。

(eval
  (let [infix (read-string "(1 + 1)")]
  (list (second infix) (first infix) (last infix))))
; => 2     

うまくいったが不格好だ。そこでマクロなのである。マクロを使えば、Clojureが評価する前にリストを操作することができる。 マクロは関数に似ており、引数と返り値をもつ。また関数と同様、データ構造として機能する。 マクロがユニークなのは、評価プロセスに適するように振る舞えることである。 マクロは、 readerevaluator の間に位置するので、リーダが読み取ったデータ構造を evaluator にわたす前に変換することができる。

例をみてみよう。

(defmacro ignore-last-operand
  [function-call]
  (butlast function-call))

(ignore-last-operand (+ 1 2 10)) ;1
; => 3

(ignore-last-operand (+ 1 2 (println "look at me!!!")))
; => 3

1で、 ignore-last-operand マクロが引数として受け取るのは、評価後の13ではなく、 (+ 1 2 10) リストそれ自体である。 この振る舞いは、関数のと異なるものである。というのも、関数は呼び出す前に、全引数を評価し、ある引数は評価したり、評価しなかったりすることは不可能だからである。 それに対して、マクロの場合、オペランドは評価されない。具体的言うと、シンボルの参照先を解決せず、シンボルをそのまま受け取る。それはリストでも同じである。 つまり、リストの第一要素が、関数や、スペシャルフォーム、マクロとしてではなく、未評価のリストデータ構造として受け取る。

もうひとつ関数と異なるのは、関数の戻り値のデータ構造は評価されないが、マクロの場合評価される。 マクロの返り値が計算されるプロセスは、 マクロ展開(macro expansion) と呼ばれ、 macroexpand をつかえば、マクロが返す評価される前のデータ構造を確認することができる。

(macroexpand '(ignore-last-operand (+ 1 2 10)))
; => (+ 1 2)

(macroexpand '(ignore-last-operand (+ 1 2 (println "look at me!!!"))))
; => (+ 1 2)

ご覧の通り、どちらの結果も (+ 1 2) というリストになる。このリストが評価されると、3が出力されるわけだ。

(defmacro infix
  [infixed]
  (list (second infixed) 
        (first infixed) 
        (last infixed)))

(infix (1 + 2))
; => 3     

f:id:poppon555:20200827135554j:plain

マクロ展開のプロセスを絵にすると、上図のとおりである。

以上が評価プロセスにフィットさせるようなマクロの振る舞いである。 マクロのありがたみというのは、 (1 + 2) といったデータ構造をClojureが評価可能な形 (+ 1 2) に変更してくれることになる。 これが意味することは、プログラマClojure自身を拡張させるさせることができること、すなわちマクロが 文法抽象(syntactic abstraction) を可能にしているということである。

Syntactic Abstraction and the -> Macro

Clojureのコードは、関数呼び出しがネストする場合がよくあり、たとえば、次のようなコードもそうである。

(defn read-resource
  "Read a resource into a string"
  [path]
  (read-string (slurp (clojure.java.io/resource path))))

関数の中身を理解するには、括弧の一番深いところ、 (clojure.java.io/resorce path) をみつけて、 右から左へと関数の戻り値が次の関数の引数に送られていく流れを追わないといけない。 この右から左にコードを読んでいくという流れは、非Lispプログラマの読み方と逆になっている。 もっともClojureの書き方になれれば問題ではないが、左から右へ、上から下へコードを読んでいきたいなら、 -> マクロが使える。 このマクロは、スレッディングマクロ、あるいはスタビーマクロと呼ばれ、先程の処理を次のように書き直せる。

(defn read-source
  [path]
  (-> path
      clojure.java.io/resorce
      slurp
      read-string))

これなら最深括弧から外側の括弧へではなく、上から下に読むことができるわけだ。 まず、 pathio/resource の引数として渡され、その関数適用結果が slurp の引数として渡り、最後にslurpの出力結果が read-string の引数に渡っていく。

どちらのコードでも処理は同じになるが、スレッディングマクロを使ったほうが、上から下へという普段から慣れた書き方なので理解しやすい。 また -> マクロで括弧も省略されてるので、ビジュアル的にもみやすくもなっている。 これが 文法抽象 の力であり、Clojureの組み込み文法とは異なる書いたものを、人間が理解できる書き方で記述できるようにしてくれるのだ。

CLOJURE for the BRAVE and TRUEでClojure学習 part5

はじめに

Clojure の学習備忘録。
今回はChatper 6: Organizing Your Project: A Librarian’s Taleで以下を学ぶ

前半が、名前空間についての説明で、
後半は、lein new app で作成されたプロジェクトで、名前空間(nsマクロ)の使い方が紹介される。

また本章では、Clojureの体系的なシステムを図書館システムのように機能していると説明される。

目次

Your Project as a Library

現実世界の図書館には、本や雑誌、DVDなどが保管されている。
そこでは住所システムが用いられ、住所がわかればそこに足を運んで、目的の品を取得できる。

もちろん、目的の品がどこにあるのかを人間が知っている必要はない。 代わりに図書館が住所と図書のタイトルを関連付けて、それを検索できるツールを提供してくれる。

Clojureでも同じようなことをやってくれるので、Clojureも順序づけられた広大な棚に格納されたオブジェクトのように考えられる。 プログラマはオブジェクトがどこの棚に格納されているかをしる必要はない。 代わりに、目的のものを取得できるように目印をClojureに与えることができる。

そのため、Clojureは目印と棚の住所との関連づけを管理する必要があり、それが名前空間で実現可能となる。 名前空間は、シンボルと棚の住所への参照先の対応付けを人間が理解できるようにしてくれるわけだ。

技術的には、名前空間clojure.lang.Namespace という種類のオブジェクトであり、他のデータ構造と同じようにあつかうことができる。 たとえば、 *ns* で現在の名前空間を参照することができ、 (ns-name *ns*) でその名前を取得できる。

(ns-name *ns*)
; => user

たとえば、REPLを開始すると、 user という名前空間にいる。プロンプトには、 user=> のように現在の名前空間が表示される。

名前空間は、ハードウェアの制約を無視すれば、いくつでも作成できる。Clojureのプログラムでは、常にどこかの名前空間に所属する。

シンボルに関していうと、今まで名前空間を意識することなくシンボルを使ってきた。たとえば、 (map inc [1,2]) と書いたとき map, inc は両方ともシンボルである。これら map などのシンボルをClojureに与えてやると、現在の名前空間内で対応する変数をみつけ、 変数に書かれてある棚の住所を取得して、棚から目的のものを(この場合、 map が参照してる関数を)取得してくれる。 もしシンボルの参照先ではなく、シンボルそれ自体を使用したいのであれば、クォートしなければならない。 Clojureのフォームをクォートすれば、そのフォームは評価されず、データとして扱われる。 次は、REPL上でクォート化したフォームを評価したサンプルである。

inc
; => #function[clojure.core/inc]
; incが参照してる関数のテキスト表記が得られる。

'inc
; => inc
; シンボル名incが得られる

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

'(map inc [1 2])
; => (map inc [1 2])
; map, inc, ベクタが評価されずそのまま出力される

Strong Objects with def

Clojureでオブジェクトを棚に保管するもっともよく使うツールは、 def である。 他にも defn もあるが、これも内部で def が使用されている。

(def great-books ["East of Eden" "The Glass Bead Game"])
; => #'user/great-books

great-books
; => ["East of Eden" "The Glass Bead Game"]

このコードは、Clojureに次のように命じている

  1. 現在の名前空間にある対応関係において great-books と変数の関連付けを更新しなさい
  2. 保管スペースがある棚を見つけなさい
  3. その棚に、 ["East of Eden" "The Glass Bead Game"] を保管しなさい
  4. その棚のアドレス(住所)を変数に書き込みなさい
  5. 書き込んだ変数(この場合, #user/great-books)を返しなさい

この処理は、変数の閉じ込めと呼ばれ、 ns-intern名前空間の対応関係にアクセスすることできる。

(ns-interns *ns*)
; => {great-books #'user/great-books}

get 関数で、特定の変数を取得できる

(get (ns-interns *ns*) 'great-books)
; => #'user/great-books

(ns-map *ns*) を使っても同様のことができる。

#'user/great-books は変数のリーダーフォームであるが、リーダフォームについては別のchapterで説明するとして、 名前空間 user にあるシンボル great-books にひもづく変数を指している。 deref で変数の参照先を取得できる。

(deref #'user/great-books)
; => ["East of Eden" "The Glass Bead Game"]

このフォームの意味するところは、

  1. 変数から棚の番地を取得して、
  2. そこまで足を運んで、
  3. 目的地にあるものを取得して、
  4. 私にくれ

ということをClojureに命じているのである。

ただつぎのように使うのが一般的だ。

great-books
; => ["East of Eden" "The Glass Bead Game"]

では、もう一度同じシンボルに対して def を使用するとどうなるだろうか?

(def great-books ["The Power of Bees" "Journey to Upstairs"])
great-books
; => ["The Power of Bees" "Journey to Upstairs"]

変数は、新しいベクタの住所で上書きされてしまう。棚の住所がかき消されて新しい住所が書き込まれてしまったわけである。 もはやはじめにセットしたベクタにはアクセスできなくなってしまう。これが名前衝突と呼ばれるものであり、混乱をまねく原因となる。

もっともこれ自体は、javascriptRubyなどの言語でも起こりうることではあるが、何が問題かといえば、 無意識に自分で書いたコードが上書きされてしまうため、サードパーティのライブラリにも自分のコードが入っていないという保証がなくなってしまう。 これを回避するためにClojureでは、名前空間を利用する。

Creating and Switch to Namespaces

Clojureには名前空間を作成する3つツールがある。 create-ns 関数, in-ns 関数、そして ns マクロである。 実際の開発で使うのは ns になるが、その説明よりも先に他の2つを先に説明する。

create-ns はシンボルを受け取って、同名の名前空間が存在しないならば、新たにシンボル名の名前空間を作成して返す。

(create-ns 'cheese.taxonomy)
; => #namespace[cheese.taxonomy]

ns-name で、名前空間名を取得できる

(ns-name (create-ns 'cheese.taxonomy))
; => cheese.taxonomy

実際には、 create-ns は使わない。というのも、名前空間を作成するだけで、そこに移動しないのは不便だからである。 in-ns がもっと一般的で,新しい名前空間を作成し、移動までしてくれる。

(in-ns 'cheese.analysis)
; => #namespace[cheese.analysis]

実行後REPLプロンプトは、 cheese.analysis> と表示されるように、実際に名前空間を移動したことを示している。 ここで def を使えば、 cheese.analysis という名前空間上にオブジェクトを保管することができる。

しかし、他の名前空間にあるデータや関数を使いたいときどうすればよいのだろうか? そのためには、 名前空間/シンボル のようにフルパスでシンボルを使えばよい。

(in-ns 'cheese.taxonomy) ; cheese.taxonomyを作成して移動
(def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
(in-ns 'cheese.analysis) ; cheese.analysisを作成して移動
cheddars
; => Unable to resolve symbol: cheddars in this context

この通り別の名前空間で定義された cheddars をそのままでは呼び出すことできないが、次であれば可能になる。

cheese.taxonomy/cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

このようにフルパスシンボルでならアクセスできるが、毎回入力するとなるとめんどくさい。 そのためClojureでは、 refer, alias というツールを用意してくれてる。

refer

refer は他の名前空間にあるオブジェクトを細かいレベルでコントールすることができる。

(in-ns 'cheese-taxonomy)
(def cheddars ["mild" "medium" "strong" "sharp" "extra sharp"])
(def bries ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"])
(in-ns 'cheese.analysis)
(clojure.core/refer 'cheese.taxonomy)
bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"] ; 別の名前空間の変数にアクセスできる

cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

refer をつかえば、フルパスで指定しなくても別の名前空間のシンボルにアクセスできる。 これが可能となっているのは、現在の名前空間のシンボル/オブジェクト対応表が更新されているからである。 次のコードを見てみよう。

(clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'bries)
; => #'cheese.taxonomy/bries

(clojure.core/get (clojure.core/ns-map clojure.core/*ns*) 'cheddars)
; => #'cheese.taxonomy/cheddars

つまり次のようなことをやってくれているわけだ。

  1. cheese.taxonomy 上で ns-interns を呼ぶ
  2. それを現在の名前空間ns-map とマージする
  3. マージした結果を新しい ns-map として現在の名前空間に作成する

refer を使うときに、フィルター用に :only, :exclude, :rename が使える。 名前から分かる通り、 :only, :exclude は現在の名前空間にマージするものを制限することができる。 :rename は別名で現在の名前空間にマージできるようになる。

:only は次のように使える

(clojure.core/refer 'cheese.taxonomy :only ['bries])
bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

cheddars
; => => RuntimeException: Unable to resolve symbol: cheddars

:exclude は次のように使える

(clojure.core/refer 'cheese.taxonomy :exclude ['bries])
bries
; => RuntimeException: Unable to resolve symbol: bries
cheddars
; => ["mild" "medium" "strong" "sharp" "extra sharp"]

:rename は次のように使える

(clojure.core/refer 'cheese.taxonomy :rename {'bries 'yummy-bries})
bries
; => RuntimeException: Unable to resolve symbol: bries
yummy-bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

同一名前空間からしか使用できない関数をつくりたいときもあるだろう。 そんなときは、 defn- を使用してprivateな関数をつくればよい。

(in-ns 'cheese.analysis)
;; Notice the dash after "defn"
(defn- private-function
  "Just an example function that does nothing"
  [])
(in-ns 'cheese.taxonomy)
(clojure.core/refer-clojure)
(cheese.analysis/private-function)
(refer 'cheese.analysis :only ['private-function])

たとえ refer を使っても、別の名前空間のprivate関数を利用できない。

alias

refer と比べると、 alias はもっとシンプルである。名前空間に短縮(ショートカット)名をつけるだけである。

(clojure.core/alias 'taxonomy 'cheese.taxonomy)
taxonomy/bries
; => ["Wisconsin" "Somerset" "Brie de Meaux" "Brie de Melun"]

cheese.taxonomy ではななく、 より短い言葉 taxonomy でシンボルにアクセスできるようになる。 おかげでREPL開発が捗る。

しかし、REPL上で全プログラムを書きあげてしまうことはないだろう。 よって次のセクションでは、現実のプロジェクトで必要になることを説明しよう。

Real Project Organization

ようやく準備が整ったので、ここからは実際のプロジェクトで今まで学習してきたことを生かしていこう。 ファイルパスと名前空間の対応関係について、 require, user を使ったファイルのロード、 そして ns を使った名前空間のセットアップ方法について説明する。

The Relationship Between File Paths and Namespace Names

2羽の鳥によって大切なチーズが盗まれてしまった。 このやっかいなチーズ泥棒を捕まえるために盗まれてしまった場所をマッピングする プロジェクトを作ってみる。

lein new app the-divine-cheese-code

ディレクトリ構造はいかのように作られるだろう。

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── the_divine_cheese_code
│       └── core.clj
└── test
    └── the_divine_cheese_code
        └── core_test.clj

src/the_divien_cheese_code/core.cljを開こう。一行目は次の通り記述されてるだろう。

(ns the-divine-cheese-code.core
  (:gen-class))

今まで名前空間を作成には in-ns を使ってきたが、 ns名前空間の作成・管理でもっとも使われるので これについて説明していく。一方 (:gen-class) はchapter12で説明する。

今の名前空間the-divine-cheese-code.core である。 Clojureでは、名前空間とファイルのパス名に次のような一対一の関係がある。

それでは the-divine-cheese-code.visualization.svg を作ってみよう。

mkdir src/the_divien_cheese_code/visualization
src/the_divine_cheese_code/visualization/svg.clj

次に、 require, use について見てみる。

Requiring and Using Namespaces

名前空間 the-divine-cheese-code.core から 名前空間 the-divine-cheese-code.visualization.svgsvgマークアップ関数を使いたいときは、 require を使えば良い。だがその前に、 svg.clj を実装しよう。

(ns the-divine-cheese-code.visualization.svg)

(defn latlng->point
  "Convert lat/lng map to comma-separated string"
  [latlng]
  (str (:lat latlng) "," (:lng latlng)))

(defn points
  [locations]
  (clojure.string/join " " (map latlng->point locations)))

latling->point, points を定義した。これらは、シーケンス化された緯度経度情報を文字列に変換する関数である。 この関数を、 core.clj から使用したい場合、 require を使う。 require名前空間のシンボルを引数に取るので、 (require 'the-divine-cheese-code.visualization.svg) のように記述すれば、 Clojureは該当のファイルを名を読み込んで評価してくれる。 評価されると、 the-divine-cheese-code.visualization.svg という名前空間が作成されて、そこに前述の関数が定義される。 適切なパスにcljを格納しても自動でClojure読み込んでくれるわけではなく、プログラマが明示的にClojureに教えてあげるないといけない。

さらに、フルパスで指定しなくてすむように refer を使う。 これで別の名前空間の関数を呼べるようになったわけなので、 heists シーケンスを core.clj に加えて確認しよう。

(ns the-divine-cheese-code.core)
;; SVGコードを評価する
(require 'the-divine-cheese-code.visualization.svg)
;; フルパス指定しなくて済ませるためにreferを使う
(refer 'the-divine-cheese-code.visualization.svg)

(def heists [{:location "Cologne, Germany"
              :cheese-name "Archbishop Hildebold's Cheese Pretzel"
              :lat 50.95
              :lng 6.97}
             {:location "Zurich, Switzerland"
              :cheese-name "The Standard Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Marseille, France"
              :cheese-name "Le Fromage de Cosquer"
              :lat 43.30
              :lng 5.37}
             {:location "Zurich, Switzerland"
              :cheese-name "The Lesser Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Vatican City"
              :cheese-name "The Cheese of Turin"
              :lat 41.90
              :lng 12.45}])

(defn -main
  [& args]
  (println (points heists)))    

lein run で実行した結果は、次のとおりになる。 main 関数では heistspoint 関数に適用している。

50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45

require を使うときに, :as, alias も合わせるとエイリアスが使用できる。

(require '[the-divine-cheese-code.visualization.svg :as svg])

これは、次と同じ意味である。

(require 'the-divine-cheese-code.visualization.svg)
(alias 'svg 'the-divine-cheese-code.visualization.svg)

別名をつけた(エイリアスした)名前空間では、次のように呼び出せる。

(svg/points heists)
; => "50.95,6.97 47.37,8.55 43.3,5.37 47.37,8.55 41.9,12.45"

use ならば、 require, refer 両方を使用せずとも別の名前空間にアクセスできる。 本番コードで使用するのはよろしくないが、REPL上で実験するときには、早くて便利である。

(use 'the-divine-cheese-code.visualization.svg)

これは次の同じことである。

(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)
(alias 'svg 'the-divine-cheese-code.visualization.svg)

またエイリアスを使うとつぎのようにもかける

(use '[the-divine-cheese-code.visualization.svg :as svg])
(= svg/points points)
; => true

(= svg/latlng->point latlng->point)
; => true

use を使えば points でアクセスできるので、一見冗長な使い方に思えるかもしれない。 しかし、 use にオプションの :only, :exclude, :as, :rename を使えば便利になる。

(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :as :only ['points])

これを use を使って書くと次のようになる。

(use '[the-divine-cheese-code.visualization.svg :as svg :only [points]])
(refer 'the-divine-cheese-code.visualization.svg :as :only ['points])
(= svg/points points)
; => true

;; エイリアス利用してアクセス可
svg/latlng->point

;; これはアウト
latlng->point
; アクセスできないので例外発生

The ns Macro

ようやく ns マクロを説明できる段階にきた。今までつかったきた in-ns, refer , alias, require は REPL上ではよく使用されるが、実際の開発では ns マクロのほうがもっと便利な機能を用意されているので使われる。 このセクションでは require, use, in-ns, alias, refer が組み込れた便利な ns の使い方をみていこう。

ns の便利なところの1つ目は、デフォルトで名前空間 clojure.core を参照してくれることである。 このおかげで、 clojure.core/println ではなく println で呼び出すことができる。

clojure.core から何を参照するかは refer と同じオプションが使える :refer-clojure を使ってコントロールできる。

(ns the-divine-cheese-code.core
  (:refer-clojure :exclude [println]))

この場合、 -main では、 clojure.core/println で呼び出さないといけなくなる。 ns 内の (:refer-clojure) フォームは、 reference(参照) と呼ばれ、上のコードは次と同じことである。

(in-ns 'the-divine-cheese-code.core)
(refer 'clojure.core :exclude ['println])

ns 内では6種類の参照がつかえる。

  • (:refer-clojure)
  • (:require)
  • (:use)
  • (:import)
  • (:load)
  • (:gen-class)

(:import), (:gen-class) は12章で説明し、 (:load) はめったに使用されないので紹介しない。

(:require)require 関数と同じような機能をもつ。

(ns the-divine-cheese-code.core
  (:require the-divine-cheese-code.visualization.svg))

これは次と等しい。

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)

注意することは、 ns の場合、 ' を使わなくてよいことである。 そして、 require では次の通り alias が使える。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :as svg]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])

複数のライブラリを参照したい場合は、次のように書く。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :as svg]
            [clojure.java.browse :as browse]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require ['the-divine-cheese-code.visualization.svg :as 'svg])
(require ['clojure.java.browse :as 'browse])

(:require)require の違いは、前者の場合, 参照したいものを絞ることができる。

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :refer [points]]))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg :only ['points])   

:all キーワードを使えば、全シンボルが参照できる

(ns the-divine-cheese-code.core
  (:require [the-divine-cheese-code.visualization.svg :refer :all]))    

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(require 'the-divine-cheese-code.visualization.svg)
(refer 'the-divine-cheese-code.visualization.svg)    

コードを読み込み、別名をつけて、参照するのが好まれた使い方なので、 :use の使用はおすすめしないが、この書き方に出くわすこともあるので使い方を知っておいたほうがよい。

(ns the-divine-cheese-code.core
  (:use clojure.java.browse))

これは次と等しい

(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)

また次のコードは、

(ns the-divine-cheese-code.core
(:use [clojure.java browse io]))

こちらと同じことである。

(in-ns 'the-divine-cheese-code.core)
(use 'clojure.java.browse)
(use 'clojure.java.io)

注意すべきは、 :use はベクタを引数にとる。ベクタの第一要素を base として残りの要素を参照する。

To Catch a Burglar

ns マクロの説明が済んだので、盗まれたチーズ問題の解決にもどろう。

盗まれた緯度経度の座標情報からSVGイメージを作成したい。 だが、2つの理由で座標情報をそのまま使用できない。 ひとつめは、緯度は、南から北にむかって数値が上がるのに対して、SVGのy座標は、数値があがると上から下に下がってしまうからである。 つまり、上下逆さにして緯度情報を利用する必要があるのだ。 2つ目は、描かれるものが非常に小さいことで、ズームしてあげる必要がある。

それをコードにすると次の通りである。

(ns the-divine-cheese-code.visualization.svg
  (:require [clojure.string :as s])
  (:require :exclude [min max]))

(defn comparator-over-maps ;1
  [comparision-fn ks]
  (fn [maps]
       (zipmap ks          ;2
               (map        ;3
                 (fn [k] (apply comparision-fn (map k maps)))
                  ks)))

(defn min (comparator-over-maps clojure.core/min [:lat :lng])) ;4
(defn max (comparator-over-maps clojure.core/max [:lat :lng]))

1は少々トリッキーなのだが、関数を返す関数である。 返された関数は、引数で指定された comparision-fn から ks をkeyにもつvalueを比較してくれる関数である。

4の comparator-over-maps で作った min, max 関数は、左上と右下の座標を求めるためのものである。

(min [{:a 1 :b 3}, {:a 5 :b 0}])
; => {:a 1 :b 0}

min 関数内で使用してる zipmap は、2つのシーケンスを引数にとり、新しいシーケンスを返す関数である。 はじめのシーケンスが、ハッシュマップのkeyとなり、もう一方のシーケンスがそのvalueとなる。

(zipmap [:a :b] [1 2])
; => {:a 1 :b 2}

したがって、2では、 ks がzipmapが返すハッシュマップのkeyとなり、3のmapの結果がそのvalueとなる。

続きのコードを書こう

(defn translate-to-00
  [locations]
  (let [mincoords (min locations)]
    (map #(merge-with - % mincoords) locations)))

(defn scale
  [width height locations]
  (let [maxrecords (max locations)
        ratio {:lat (/ height (:lat maxrecords))
               :lng (/ width (:lng maxrecords))}]
    (map #(merge-with * % ratio) locations)))

translate-to-00 は、 min で盗まれた座標の最小のものを取得して、そこを原点とした相対座標を返す。 merge-with は次のように機能する関数である。

(merge-with - {:lat 50 :lng 10} {:lat 5 :lng 5})
; => {:lat 45 :lng 5}

scale 関数は、最大座標をもとに倍率を求めて、残りの座標にも展開して返す関数である。

svg.clj の残りのコードは次の通り。

(defn latlng->point
  "Convert lat/lng map to comma-separated string" 
  [latlng]
  (str (:lat latlng) "," (:lng latlng)))

(defn points
  "Given a seq of lat/lng maps, return string of points joined by space"
  [locations]
  (s/join " " (map latlng->point locations)))

(defn line
  [points]
  (str "<polyline points=\"" points "\" />"))

(defn transform
  "Just chains other functions"
  [width height locations]
  (->> locations
       translate-to-00
       (scale width height)))

(defn xml
  "svg 'template', which also flips the coordinate system"
  [width height locations]
  (str "<svg height=\"" height "\" width=\"" width "\">"
       ;; These two <g> tags change the coordinate system so that
       ;; 0,0 is in the lower-left corner, instead of SVG's default
       ;; upper-left corner
       "<g transform=\"translate(0," height ")\">"
       "<g transform=\"rotate(-90)\">"
       (-> (transform width height locations)
            points
            line)
       "</g></g>"
       "</svg>"))

ここで登場した関数は簡単なもので、 {:lat x :lng y} マップを受け取って、SVGで表現できるように変換してくれる。

latlng->point 関数は、SVGマークアップで表現できる文字列の座標情報に変換してくれるものである。 points 関数は、 lat/lng シーケンスマップをカンマ区切りの座標情報に変換してくれる。 line 関数は、与えられた座標からSVGマークアップで表現できる直線情報を返してくれる。 transform 関数は、座標シーケンスを平行移動させ、倍率をあげる。 xml 関数は、最終的なSVGマークアップ表記を作成してくれる。

core.clj も仕上げよう

(ns the-divine-cheese-code.core
  (:require [clojure.java.browse :as browse]
            [the-divine-cheese-code.visualization.svg :refer [xml]])
  (:gen-class))

(def heists [{:location "Cologne, Germany"
              :cheese-name "Archbishop Hildebold's Cheese Pretzel"
              :lat 50.95
              :lng 6.97}
             {:location "Zurich, Switzerland"
              :cheese-name "The Standard Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Marseille, France"
              :cheese-name "Le Fromage de Cosquer"
              :lat 43.30
              :lng 5.37}
             {:location "Zurich, Switzerland"
              :cheese-name "The Lesser Emmental"
              :lat 47.37
              :lng 8.55}
             {:location "Vatican City"
              :cheese-name "The Cheese of Turin"
              :lat 41.90
              :lng 12.45}])

(defn url
  [filename]
  (str "file:///"
       (System/getProperty "user.dir")
       "/"
       filename))

(defn template
  [contents]
  (str "<style>polyline { fill:none; stroke:#5881d8; stroke-width:3}</style>"
       contents))

(defn -main
  [& args]
  (let [filename "map.html"]
    (->> heists
         (xml 50 100)
         template
         (spit filename))
    (browse/browse-url (url filename))))    

複雑なコードはない。 -mainxml, template からマークアップを完成させて、 spit でファイルに吐き出し、 browse/browse-url でブラウズしてる。

さいごに

実際には、nsマクロの使い方を知っておけばいいのが、
それがどんな役割をしてくれるのを分解して説明してくれる章でした。

CLOJURE for the BRAVE and TRUEでClojure学習 part4

はじめに

Clojure の学習備忘録。 今回は、Chapter 5: Functional Programming では以下を学習する

  • pure functionとはなんであるか?
  • pure functionがなぜ便利なのか?

以下pure functionを純粋関数と呼ぶ。

目次

Pure Functions: What and Why

println, rand を除けば、今まで使ってきた関数はすべて純粋関数である。 純粋関数は、以下2つの特性をもつ。 

  • 渡された引数が同じならば、必ず同じ結果を返す これは 参照透過性 と呼ばれる
  • サイド・エフェクト(副作用)を生まない。 関数の外にある変数やファイルといった関数スコープ外にあるアクセス可能な対象を変更しないこと。

この2つの特性は、関数の独立化を可能にし、他のシステムに影響を与えないため、より論理的なコーディングができる。 「この関数を呼び出したら、他に影響しないだろうか」といったことで悩む必要はなくなるわけだ。

Pure Functions Are Referentially Transparent

純粋関数かどうかは、渡される引数と、イミュータブルな返り値という2点においてのみ決まる。

参照透過性の例は次の通り。

(+ 1 3)
: => 3

イミュータブルな値にのみ依存してるならば、その関数は参照透過性をもつといえる。 文字列 , Daniel-san もイミュータブルなので、次の関数も参照透過性をもつと呼べる。

(defn wisdom
  [words]
  (str words ", Daniel-san"))

(wisdom "Always bathe on Fridays")
; => "Always bathe on Fridays, Daniel-san"     

一方参照透過性を満たさないは次の通り。 実行するたびに異なる結果を返すため、乱数を利用する関数はみな参照透過性をもつことはない。

(defn year-end-evaluation
  []
  (if (> (rand) 0.5)
    "You get a raise!"
    "Better luck next year!"))     

ファイルを読み込む関数もまた参照透過性をもたない。というのは、ファイルの内容は常に一定だと保証されないからである。 よって analyze-file は参照透過性がないが、 analysis は参照透過性をもつ関数になる。

(defn analyze-file
  [filename]
  (analysis (slurp filename)))

(defn analysis
  [text]
  (str "Character count: " (count text)))     

Pure Functions Have No Side Effect

副作用は、名前と値の関連付けを変更するものであり、javascriptを例にみてみよう。

var haplessObject = {
  emotion: "Carefree!"
};

var evilMutator = function(object){
  object.emotion = "So emo :'(";
}

evilMutator(haplessObject);
haplessObject.emotion;
// => "So emo :'("

もっとも副作用がなければ、ファイルへの書き出しや画面のRGB変更などは実現できない。 しかし、それでも副作用は変数名やkey名の参照先が何なのかをわからなくさせるため潜在的な危険を残してしまう。 参照先がなぜ・どのように変更されたのか追うのが困難になって、デバッグもむずかしくなったりする。副作用のある関数は、呼び出したときに外部の変数を変更しないかだけではなく、副作用関数に依存しているすべての関数でも同じく注意をはらないといけない。 一方副作用のない関数ならば、インプットとアウトプットの関係のみを考えばよく、それ以外に変更されること心配しなくてよい。

というわけで、副作用のある関数は限定的に使用するのがよい。 Clojureは、コアとなるデータ構造がイミュータブルなので、副作用のない関数を作成しやすい。

Living with Immutable Data Structures

イミュータブルなデータ構造というのは、副作用がないということである。 しかし副作用なしでどうやってコードをかけばよいのだろうか?

Recursion Instead of for/while

javascriptで次のようなコードを書いたことはあるだろう。

var wrestlers = getAlligatorWrestlers();
var totalBites = 0;
var l = wrestlers.length;

for(var i=0; i < l; i++){
  totalBites += wrestlers[i].timesBitten;
}

ループの外にある変数(totalBites)だけでなく、ループ内の変数iにも副作用がある。 このような副作用は対して問題にならないが、Clojureはそれも許さない。

Clojureでループ処理を書くにはどうすればよいのか? map, reduce を使用するやり方もあるが、 ここでは再帰を利用した関数型プログラミングのやり方を紹介する。

次の例をみてほしい。 Clojureでは再代入の演算子がないので、新しいスコープを作らない限り新しい値を変数にバインドできない。

(def great-baby-name "Rosanthony")
great-baby-name
; => "Rosanthony"

(let [great-baby-name "Bloodthunder"]
  great-baby-name)
; => "Bloodthunder"

great-baby-name
; => "Rosanthony"     

はじめにグローバル空間に変数名 great-baby-nameResonthony をバインドする。 次に、 let で新しいスコープをつくり、そこで great-baby-nameBloodthunder をバインドする。 let 式の評価が終わると、グローバル空間にもどって great-baby-nameRosanthony として再び評価される。

このようにスコープが異なると同じ変数名に別の値をバインドできる性質は、再帰に利用され、次の例は再帰的な問題解決である。

(defn sum
  ([vals] (sum vals 0))        ; 1
  ([vals accumlating-total]
    (if (empty? vals)          ; 2 
      accumlating-total
      (sum (rest vals) (+ (first vals) accumlating-total)))))

sum関数は2つの引数をうけとり、ひとつはコレクション(vals)で、もうひとつは蓄積用変数(accumlating-total)で、 Arityのオーバロードを利用して、accumlating-totalの初期値は0にしてる(1)。

どんな再帰的な解決も処理を継続するかどうかのチェックがあるように、sum関数も引数のチェックをおこない、 vals が空かどうかチェックする。 もし空ならば(2)、コレクションの全要素の処理が完了したので、 accumlating-total を返す。 vals が空でないと、まだ未処理の要素が残ってるので、再帰的にsumを呼び出す。 そのときの第一引数は、 (rest vals) とvalsの残りの要素が渡され、第二引数には、 (+ (first vals) accumulating-total) となり、 valsの第一要素を蓄積用変数に追加したものが渡される。これでreduceと同じように合計数を計算できるわけだ。

ただパフォーマンスの都合上, 実際のプログラムでは次のように recur を使用したほうがよい。 これはClojureが、末尾再帰をサポートしてないからということであるが、末尾再帰については説明を割愛する。

(defn sum
  ([vals]
     (sum vals 0))
  ([vals accumulating-total]
     (if (empty? vals)
       accumulating-total
       (recur (rest vals) (+ (first vals) accumulating-total)))))

Function Compsition Instead of Attribute Mutation

オブジェクトの状態を完全なものに仕上げるとき、ミューテーション(状態変更)を行うことがある。 たとえばRubyで、 GlamourShotCaption オブジェクトが入力値のスペース除去・ "lol"の大文字化する例を考えよう。

class GlamourShotCaption
  attr_reader :text
  def initialize(text)
    @text = text
    clean!
  end

  private
  def clean!
    text.trim!
    text.gsub!(/lol/, "LOL")
  end
end

best = GlamourShotCaption.new("My boa constrictor is so sassy lol!  ")
best.text
; => "My boa constrictor is so sassy LOL!"     

Glamourshotcaption クラスは、cleanメソッドで処理をカプセル化する。 Glamourshotcaption オブジェクトをつくると、内部変数に文字列を割り当てて、このミューテーション(内部状態変更)処理が実行される。

Clojureの場合、次のようになる。

(require '[clojure.string :as s])
(defn clean
  [text]
  (s/replace (s/trim text) #"lol" "LOL"))

(clean "My boa constrictor is so sassy lol!   ")
; => "My boa constrictor is so sassy LOL!"

Clojureでは状態変更はまったく行われない。 clean 関数は、イミュータブルな値を受けとると、純粋関数の s/trim でスペースをトリミングし、 さらにその結果を 純粋関数 s/replace に渡して、大文字化した結果を返す

このように関数を組み合わせるやり方は、関数合成とよばれる。 先程の再帰が同じ関数を引数を変えて何度も呼び出したのに対して、関数合成は異なる関数を組み合わせる。 一般的に関数型プログラミングでは、このようにシンプルな関数を組み合わせてより複雑な処理を構築する。

Cool Things to Do With Pure Functions

データから新しいデータを生み出すのと同じように、関数から新しい関数を生み出すことができる。 前chapterで登場した partial もそれであるが、ここではさらに comp, memorize を紹介する。

comp

前節でみたように、入力と出力の関係にだけに注目すればよいので、純粋な関数同士の合成は常に安全になる。 関数合成はよく使われるので、Clojureでは comp 関数で合成関数をつくれる。

((comp inc *) 2 3)    
; => 7

ここでは、 inc, * から匿名合成関数をつくっている。この関数に引数2,3を渡すと、 2 * 3が行われた後、それに+1した結果を返す。 数学的な用語で説明すると、f1, f2, …, fnから合成関数gをつくったとき g(x1, x2, …, xn)は、f1(f2(fn(x1, x2, …, xn)))と同義である。

注意すべきは、 最初に適用される * は引数をいくつでも受け取れるが、 それ以外の関数は1つしか引数が取れないということである。

次の例では、 comp を使ってロールプレーイングゲームのキャラクタの属性を抜き出せる。

(def character
  {:name "Smooches McCutes"
   :attributes {:intelligence 10
                :strength 4
                :dexterity 5}})
(def c-int (comp :intelligence :attributes))
(def c-str (comp :strength :attributes))
(def c-dex (comp :dexterity :attributes))

(c-int character)
; => 10

(c-str character)
; => 4

(c-dex character)
; => 5

匿名関数を使っても同じことができるが、 comp を使ったほうがコード量が少なくエレガントである。 これら3つの関数はすべて引数が1つであるが、合成したい関数のなかに2つ以上の引数をもつものがあるときどのようにすればよいだろうか? そういうときは匿名関数を使える。

(defn spell-slots
  [char]
  (int (inc (/ (c-int char) 2))))

(spell-slots character)
; => 6

このspell-slotsは、はじめ2で割り、それに1を足し、最後に小数をなくすため整数にする関数である。 割り算は引数を2つもっているので、 comp を使って同じ処理をさせるには次のようにする。

(def spell-slots-comp (comp int inc #(/ % 2) c-int))

2で割るために、匿名関数で包むわけだ。

memoize

純粋関数を使う時にもうひとつ便利なのがメモイズであり、特定の関数の呼び出し結果を記録することができる。 メモイズは、関数の引数と戻り値を保存することができる、2回目同じ引数で関数を呼び出したとき、すぐさま結果を受け取れる。 処理に時間かかる関数を使うときには便利である。たとえば、処理に1秒かかる関数がメモイズされていない場合は次の通り。

(defn sleepy-identity
  "Returns the given value after 1 second"
  [x]
  (Thread/sleep 1000)
  x)
(sleep-identity "Mr. Fantasitico")
; => "Mr. Fantasitico" after 1 second

(sleep-identity "Mr. Fantasitico")
; => "Mr. Fantasitico" after 1 second

2回目の呼び出しも1秒かかる。しかし、メモイズを利用すれば1秒かかるのは、1回目呼び出しだけになる。 それ以降の呼び出しは、即座に結果を返す。

(def memo-sleepy-identity (memorize sleepy-identity))
(meom-sleepy-identity  "Mr. Fantasitico")
; => 1秒後に"Mr. Fantasitico"を返す

(meom-sleepy-identity  "Mr. Fantasitico")
; => すぐに"Mr. Fantasitico"を返す

さいごに

はじめに書いた内容を再度整理すると、

  • pure functionとはなんであるか?
    同じ引数ならば同じ結果を返し、副作用がない関数のこと。

  • pure functionがなぜ便利なのか?
    クラスのメソッドならば別の箇所で利用しにくいが、関数ならば簡単に転用できる
    関数合成を使えば、シンプルな関数を組合わせてより複雑な処理を実現できる

これはClojureに限った話ではないなとあらためておもう。 jsでもループに再帰関数を利用できるわけなので、他の言語を利用するときも可能な限り純粋関数で書くようにしよう。 個人的には、純粋関数で書くことのなによりもメリットは単体テストが書きやすいことだと思う。

CLOJURE for the BRAVE and TRUEでClojure学習 part3

はじめに

Clojure の学習備忘録。 今回は、Chapter 4: Core Functions in Depthの残り箇所。

今回のあつかうテーマは、以下の通り。

  • 遅延シーケンス
  • 無限シーケンス
  • コレクション関数(conj, into)
  • 代表的な高階関数(apply, partial, complement)

遅延シーケンスといっても、実際に要素にアクセスするまで全く計算しないではなく、 パフォーマンス上先行するいくつか要素も計算してしまうというのは面白い。

Lazy Seqs

mapは他のシーケンス関数と同様、引数のコレクションにseqを適用するだけではなく、遅延シーケンス を返す。
遅延シーケンスとは、要素に実際にアクセスするまで計算を行わないシーケンスのことである。 それに対してシーケンスの全要素が計算されているものは、リアライズシーケンスと呼ぶ。 必要になるタイミングまで計算を遅延させることは、より効率的なプログラミングを可能にし、無限シーケンスが構築できるようになる。

Demonstrating Lazy Seq Effciency

遅延シーケンスについて理解するために、次の例を考えよう。あなたはヴァンパイアを特定するタスクに参加しており、1 億人の容疑者の中からヴァンパイアを特定しないといけない。ヴァンパイアデータベースのサブセットはスタブ化して、次の通りである。

(def vampire-database
  {0 {:makes-blood-puns? false, :has-pulse? true  :name "McFishwich"}
    1 {:makes-blood-puns? false, :has-pulse? true  :name "McMackson"}
    2 {:makes-blood-puns? true,  :has-pulse? false :name "Damon Salvatore"}
    3 {:makes-blood-puns? true,  :has-pulse? true  :name "Mickey Mouse"}})

(defn vampire-related-details
  [social-security-number]
  (Thread/sleep 1000)
  (get vampire-database social-security-number))

(defn vampire?
  [record]
  (and (:makes-blood-puns? record)
        (not (:has-pulse? record))
        record))

(defn identify-vampire
  [social-security-numbers]
  (first (filter vampire?
                  (map vampire-related-details social-security-numbers))))

vampire-related-details は 1 秒でデータベースからレコードを見つける関数である。 vampire? はレコードがヴァンパイアかどうかを判定する関数。 identify-vampireマイナンバーとデータベースのレコードをマッピングして、はじめに一致したレコードを返す。

これらの関数の処理時間を計測するには、 time を使用すればよい。

(time (vampire-related-details 0))
; => "Elapsed time: 1001.373258 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

1 行目には、関数処理にかかった時間が出力される。
2 行目は、関数の戻り値である、データベースのレコードが出力される。

遅延が考慮されていない map ならば、全要素に対して vampire-related-details 関数を適用し、その後、 filter を適用することになる。1 億人の容疑者からヴァンパイアを見つけるならば、全要素なめるのに 1 億秒つまり 12 日必要となり、街の半分の人が死んでしまう。しかし Clojuremap は遅延を考慮されているので、そんな心配はいらない。

(time (def mapped-details (map vampire-related-details (range 0 1000000))))
; =>"Elapsed time: 0.053236 msecs"
; => #'user/mapped-details

この例では、 range によって 0 から 999,999 までの遅延シーケンスを生成し、 map が返す遅延シーケンスを mapped-details にバインドしている。しかし、 range が返すシーケンスの全要素に対して vampire-related-details はまだ適用されていないので、処理時間は 12 日もかからず、一瞬で終わる。

遅延シーケンスは、一つはシーケンスの要素をリアライズ方法が記述されたレシピと、今までリアライズされた要素の2つから構成される。 map を使用してもどの要素もまだリアライズされていないが、要素を生成するレシピはある。リアライズされていない要素にアクセスすれば、その度遅延シーケンスはレシピにあるとおり要素を料理して返すことになる。

上の例の場合、 map-detailsリアライズされていないが、map-details の要素にアクセスすれば、要素に対してレシピを適用することになり、検索時間に 1 秒かかるだろう。

(time (first mapped-details))
; => Elapsed time: 32070.326583 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

処理に 32 秒かかっている。1 億秒よりかはずっと速くなったが、期待しているよりも 31 秒も多くかかってしまっている。ひとつの要素にアクセスするだけなので 1 秒で済むはずなのになぜだろうか?

それは、Clojure が遅延シーケンスをチャンクに分割するからなのである。1 要素をリアライズするときに先行していくつかの要素もリアライズしている。例の場合だと、1 要素アクセスしても続く 31 要素もリアライズしたので 32 秒かかったのである。 これはパフォーマンス的な理由で行われている。

遅延シーケンスの要素の評価は、初回アクセスのときだけである。よって、もう一回 mapped-details のはじめの要素にアクセスしてもほとんど時間はかからない。

(time (first (mapped-details)))
; => "Elapsed time: 0.022 msecs"
; => {:name "McFishwich", :makes-blood-puns? false, :has-pulse? true}

遅延シーケンスについて整理できたので、はじめに書いたコードが時間かからないことも理解できるだろう

(time (identify-vampire (range 0 1000000)))
; => "Elapsed time: 32019.912 msecs"
; => {:name "Damon Salvatore", :makes-blood-puns? true, :has-pulse? false}

Infinite Sequences

遅延シーケンスの特徴を利用して、無限シーケンスも作成できる。今までシーケンスは有限であったが、Clojure では無限シーケンスを作成できる関数を用意されてる。 repeat はその一つであり、渡された引数の値からなる無限シーケンスを構築する

(concat (take 8 (repeat "na")) ["Batman!"])
; => ("na" "na" "na" "na" "na" "na" "na" "na" "Batman!")

ここでは、文字列 "na" を要素にもつ無限シーケンスを構築している。

repeatedly をつかえば、もっと柔軟な無限シーケンスを構築できる

(take 3 (repeatedly (fn [] (rand-int 10))))
; => (1 4 0)

repeatedly でできた無限シーケンスの要素は、匿名関数 (fn [] (rand-int 10)) の適用結果になる。REPL を実行するたびに、取得要素はランダムに変化する。

遅延シーケンスをリアライズする first や take といった関数は、シーケンスの次の要素がなんであるかを知らないし、取得した要素の次があるならば、ただそれを取得するだけである。

(defn even-numbers
  ([] (even-numbers 0))
  ([n] (cons n (lazy-seq (even-numbers (+ n 2))))))

(take 10 (even-numbers))
; => (0 2 4 6 8 10 12 14 16 18)

ここでは、再帰を利用した無限シーケンスを作成してる。cons は第一引数の要素と第二引数のリストから新たなリストを作成している。

(cons 0 '(2 4 6))
; => (0 2 4 6)

even-numbers では、今の要素と次の要素の計算手順が記された遅延シーケンスをコンス操作の引数に渡すことで 無限シーケンスを構成している。

The Collection Abstraction

シーケンスの抽象化は、構成要素各々に対するオペレーションに関するものに対して、コレクションの抽象化は、データ構造全体に関するものになる。たとえば、 count, empty?, every? とった関数は、個々の要素ではなく、その全体を対する操作になる。

(empty? [])
; => true

(empty? ["no"])
: => false

ここでは代表的で、よくごっちゃになりがちな関数 into, conj をとりあげる。

into

今までみたきたように、シーケンス関数の戻り値はオリジナルのデータ構造ではなく、seq である。それをオリジナルなデータ構造に変換したいとき into がつかえる。

(map identity {:sunlight-reaction "Glitter!"})
; => ([:sunlight-reaction "Glitter!"])

(into {} (map identity {:sunlight-reaction "Glitter!"}))
; => {:sunlight-reaction "Glitter!"}

map 関数は hashmap を seq に変換するが、 into で hashmap に変換し直している。hashmap 以外のデータ構造にも変換できる。

ベクタに変換する

(map identity [:garlic :sesame-oil :fried-eggs])
; => (:garlic :sesame-oil :fried-eggs)

(into [] (map identity [:garlic :sesame-oil :fried-eggs]))
; => [:garlic :sesame-oil :fried-eggs]

セットに変換する

(map identity [:garlic-clove :garlic-clove])
; => (:garlic-clove :garlic-clove)

(into #{} (map identity [:garlic-clove :garlic-clove]))
; => #{:garlic-clove}

into の第一引数が空である必要はない。空ではないと、追記させることができる。

(into {:favorite-emotion "gloomy"} [[:sunlight-reaction "Glitter!"]])
; => {:favorite-emotion "gloomy" :sunlight-reaction "Glitter!"}

(into ["cherry"] '("pine" "spruce"))
; => ["cherry" "pine" "spruce"]

conj

conj 関数もコレクションに要素を追加するが、into と少し異なる。

(conj [0] [1])
; => [0 [1]]

[1] のまま追加されてしまう。

(into [0] [1])
; => [0 1]

一方 into の場合、 1 で追加される conj で同じ結果を得たい場合は、次の通り。

(conj [0] 1)
; => [0 1]

conjスカラー値で受け取るのに対して、 into はコレクションを引数にとるという違いがある。

Function Functions

関数を引数にとったり、戻り値として返す代表的な高階関数として、 apply, partial をとりあげる。

apply

apply は、引数に関数とシーケンスをうけとり、シーケンスを関数の引数として適用するたとえば、 受け取った引数で最大の値を返す max 関数で考えてみる。

(max 0 1 2)

しかし、引数にベクタを受け取った場合、期待どおりにいかない。

(max [0 1 2])
; => [0 1 2]

max に渡すときは、ベクタとしてまとめて渡してはいけず、比較したい値をそれぞれ引数としてわたさないといけない。そして、ベクタとして渡しても動作させるには、 apply が使える。

(apply max [0 1 2])

partial

partial は引数に、一個の関数(f)と 0 個以上の引数(arg)をとり、新しい関数を返す。partial が返す関数を引数(new arg)つきで呼び出すと、partial に渡した f に arg と new arg を渡して適用される。

(def add10 (partial + 10))
(add10 3)
; => 13
(add10 5)
; => 15

(def add-missing-elements
  (partial conj ["water" "earth" "air"]))

(add-missing-elements "unobtainium" "adamantium")
; => ["water" "earth" "air" "unobtainium" "adamantium"]

add10 を呼び出すと、 (+ 10) に与えられた引数を追加して (+ 10 3) が実行される。(+ 10) のように+関数の第一引数を 10 で固定させた関数を作るのが、 partial である。

コンテキストは異なるが関数とその引数の組み合わせが同じものを繰り返し使用するとき、partial は効果的である。

(defn lousy-logger
  [log-level message]
  (condp = log-level
    :warn (clojure.string/lower-case message)
    :emergency (clojure.string/upper-case message)))

(def warn (partial lousy-logger :warn))

(warn "Red light ahead")
; => "red light ahead"

(warn "Red light ahead") を呼び出すことは、 (lousy-logger :warn "Red light ahead") を呼び出すことと同じである。

complement

前節で、1 億人からヴァンパイアを特定する identify-vampire 関数を作成したが、逆に人間を特定するにはどうすればよいだろうか?

(defn identify-humans
  [social-security-numbers]
  (filter #((not (vampire? %)))
          (map vampire-related-details social-security-numbers)))

filter 関数の第一引数は、 #(not (vampire? %)) となっているが、 この述語関数の戻り値を反転させるのが、 complement 関数である。

(defn not-vampire? (complement vampire?))
(defn identify-humans
  [social-security-numbers]
  (filter not-vampire?
          (map vampire-related-details social-security-numbers)))

CLOJURE for the BRAVE and TRUEでClojure学習 part2

はじめに

Clojure の学習備忘録。 今回は、Chapter 4: Core Functions in Depth の途中まで。

Sequence とはなにかを説明した章であり、map, reduce といったいくつか重要な Sequence 関数が紹介される。

js と違って、
Clojure が hashMap も sequence として扱われる点は面白い。

Programing to Abstractions

Clojure の重要な概念に、抽象化プログラミングがある。抽象化プログラミングではない Emacs Lisp(elisp)の場合、 mapcar 関数を使えば、Clojuremap と同じように新しいリストを生成できる。しかし、elisp でハッシュマップに対して map 関数を適用したい場合、 maphash 関数が必要になる。一方 Clojure の場合、リストであれハッシュマップであれ、 同じ map 関数ですませられる。

elisp では同じ map 操作でもデータの構造に応じて異なる関数を使用しないといけないが、Clojure の場合ひとつだけでよいのである。 reduce 関数の場合でも同じである。

というのも Clojure は、データ構造ではなく、 抽象化シーケンス という観点で mapreduce 関数を定義しているからなのである。

あとで紹介する first, rest, cons といったシーケンスのコアとなる操作が適用できるデータ構造であれば、mapreduce その他シーケンス関数をいつでも Clojure は適用可能である。これこそ Clojure が意味する抽象化プログラミングであり、Clojure 哲学の中心的な考え方である。

それでは map 関数を例に抽象化シーケンスについてもう少し詳しくみてみよう。

Treating Lists, Vectors, Sete, And Maps as Sequences

特定のプログラミング言語関係なく map 操作の中心的な振る舞いを考えると、それは y1 = f(x1), y2 = f(x2), yn = f(xn)のように x というシーケンスから y の新しいシーケンスを生成することである。

ここでいうシーケンスというのは、順番に要素が並べられている集合や、あるいは要素に順番がない集合のことであったり、あるいは要素同士が前後関係をもった集合も考えられる。

map 操作やシーケンスの説明では、リスト、ベクタ、その他具体的なデータ構造がどうなのかをはっきり示さない。このようにできるだけ抽象的に考えプログラミングするよう Clojure は設計され、データ構造の抽象化という観点で関数が実装されている。

シーケンスのコアとなる関数 first, rest, cons が適用できるならば、 そのデータ構造は抽象化シーケンスを実現しているといえる。
リストや、ベクタ、セット、ハッシュマップは、みな抽象化シーケンスであるので、 map 関数を適用できるのである。

(defn titleize
  [topic]
  (str topic " fro the Brave and True"))
(map titleize ["Hamsters" "Ragnarok"])
(map titleize '("Empathy" "Decoratting"))
(map titleize #{"Elbows" "Soap Carving"})
(map #(titleize (second %)) {:uncomfortable-thing "Winking"})

はじめの2つは、ベクタやリストに map を適用している。
3つめは、順序のないセットに、4つめはハッシュマップに map が適用してる。

first ,rest, and cons

このセクションでは、Javascript で、リンクリストと3つの関数(first, rest, cons)を実装する。これら3つの関数を使って、 map 関数の組み立て方を説明する。

重要なのは、リンクリストを使った具体的な実装が、Clojure のおけるシーケンス抽象化とどのように異なるのか理解することであり、 特定のデータ構造における実装がどうなのかに着目しなくてよい。

あるデータ構造に対してシーケンス関数が適用できるかは、 first, rest, cons 関数が適用できるかどうかであり、 もし適用できるならば、そのデータ構造にはシーケンスライブラリが適用できる。

リンクリストとは、各ノードがリンクして一列のシーケンスになってるものである。

ここでは、javascript でリンクリストを実装する。
node3 はリンクリストの最後の要素で、next は null になる。

var node3 = {
  value: "last",
  next: null,
};

node2 の next は、node3 を node1 の next は、node2 を参照しており、 以上でリンクリストとなってる。

var node2 = {
  value: "second",
  next: node3,
};

var node1 = {
  value: "first",
  next: node2,
};

このリンクリストに対して 3 つの関数(first, rest, cons)を実装する。

各関数の内容は以下の通り first は、受け取った node の value を返し、
rest は、受け取った node のあとに続く value を返し、
cons は、新しい node をリストの先頭に追加する。

そしてこれら3つが実装できると、
それらを組み合わせて、 map, reduce, filter やその他シーケンス関数が実装できるようになる。

注目してほしいのは、 first, rest の引数名が node になっていることだ。

var first = function (node) {
  return node.value;
};

var rest = function (node) {
  return node.next;
};

var cons = function (newValue, node) {
  return {
    value: newValue,
    next: node,
  };
};

first(node1);
// => "first"

first(rest(node1));
// => "middle"

first(rest(rest(node1)));
// => "last"

var node0 = cons("new first", node1);
first(node0);
// => "new first"

first(rest(node0));
// => "first"

では、準備が整ったので 3つの関数を使って、 map を実装してみる。

var map = function (list, transform) {
  if (list === null) {
    return null;
  } else {
    return cons(transform(first(list)), map(rest(list), transform));
  }
};

map 関数は、リストの1つ目の要素を変換して、残りのリストに対して null が返すまで再帰的に適用していく。
全要素に "mapped!" を文字列追加させて、1つ目の要素取得するならば次のとおりになる。

first(
  map(node1, function (val) {
    return val + " mapped!";
  })
);

// => "first mapped!"

いけてるのは、 map 関数が first, rest, cons だけで実装されているということである。
どんなデータ構造であっても、 first, rest, cons が適用できるならば、 map は機能するのだ。

実際リンクリストだけではなく、配列でも map が機能することを確認しよう。

var first = function (array) {
  return array[0];
};

var rest = function (array) {
  var sliced = array.slice(1, array.length);
  if (sliced.length == 0) {
    return null;
  } else {
    return sliced;
  }
};

var cons = function (newValue, array) {
  return [newValue].concat(array);
};

var list = ["Transylvania", "Forks, WA"];
map(list, function (val) {
  return val + " mapped!";
});
// => ["Transylvania mapped!", "Forks, WA mapped!"]

ここでも、Javascript の Array 関数をつかって、 first, rest, cons を実装してる。
map 関数は、新しく定義された first, rest, cons を参照するようになるので、 array でも機能する。

これで first, rest, cons が実装できれば、 map やその他のシーケンス関数が自由に使用できることがわかるだろう。

Abstraction Through Indirection

しかし、 first 関数がどんなデータ構造でも適用できる説明がないので、上記説明では不十分である。Clojure では、これを2つの間接的な方法で実現している。

プログラミングの世界で間接というのは、1 つの名前が複数の関連した意味を持つという意味でよく使用される。この場合も、 first は複数の、データ構造に依存した意味を持っている。間接は、抽象化を可能にする。

ポリモーフィズムClojure が提供する間接の1つの手段である。詳細は割愛するが、ポリモーフィズム関数は、引数のタイプに応じて異なる機能を提供できる(引数の個数で別の機能を提供するマルチ Arity 関数とは違う) この点については、Chapater13 章で紹介する。

シーケンスに関しては、Clojure はもっと簡単な変換を通じて抽象的な関数が適用できるデータ構造を生成し、間接を実現している。seq 関数を使うのだ。

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

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

(seq #{1 2 3})
; => (1 2 3)

(seq {:name "Bill Compton" :occupation "Dead mopey guy"})
; => ([:name "Bill Compton"] [:occupation "Dead mopey guy"])

ここでは、2 点注目することがある。
1つ目は、 seq 関数は、リストのように振る舞う値を必ず返す(これをシーケンスあるいは seq と呼ぶ)。
2つ目は、ハッシュマップに対して seq を適用すると、key, value を2要素からなるシーケンスを返す。 このため、ハッシュマップでもベクタのように map 関数を適用できるのである。4つ目の例がそれである。

into 関数を使えば、seq から map に戻すことができる。

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

以上から Clojure のシーケンス関数は、引数に seq 関数を適用している。抽象化シーケンスが実装されたデータ構造であれば、 reduce, filter, distict, group-by といった有名な関数が適用できるわけだ。

Seq Function Examples

map

今まで map 関数を見てきたので、今回は map の新しい機能2つを紹介する。 1つは複数のコレクションを引数に取る場合であり、もう一つは関数コレクションを引数に取る場合である。 またよく map で使う keyword マッピングも紹介する。

今まで見てきた map 関数は、次のようにひとつのコレクションを引数にとるもの。

(map inc [1 2 3])

しかし、複数のコレクションを map にわたすことができ、こんな感じである。

(map str ["a" "b" "c"] ["A" "B" "C"])

この処理は、次の処理とやってることおなじである。

(list (str "a" "A") (str "b" "B") (str "c" "C"))

コレクションの同じインデックスのものが、 str に渡されてマッピングされていく。コレクションの要素数が同じでないといけないことには注意しよう。

別の例でもみてみよう。1つは、過去4日間の人間から摂取した血のリットル数で、もうひとつは動物から摂取した血のリットル数のベクタである。 unify-diet-data 関数は、人間・動物の両方から一日の摂取量を引数にとり、ひとつのハッシュマップにまとめる関数である。

(def human-consumption   [8.1 7.3 6.6 5.0])
(def critter-consumption [0.0 0.2 0.3 1.1])
(defn unify-diet-data
  [human critter]
  {:human human
    :critter critter})
(map unify-diet-data human-consumption critter-consumption)

今度は、 map 関数コレクションを引数にとったときの処理についてみてみる。この機能を使えば、ひとまとめの計算処理が可能になる。

(def sum #(reduce + %))
(def avg #(/ (sum %) (count %)))
(defn stats
  [numbers]
  (map #(% numbers) [sum count avg]))

(stats [3 4 10])
; => (17 3 17/3)
(stats [80 1 44 13 6])
; => (144 5 144/5)

ここでは, stats 関数がベクタに可能のされた関数を numbers に対して一つずつ適用している。

Clojure 使いがよく map で使う、ハッシュマップコレクションから特定の key の value を取得する処理についても紹介する。

(def identities
  [{:alias "Batman" :real "Bruce Wayne"}
    {:alias "Spider-Man" :real "Peter Parker"}
    {:alias "Santa" :real "Your mom"}
    {:alias "Easter Bunny" :real "Your dad"}])

(map :real identities)
; => ("Bruce Wayne" "Peter Parker" "Your mom" "Your dad")

reduce

Chapter3 で reduce について紹介したが、ここではまた別の使い方を紹介する。

ひとつめは、ハッシュマップを受けおり、key はそのまま value を更新する処理である。

(reduce (fn [new-map [key val]]
          (assoc new-map key (inc val)))
        {}
        {:max 30 :min 10})
; => {:max 31, :min 11}

引数に受け取った {:max 30 :min 10} は、 ([:max 30] [:min 10]) として reduce 内部で使用されるのでうまくいくわけだ。

もうひとつの reduce の便利な使い方は、value に応じて key をフィルターするものである。次の例では、匿名関数を利用して value が 4 より大きいかどうかを判別し、小さい場合はカットしてる。だから 3.9 の key はなくなる。

(reduce (fn [new-map [key val]]
          (if (> val 4)
            (assoc new-map key val)
            new-map))
        {}
        {:human 4.1
          :critter 3.9})
; => {:human 4.1}

お持ち帰りしてほしいポイントは、 reduce 関数が初見よりもずっと柔軟な関数であるということである。reduce をつかって、map, filter, some と同じ処理ができるように実装してみてもよいだろう。

take, drop, take-while, and drop-while

take, drop はともに number と collection の2つの引数を受け取る。 take は 1 から n 番目までの要素を取得するのに対して、 drop では 1 から n 番目を除いた残りの要素を返す。

(take 3 [1 2 3 4 5 6 7 8 9 10])
; => (1 2 3)

(drop 3 [1 2 3 4 5 6 7 8 9 10])
; => (4 5 6 7 8 9 10)

姉妹関数の take-while, drop-while はよりおもしろい挙動をする。ともに返り値が真偽値である述語関数を受け取り、それを使って take あるいは drop を続けるかどうかを判断する

たとえば、ベクタ表記された食事日誌でしてみる。日誌は、月・日ごとに食したものが記録されている。

(def food-journal
  [{:month 1 :day 1 :human 5.3 :critter 2.3}
    {:month 1 :day 2 :human 5.1 :critter 2.0}
    {:month 2 :day 1 :human 4.9 :critter 2.1}
    {:month 2 :day 2 :human 5.0 :critter 2.5}
    {:month 3 :day 1 :human 4.2 :critter 3.3}
    {:month 3 :day 2 :human 4.0 :critter 3.8}
    {:month 4 :day 1 :human 3.7 :critter 3.9}
    {:month 4 :day 2 :human 3.7 :critter 3.6}])

匿名述語関数である #(< (:month %) 3) をつかって、1, 2 月の日誌データの取得できる。

(take-while #(< (:month %) 3) food-journal)
; => ({:month 1 :day 1 :human 5.3 :critter 2.3}
;     {:month 1 :day 2 :human 5.1 :critter 2.0}
;     {:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 2 :day 2 :human 5.0 :critter 2.5})

take-while は、3 月の日誌データを読み取ると、匿名関数が false を返し、それ以降の処理をやめるのである。

同じような考え方で、 drop-whiletrue である限り要素を drop する。

(drop-while #(< (:month %) 3) food-journal)
; => ({:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8}
;     {:month 4 :day 1 :human 3.7 :critter 3.9}
;     {:month 4 :day 2 :human 3.7 :critter 3.6})

take-while, drop-while の両方を使用すれば、2, 3 月のデータを取得できる

(take-while #(< (:month %) 4)
            (drop-while #(< (:month %) 2) food-journal))
; => ({:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 2 :day 2 :human 5.0 :critter 2.5}
;     {:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8})

filter and some

filter 関数を使えば述語関数が true を返すシーケンス内の全要素がえられる。たとえば、先程の食事日誌を使って、人間から摂取したリットル数が、5 未満のものをえよう。

(filter #(< (:human %) 5) food-journal)

; => ({:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8}
;     {:month 4 :day 1 :human 3.7 :critter 3.9}
;     {:month 4 :day 2 :human 3.7 :critter 3.6})

これと同じ結果を、 take-while でも実現できるが、 take-while のほうが効率はよい。なぜなら、filter の場合全要素をなめるに対して、~take-while~ では述語関数が false を返した途端処理を止めるからである。

述語関数が true を返す要素を、コレクションが含むかどうかを知りたい場合は、 some をつかえばよい。some は、述語関数が true を初めて返した値を返す。

(some #(> (:critter %) 5) food-journal)
; => nil

(some #(> (:critter %) 3) food-journal)
; => true
(some #(and (> (:critter %) 3) %) food-journal)
; => {:month 3 :day 1 :human 4.2 :critter 3.3}

食事日誌には、動物から 5 リットル以上摂取したことが記録がないので、はじめの例では nil を、 一方 3 リットル以上摂取した記録はあるので true を返している。

注目するべきは、返り値が、日誌記録レコードではなく true であること。もしレコードがほしいならば次の通り。

(some #(and (> (:critter %) 3) %) food-journal)
; => {:month 3 :day 1 :human 4.2 :critter 3.3}

sort and sort-by

sort 関数を使えば昇順で並び替えできる。

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

もっと凝った並び替えするならば、 sort-by が使える。関数をコレクションの要素に適用して、その戻り値でソートできる。

(sort-by count ["aaa" "c" "bb"])
; => ("c" "bb" "aaa")

sort を使えば, アルファベット順で "aaa" "c" "bb" となるに対して、 sort-by を使えば、count を適用した結果(3, 1 ,2)で並び替える。

concat

concat はシンプルで、あるシーケンスを別のシーケンスの末尾に追加する

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

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 らしさを感じ取れるだろう。 にしても長い。。。

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