Clojure

Clojureを学ぶ - 関数

関数の作成

Clojureは関数型言語です。関数は第一級であり、他の関数に渡したり、他の関数から返したりすることができます。ほとんどのClojureコードは、主に純粋関数(副作用がない)で構成されているため、同じ入力で呼び出すと常に同じ出力が得られます。

defn は名前付き関数を定義します。

;;    name   params         body
;;    -----  ------  -------------------
(defn greet  [name]  (str "Hello, " name) )

この関数は、単一のパラメータnameを持ちますが、パラメータベクターには任意の数のパラメータを含めることができます。

関数を呼び出すには、関数名の位置(リストの最初の要素)に関数名を指定します。

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

多重引数関数

関数は、異なる数のパラメータ(異なる「引数」)を受け取るように定義できます。異なる引数はすべて同じdefnで定義する必要があります。defnを複数回使用すると、以前の関数が置き換えられます。

各引数は([param*] 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で作成できます。

;;    params         body
;;   ---------  -----------------
(fn  [message]  (println message) )

無名関数には名前がないため、後で参照することはできません。むしろ、無名関数は通常、別の関数に渡される時点で作成されます。

または、すぐに呼び出すことも可能です(これは一般的な用法ではありません)。

;;     operation (function)             argument
;; --------------------------------  --------------
(  (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などは、複数のパラメータに使用されます。

  • %&は、残りの(可変引数)パラメータに使用されます。

ネストされた無名関数は、パラメータに名前がないため、あいまいさが発生するため、ネストは許可されていません。

;; Equivalent to: (fn [x] (+ 6 x))
#(+ 6 %)

;; Equivalent to: (fn [x y] (+ x y))
#(+ %1 %2)

;; Equivalent to: (fn [x y & zs] (println x y zs))
#(println %1 %2 %&)

注意点

一般的なニーズの1つは、要素を受け取り、それをベクトルでラップする無名関数です。それを次のように記述しようとするかもしれません。

;; DO NOT DO THIS
#([%])

この無名関数は、同等のものに展開されます。

(fn [x] ([x]))

この形式はベクトルでラップするおよび引数なしでベクトルを呼び出そうとします(追加の括弧のペア)。代わりに

;; Instead do this:
#(vector %)

;; or this:
(fn [x] [x])

;; or most simply just the vector function itself:
vector

関数の適用

apply

apply関数は、0個以上の固定引数を使用して関数を呼び出し、残りの必要な引数を最後のシーケンスから取得します。最後の引数は必ずシーケンスである必要があります。

(apply f '(1 2 3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 '(2 3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 2 '(3 4))    ;; same as  (f 1 2 3 4)
(apply f 1 2 3 '(4))    ;; same as  (f 1 2 3 4)

これらの4つの呼び出しはすべて(f 1 2 3 4)と同等です。applyは、引数がシーケンスとして渡されているが、シーケンス内の値を使用して関数を呼び出す必要がある場合に便利です。

たとえば、applyを使用して、これを記述することを回避できます。

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

代わりに、次のように簡単に記述できます。

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

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

let

letは、「レキシカルスコープ」で記号を値にバインドします。レキシカルスコープは、名前の新しいコンテキストを作成し、周囲のコンテキスト内にネストされます。letで定義された名前は、外側のコンテキストの名前よりも優先されます。

;;      bindings     name is defined here
;;    ------------  ----------------------
(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)
  ) ;; end of let scope
) ;; end of function

messenger関数はmsg引数を受け取ります。ここで、defnmsgのレキシカルスコープも作成しています。これはmessenger関数内でのみ意味を持ちます。

その関数スコープ内で、letab、およびcを定義するための新しいスコープを作成します。let式の後にaを使用しようとすると、コンパイラはエラーを報告します。

クロージャ

fn特殊形式は「クロージャ」を作成します。これは、周囲のレキシカルスコープ(上記のmsgab、またはcなど)を「クローズオーバー」し、その値をレキシカルスコープを超えてキャプチャします。

(defn messenger-builder [greeting]
  (fn [who] (println greeting who))) ; closes over greeting

;; greeting provided here, then goes out of scope
(def hello-er (messenger-builder "Hello"))

;; greeting value still available because hello-er is a closure
(hello-er "world!")
;; Hello world!

Java相互運用

Javaコードの呼び出し

以下は、ClojureからJavaを呼び出すための呼び出し規則の概要です。

タスク Java Clojure

インスタンス化

new Widget("foo")

(Widget. "foo")

インスタンスメソッド

rnd.nextInt()

(.nextInt rnd)

インスタンスフィールド

object.field

(.-field object)

静的メソッド

Math.sqrt(25)

(Math/sqrt 25)

静的フィールド

Math.PI

Math/PI

Javaのメソッド vs 関数

  • JavaメソッドはClojure関数ではありません

  • それらを保存したり、引数として渡したりすることはできません

  • 必要な場合は、関数でラップできます

;; make a function to invoke .length on arg
(fn [obj] (.length obj))

;; same thing
#(.length %)

知識をテスト

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

2) defを使用して、最初にfn特殊形式を使用して、次に#()リーダマクロを使用して、greetを再定義します。

;; using fn
(def greet __)

;; using #()
(def greet __)

3) 次の関数greetingを定義します。

  • 引数が指定されていない場合は、「Hello, World!」を返します。

  • 1つの引数xが指定されている場合は、「Hello, x!」を返します。

  • 2つの引数xとyが指定されている場合は、「x, y!」を返します。

;; Hint use the str function to concatenate strings
(doc str)

(defn greeting ___)

;; For testing
(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] ___)

;; Tests
(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では、これは補完関数です。

(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/powMath/cosMath/sinMath/PI)を使用して、次の数学的事実を実証します。

  • 円周率のコサインは-1です

  • 一部のxについて、sin(x)^2 + cos(x)^2 = 1

11) HTTP URLを文字列として受け取り、そのURLをWebから取得し、コンテンツを文字列として返す関数を定義します。

ヒント: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]
  ___)