関数を作る

Clojureは関数型言語だ。関数はファーストクラスであり、他の関数に渡したり他の関数から返したりすることができる。多くのClojureコードは主に(副作用のない)純粋関数で構成されているので、同じ入力で呼び出すと同じ出力が得られる。

defn は名前付きの関数を定義する:

;;    名前   引数           本体
;;    -----  ------  -------------------
(defn greet  [name]  (str "Hello, " name) )

この関数は単一の引数 name を持つが、引数のベクターには任意個の引数を含めることができる。

関数名を「関数位置」(リストの最初の要素)に置いて関数を呼び出してみよう:

user=> (greet "students")
"Hello, students"

マルチアリティ関数

関数は異なる個数の引数(異なる「アリティ」(arity))をとるように定義することができる。異なるアリティはすべて同じ defn の中に定義されなければならない―― defn を複数回使うと以前の関数を置き換えてしまう。

個々のアリティは ([args*] body*) というリストだ。あるアリティは別のアリティを呼び出すことができる。本体部(body)は任意個の式を含むことができ、戻り値は最後の式の結果だ。

(defn messenger
  ([]     (messenger "Hello world!"))
  ([msg]  (println msg)))

この関数は2つのアリティ(0引数と1引数)を宣言している。0引数のアリティは1引数のアリティをデフォルト値で呼び出して出力する。この関数を適切な個数の引数を渡して呼び出す:

user=> (messenger)
Hello world!
nil

user=> (messenger "Hello class!")
Hello class!
nil

可変長引数関数

関数は可変個の引数を定義することもできる――これは「可変長引数」関数として知られている。可変の引数は引数リストの末尾になければならない。この引数はシーケンスにまとめられており、関数から使うことができる。

可変の引数の始まりは & で示される。

(defn hello [greeting & who]
  (println greeting who))

この関数は引数 greeting とリストにまとめられる who という名前の可変個の引数(0個以上)をとる。このことは3引数で呼び出してみると分かる:

user=> (hello "Hello" "world" "class")
Hello (world class)

printlnwho を出力すると、まとめられた2要素のリストとして出力されることが分かる。

無名関数

無名関数は fn で作ることができる:

;;    引数           本体
;;   ---------  -----------------
(fn  [message]  (println message) )

無名関数には名前がないので、あとで参照することはできない。むしろ、無名関数は別の関数に渡す時点で作るのが典型的だ。

もしくは直ちに呼び出すことも可能だ(一般的な利用方法ではないが)。

;;     操作 (関数)                      引数
;; --------------------------------  --------------
(  (fn [message] (println message))  "Hello world!" )

;; Hello world!

ここではより大きな式の関数位置で無名関数を定義したが、その式は直ちに無名関数の式を引数で呼び出している。

多くの言語には文と式の両方があり、文は命令的に何かを行い値は返さないが、式は値を返す。Clojureには値を返す だけがある。 if のようなフロー制御の式でさえこれに含まれることはあとで見よう。

defn vs fn

defndeffn を縮約したものだと考えると便利かもしれない。 fn が関数を定義し、 def が関数を名前に束縛する。これらは等価だ:

(defn greet [name] (str "Hello, " name))

(def greet (fn [name] (str "Hello, " name)))

無名関数のシンタックス

Clojureのリーダーに実装された fn による無名関数のより短い形式がある: #() 。このシンタックスは引数リストを省略し、位置に基づいて引数を指定する。

  • % は単一の引数に使う

  • %1, %2, %3 などは複数の引数に使う

  • %& は任意の残りの(可変長)引数に使う

ネストした無名関数では引数が名付けられていないことで曖昧さが生じるため、ネストすることは認められていない。

;; これと等価: (fn [x] (+ 6 x))
#(+ 6 %)

;; これと等価: (fn [x y] (+ x y))
#(+ %1 %2)

;; これと等価: (fn [x y & zs] (println x y zs))
#(println %1 %2 %&)

落とし穴

要素をとってベクターに包む無名関数が必要になることは一般的だ。このように書こうとするかもしれない:

;; こうしてはならない
#([%])

この無名関数はこれと等価に展開される:

(fn [x] ([x]))

このフォームはベクターで包んで さらに そのベクターを引数なしで呼び出そうとする(追加の丸括弧の組)。そうではなくて:

;; 代わりにこうする:
#(vector %)

;; もしくはこうだ:
(fn [x] [x])

;; もしくは最もシンプルなのは単に vector 関数そのものを使うことだ:
vector

関数を適用する

apply

apply 関数は関数を0個以上の固定の引数で呼び出し、残りの必要な引数を最後のシーケンスから取り出す。最後の引数は 必ず シーケンスでなければならない。

(apply f '(1 2 3 4))    ;; (f 1 2 3 4) と同じ
(apply f 1 '(2 3 4))    ;; (f 1 2 3 4) と同じ
(apply f 1 2 '(3 4))    ;; (f 1 2 3 4) と同じ
(apply f 1 2 3 '(4))    ;; (f 1 2 3 4) と同じ

これら4つの呼び出しはすべて (f 1 2 3 4) と等価だ。 apply は引数がシーケンスとして渡されてきたがそのシーケンス内の値で関数を呼び出さなければならない場合に便利だ。

例えば、こう書くのを避けるために apply を使うことができる:

(defn plot [shape coords]   ;; coords は [x y]
  (plotxy shape (first coords) (second coords)))

代わりにシンプルに書くことができる:

(defn plot [shape coords]
  (apply plotxy shape coords))

ローカル変数とクロージャ

let

let は「レキシカルスコープ」でシンボルを値に束縛する。レキシカルスコープは名前のための新たなコンテキストを作り、それは周囲のコンテキストの内側にネストされる。 let で定義された名前は外側のコンテキストにおける名前に優先する。

;;      束縛        名前はここで定義されている
;;    ------------  ----------------------
(let  [name value]  (code that uses name))

個々の let は0個以上の束縛を定義し、本体に0個以上の式を持つことができる。

(let [x 1
      y 2]
  (+ x y))

この let 式は xy の2つのローカル束縛を作っている。式 (+ x y)let のレキシカルスコープ内にあり、xが1に、yが2に解決される。 let 式の外側では、xとyはすでに値に束縛されていない限り継続して意味を持つことはない。

(defn messenger [msg]
  (let [a 7
        b 5
        c (clojure.string/capitalize msg)]
    (println a b c)
  ) ;; let のスコープの終わり
) ;; 関数の終わり

messenger関数は msg 引数をとる。ここで defn もまた msg のレキシカルスコープを作っている―― messenger 関数の範囲内でだけ意味がある。

この関数のスコープの範囲内に leta, b, c を定義する新たなスコープを作っている。let式のあとで a を使おうとすれば、コンパイラがエラーを報告するだろう。

クロージャ

fn 特殊形式は「クロージャ」(closure)を作る。クロージャは周囲のレキシカルスコープ(上の msg, a, b, c など)を「閉じ込め」(close over)て、その値をレキシカルスコープを超えて捕捉する。

(defn messenger-builder [greeting]
  (fn [who] (println greeting who))) ; greeting を閉じ込める

;; greeting はここで与えられて、スコープを外れる
(def hello-er (messenger-builder "Hello"))

;; hello-er がクロージャなので greeting の値は引き続き得られる
(hello-er "world!")
;; Hello world!

Javaとの相互運用

Javaコードを呼び出す

以下はClojureからJavaを呼び出すための呼び出し規約(calling conventions)のまとめだ:

タスク Java Clojure

インスタンス化

new Widget("foo")

(Widget. "foo")

インスタンスメソッド

rnd.nextInt()

(.nextInt rnd)

インスタンスフィールド

object.field

(.-field object)

staticメソッド

Math.sqrt(25)

(Math/sqrt 25)

staticフィールド

Math.PI

Math/PI

Javaメソッド vs 関数

  • JavaのメソッドはClojureの関数ではない

  • 保持したり引数として渡したりすることはできない

  • 必要な場合に関数にラップすることができる

;; 引数に対して .length を呼び出すための関数を作る
(fn [obj] (.length obj))

;; 同じこと
#(.length %)

知識をテストしよう

1) 引数をとらず「Hello」と出力する関数 greet を定義しよう。 _ を実装で置き換えよう: (defn greet [] _)

2) greet を最初は fn 特殊形式、次は #() リーダーマクロとともに def を使って再定義しよう。

;; fn を使う
(def greet __)

;; #() を使う
(def greet __)

3) 次のような関数 greeting を定義しよう:

  • 引数を与えられなければ"Hello, World!"を返す

  • 1つの引数xを与えられると"Hello, x!"を返す

  • 2つの引数xとyを与えられると"x, y!"を返す

;; ヒント 文字列を結合するのに str 関数を使う
(doc str)

(defn greeting ___)

;; テスト用
(assert (= "Hello, World!" (greeting)))
(assert (= "Hello, Clojure!" (greeting "Clojure")))
(assert (= "Good morning, Clojure!" (greeting "Good morning" "Clojure")))

4) 単一の引数 x をとってそのまま返す関数 do-nothing を定義しよう。

(defn do-nothing [x] ___)

Clojureでは、これは identity 関数だ。それ自体ではidentityはあまり役に立たないが、高階関数を扱う際に必要になることがある。

(source identity)

5) 任意個の引数をとり、すべて無視して数 100 を返す関数 always-thing を定義しよう。

(defn always-thing [__] ___)

6) 単一の引数 x をとる関数 make-thingy を定義しよう。この関数は、任意個の引数をとって常にxを返すような別の関数を返す必要がある。

(defn make-thingy [x] ___)

;; テスト
(let [n (rand-int Integer/MAX_VALUE)
      f (make-thingy n)]
  (assert (= n (f)))
  (assert (= n (f 123)))
  (assert (= n (apply f 123 (range)))))

Clojureでは、これは constantly 関数だ。

(source constantly)

7) 別の関数をとって引数なしで3回呼び出す関数 triplicate を定義しよう。

(defn triplicate [f] ___)

8) 単一の引数 f をとる関数 opposite を定義しよう。この関数は、任意個の引数をとって f を適用して結果に not を呼び出すような別の関数を返す必要がある。Clojureの not 関数は論理否定をする。

(defn opposite [f]
  (fn [& args] ___))

Clojureでは、これはcomplement関数だ。

(defn complement
  "Takes a fn f and returns a fn that takes the same arguments as f,
  has the same effects, if any, and returns the opposite truth value."
  [f]
  (fn
    ([] (not (f)))
    ([x] (not (f x)))
    ([x y] (not (f x y)))
    ([x y & zs] (not (apply f x y zs)))))

9) 別の関数と任意個の引数をとり、その関数を引数に3回適用する関数 triplicate2 を定義しよう。前にtriplicateの問題で定義した関数を再利用しよう。

(defn triplicate2 [f & args]
  (triplicate ___))

10) java.lang.Math クラス(Math/pow, Math/cos, Math/sin, Math/PI)を使って、次の数学的な事実を示そう:

  • piのコサインは-1

  • あるxについて sin(x)^2 + cos(x)^2 = 1

11) HTTP URLを文字列としてとり、WebからそのURLで取得した内容を文字列として返す関数を定義しよう。

ヒント: java.net.URL クラスとその openStream を使う。そして内容を文字列として得るためにClojureの slurp 関数を使う。

(defn http-get [url]
  ___)

(assert (.contains (http-get "https://www.w3.org") "html"))

実は、Clojureの slurp 関数は引数をファイル名として解釈しようとする前に先にURLとして解釈する。単純化したhttp-getを書こう:

(defn http-get [url]
  ___)

12) 2つの引数をとる関数 one-less-arg を定義しよう:

  • f, 関数

  • x, 値

そして x と追加の引数に対して f を呼び出すような別の関数を返す。

(defn one-less-arg [f x]
  (fn [& args] ___))

Clojureでは、 partial 関数がこれをより一般化したものだ。

13) 2つの関数 fg を引数としてとる関数 two-fns を定義しよう。この関数は、1つの引数をとって g を呼び出してからその結果に f を呼び出した結果を返すような別の関数を返す。

つまり、その関数は fg の合成を返す。

(defn two-fns [f g]
  ___)