1. 序章

Clojure は、ScalaやKotlinと同様に、完全にJava仮想マシン上で実行される関数型プログラミング言語です。 ClojureはLisp派生物と見なされ、他のLisp言語の経験がある人なら誰でも知っているでしょう。

このチュートリアルでは、Clojure言語の概要を説明し、Clojure言語の使用を開始する方法と、Clojureがどのように機能するかについてのいくつかの重要な概念を紹介します。

2. Clojureのインストール

Clojureは、LinuxおよびmacOSで使用するためのインストーラーおよびコンビニエンススクリプトとして利用できます。 残念ながら、この段階では、Windowsにはそのようなインストーラーがありません。

ただし、LinuxスクリプトはCygwinやWindowsBashなどで機能する可能性があります。 言語をテストするために使用できるオンラインサービスもあります。古いバージョンには、使用できるスタンドアロンバージョンがあります。

2.1. スタンドアロンダウンロード

スタンドアロンJARファイルは、 MavenCentralからダウンロードできます。 残念ながら、1.8.0より新しいバージョンは、JARファイルがより小さなモジュールに分割されているため、この方法で簡単に機能しなくなりました。

このJARファイルがダウンロードされると、実行可能JARとして扱うだけで、インタラクティブREPLとして使用できます。

$ java -jar clojure-1.8.0.jar
Clojure 1.8.0
user=>

2.2. REPLへのWebインターフェース

Clojure REPLへのWebインターフェースは、 https://repl.it/languages/clojure で入手でき、何もダウンロードせずに試すことができます。 現在、これはClojure 1.8.0のみをサポートし、新しいリリースはサポートしていません。

2.3. MacOSのインストーラー

macOSを使用していて、Homebrewがインストールされている場合、Clojureの最新リリースを簡単にインストールできます。

$ brew install clojure

これは、執筆時点で最新バージョンのClojure –1.10.0をサポートします。 インストールすると、clojureまたはcljコマンドを使用するだけでREPLをロードできます。

$ clj
Clojure 1.10.0
user=>

2.4. Linux上のインストーラー

Linuxにツールをインストールするために、自己インストール型のシェルスクリプトを使用できます。

$ curl -O https://download.clojure.org/install/linux-install-1.10.0.411.sh
$ chmod +x linux-install-1.10.0.411.sh
$ sudo ./linux-install-1.10.0.411.sh

macOSインストーラーと同様に、これらはClojureの最新リリースで使用可能であり、clojureまたはcljコマンドを使用して実行できます。

3. ClojureREPLの概要

上記のすべてのオプションにより、ClojureREPLにアクセスできます。 これは、Java9以降のJShellツールと同等のClojureを直接使用するものであり、Clojureコードを入力して、結果をすぐに直接確認できます。 これは、特定の言語機能がどのように機能するかを実験して発見するための素晴らしい方法です。

REPLがロードされると、標準のClojureコードを入力してすぐに実行できるプロンプトが表示されます。 これには、単純なClojureコンストラクト、および他のJavaライブラリーとの相互作用が含まれますが、ロードするにはクラスパスで使用可能である必要があります。

REPLのプロンプトは、現在作業している名前空間を示しています。 私たちの仕事の大部分では、これは user 名前空間であるため、プロンプトは次のようになります。

user=>

この記事の残りの部分はすべて、Clojure REPLにアクセスできることを前提としており、そのようなツールで直接機能します。

4. 言語の基本

Clojure言語は、他の多くのJVMベースの言語とは非常に異なって見え、最初は非常に珍しいように見えるかもしれません。 これはLispの方言と見なされ、他のLisp言語と非常によく似た構文と機能を備えています。

Clojureで作成するコードの多くは、他のLisp方言と同様に、Listsの形式で表現されています。 次に、リストを評価して、より多くのリストまたは単純な値の形式で結果を生成できます。

例えば:

(+ 1 2) ; = 3

これは3つの要素で構成されるリストです。 「+」記号は、この呼び出し(加算)を実行していることを示します。 残りの要素は、この呼び出しで使用されます。 したがって、これは「1+2」と評価されます。

ここでリスト構文を使用することにより、これを簡単に拡張できます。 たとえば、次のことができます。

(+ 1 2 3 4 5) ; = 15

そして、これは「1 + 2 + 3 +4+5」と評価されます。

セミコロン文字にも注意してください。 これはClojureでコメントを示すために使用され、Javaで見られるような式の終わりではありません。

4.1。 単純種類

ClojureはJVM上に構築されているため、他のJavaアプリケーションと同じ標準タイプにアクセスできます。 タイプは通常自動的に推測され、明示的に指定する必要はありません。

例えば:

123 ; Long
1.23 ; Double
"Hello" ; String
true ; Boolean

特別なプレフィックスまたはサフィックスを使用して、より複雑なタイプを指定することもできます。

42N ; clojure.lang.BigInt
3.14159M ; java.math.BigDecimal
1/3 ; clojure.lang.Ratio
#"[A-Za-z]+" ; java.util.regex.Pattern

java.math.BigIntegerの代わりにclojure.lang.BigIntタイプが使用されることに注意してください。 これは、Clojureタイプにいくつかのマイナーな最適化と修正があるためです。

4.2. キーワードと記号

Clojureは、キーワードとシンボルの両方の概念を提供します。 キーワードはそれ自体のみを参照し、マップキーなどによく使用されます。 一方、記号は他のものを指すために使用される名前です。 たとえば、変数の定義と関数名はシンボルです。

接頭辞がコロンの名前を使用してキーワードを作成できます。

user=> :kw
:kw
user=> :a
:a

キーワードはそれ自体と直接同等であり、他のものとは直接同等ではありません。

user=> (= :a :a)
true
user=> (= :a :b)
false
user=> (= :a "a")
false

単純な値ではないClojureの他のほとんどのものは、シンボルと見なされます。 これらはを参照するものに評価されますが、キーワードは常にそれ自体に評価されます。

user=> (def a 1)
#'user/a
user=> :a
:a
user=> a
1

4.3. 名前空間

Clojure言語には、コードを編成するためのネームスペースの概念があります。 私たちが書くコードはすべて名前空間に存在します。

デフォルトでは、REPLは user 名前空間で実行されます。これは、「user=>」というプロンプトで確認できます。

nsキーワードを使用して名前名を作成および変更できます

user=> (ns new.ns)
nil
new.ns=>

名前空間を変更すると、古い名前空間で定義されていたものはすべて使用できなくなり、新しい名前空間で定義されたものはすべて使用できるようになります。

名前空間を完全に修飾することで、名前空間全体の定義にアクセスできます。 たとえば、名前空間 clojure.string は、関数upper-caseを定義します。

clojure.string 名前空間にいる場合は、直接アクセスできます。 そうでない場合は、 clojure.string /upper-caseとして修飾する必要があります。

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; This is not visible in the "user" namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: upper-case in this context
user=> (ns clojure.string)
nil
clojure.string=> (upper-case "hello") ; This is visible because we're now in the "clojure.string" namespace
"HELLO"

require keyword を使用して、別の名前空間の定義に簡単にアクセスすることもできます。 これを使用できる主な方法は2つあります。使いやすいように短い名前の名前空間を定義する方法と、プレフィックスなしで別の名前空間の定義に直接アクセスする方法です。

clojure.string=> (require '[clojure.string :as str])
nil
clojure.string=> (str/upper-case "Hello")
"HELLO"

user=> (require '[clojure.string :as str :refer [upper-case]])
nil
user=> (upper-case "Hello")
"HELLO"

これらは両方とも現在の名前空間にのみ影響するため、別の名前空間に変更するには新しい名前空間が必要になります必要。 これにより、名前空間がよりクリーンに保たれ、必要なものだけにアクセスできるようになります。

4.4. 変数

単純な値を定義する方法がわかれば、それらを変数に割り当てることができます。キーワード def を使用してこれを行うことができます:

user=> (def a 123)
#'user/a

これが完了すると、記号aを使用できるようになります。 この値を表現したい場所:

user=> a
123

変数の定義は、必要に応じて単純または複雑にすることができます。

たとえば、変数を数値の合計として定義するには、次のようにします。

user=> (def b (+ 1 2 3 4 5))
#'user/b
user=> b
15

変数を宣言したり、変数のタイプを示したりする必要がないことに注意してください。 Clojureは、これらすべてを自動的に判断します。

定義されていない変数を使用しようとすると、代わりにエラーが発生します。

user=> unknown
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: unknown in this context
user=> (def c (+ 1 unknown))
Syntax error compiling at (REPL:1:8).
Unable to resolve symbol: unknown in this context

def関数の出力が入力とわずかに異なることに注意してください。 変数aを定義すると、‘user /aの文字列が返されます。 これは、結果がシンボルであり、このシンボルが現在の名前空間で定義されているためです。

4.5. 機能

Clojureで関数を呼び出す方法の例をいくつか見てきました。 呼び出される関数で始まり、次にすべてのパラメーターで始まるリストを作成します。

このリストが評価されると、関数から戻り値が取得されます。 例えば:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]
user=> (java.time.Instant/parse "2019-01-15T07:55:00Z")
#object[java.time.Instant 0x6b8d96d9 "2019-01-15T07:55:00Z"]
user=> (java.time.OffsetDateTime/of 2019 01 15 7 56 0 0 java.time.ZoneOffset/UTC)
#object[java.time.OffsetDateTime 0xf80945f "2019-01-15T07:56Z"]

1つの関数呼び出しの出力をパラメーターとして別の関数に渡したい場合は、関数の呼び出しをネストすることもできます。

user=> (java.time.OffsetDateTime/of 2018 01 15 7 57 0 0 (java.time.ZoneOffset/ofHours -5))
#object[java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07:57-05:00"]

また、必要に応じて関数を定義することもできます。 関数はfnコマンドを使用して作成されます。

user=> (fn [a b]
  (println "Adding numbers" a "and" b)
  (+ a b)
)
#object[user$eval165$fn__166 0x5644dc81 "user$eval165$fn__166@5644dc81"]

残念ながら、これは関数に使用できる名前を与えません。 代わりに、 def、を使用して、この関数を表すシンボルを定義できます。これは、変数で見たとおりです。

user=> (def add
  (fn [a b]
    (println "Adding numbers" a "and" b)
    (+ a b)
  )
)
#'user/add

この関数を定義したので、他の関数と同じように呼び出すことができます。

user=> (add 1 2)
Adding numbers 1 and 2
3

便宜上、 Clojureでは、defnを使用して1つのgoで名前付きの関数を定義することもできます。

例えば:

user=> (defn sub [a b]
  (println "Subtracting" b "from" a)
  (- a b)
)
#'user/sub
user=> (sub 5 2)
Subtracting 2 from 5
3

4.6. Letとローカル変数

def呼び出しは、現在の名前空間に対してグローバルなシンボルを定義します。 これは通常、コードを実行するときに望まれることではありません。 代わりに、 Clojureは、ブロックにローカルな変数を定義するためのlet呼び出しを提供します。 これは、変数が関数の外に漏れないようにする関数内でそれらを使用する場合に特に便利です。

たとえば、サブ関数を定義できます。

user=> (defn sub [a b]
  (def result (- a b))
  (println "Result: " result)
  result
)
#'user/sub

ただし、これを使用すると、次の予期しない副作用が発生します。

user=> (sub 1 2)
Result:  -1
-1
user=> result ; Still visible outside of the function
-1

代わりに、letを使用して書き直してみましょう。

user=> (defn sub [a b]
  (let [result (- a b)]
    (println "Result: " result)
    result
  )
)
#'user/sub
user=> (sub 1 2)
Result:  -1
-1
user=> result
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: result in this context

今回は、resultシンボルは関数の外部には表示されません。 または、実際、それが使用されたletブロックの外側。

5. コレクション

これまでのところ、私たちは主に単純な値を操作してきました。 リストも見ましたが、それ以上はありません。 Clojureには、使用できるコレクションの完全なセットがありますが、リスト、ベクター、マップ、およびセットで構成されています

  • ベクトルは値の順序付きリストです。他のコレクションを含め、任意の値をベクトルに入れることができます。
  • セットは順序付けられていない値のコレクションであり、同じ値を複数回含めることはできません。
  • マップは、キーと値のペアの単純なセットです。 マップのキーとしてキーワードを使用することは非常に一般的ですが、他のコレクションを含め、任意の値を使用できます。
  • リストはベクトルに非常に似ています。 違いは、JavaのArrayListLinkedListの違いに似ています。 通常はベクトルが推奨されますが、最初に要素を追加する場合、または要素に順番にアクセスするだけの場合は、リストの方が適しています。

5.1. コレクションの構築

これらのそれぞれの作成は、省略表記または関数呼び出しを使用して行うことができます。

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

; List
user=> '(1 2 3)
(1 2 3)
user=> (list 1 2 3)
(1 2 3)

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

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

SetMapの例では、同じ順序で値が返されないことに注意してください。 これは、これらのコレクションが本質的に順序付けられておらず、表示される内容がメモリ内でどのように表現されているかによって異なるためです。

また、リストを作成するための構文は、式の標準のClojure構文と非常に似ていることがわかります。 Clojure式は、実際には評価されるリストですが、ここでのアポストロフィ文字は、評価するのではなく、実際の値のリストが必要であることを示しています。

もちろん、他の値と同じ方法でコレクションを変数に割り当てることができます。 あるコレクションを別のコレクション内のキーまたは値として使用することもできます。

リストはseqと見なされます。 これは、クラスがISeqインターフェースを実装することを意味します。 他のすべてのコレクションは、seq関数を使用してseqに変換できます。

user=> (seq [1 2 3])
(1 2 3)
user=> (seq #{1 2 3})
(1 3 2)
user=> (seq {:a 1 2 3})
([:a 1] [2 3])

5.2. コレクションへのアクセス

コレクションができたら、コレクションを操作して値を元に戻すことができます。 それぞれが異なるセマンティクスを持っているため、これをどのように行うことができるかは、問題のコレクションにわずかに依存します。

Vectorsは、indexによって任意の値を取得できる唯一のコレクションです。 これは、ベクトルとインデックスを式として評価することによって行われます。

user=> (my-vector 2) ; [1 2 3]
3

マップについても同じ構文を使用して同じことができます

user=> (my-map :b)
2

ベクトルとリストにアクセスして、最初の値、最後の値、およびリストの残りの部分を取得するための関数もあります。

user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)

マップには、キーと値のリスト全体を取得するための追加機能があります。

user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)

設定する必要がある唯一の実際のアクセスは、特定の要素がメンバーであるかどうかを確認することです。

これは、他のコレクションへのアクセスと非常によく似ています。

user=> (my-set 1)
1
user=> (my-set 5)
nil

5.3. コレクションの識別

コレクションにアクセスする方法は、コレクションの種類によって異なることがわかりました。 特定の方法とより一般的な方法の両方で、これを決定するために使用できる一連の関数があります。

各コレクションには、特定の値がそのタイプであるかどうかを判別するための特定の関数があります。リストの場合は list?、セットの場合は set?などです。 さらに、特定の値が任意の種類の seqであるかどうかを判断するためのseq?と、特定の値があらゆる種類–これはベクトルとマップを意味します。

user=> (vector? [1 2 3]) ; A vector is a vector
true
user=> (vector? #{1 2 3}) ; A set is not a vector
false
user=> (list? '(1 2 3)) ; A list is a list
true
user=> (list? [1 2 3]) ; A vector is not a list
false
user=> (map? {:a 1 :b 2}) ; A map is a map
true
user=> (map? #{1 2 3}) ; A set is not a map
false
user=> (seq? '(1 2 3)) ; A list is a seq
true
user=> (seq? [1 2 3]) ; A vector is not a seq
false
user=> (seq? (seq [1 2 3])) ; A vector can be converted into a seq
true
user=> (associative? {:a 1 :b 2}) ; A map is associative
true
user=> (associative? [1 2 3]) ; A vector is associative
true
user=> (associative? '(1 2 3)) ; A list is not associative
false

5.4. コレクションの変更

Clojureでは、ほとんどの関数型言語と同様に、すべてのコレクションは不変です。 コレクションを変更するために行うことはすべて、変更を表すために作成される新しいコレクションになります。 これにより、効率が大幅に向上し、偶発的な副作用のリスクがなくなります。

ただし、これを理解することにも注意する必要があります。そうしないと、コレクションに期待される変更が行われなくなります。

ベクトル、リスト、またはセットへの新しい要素の追加は、conjを使用して行われます。 これは、これらの各ケースで異なる動作をしますが、基本的な目的は同じです。

user=> (conj [1 2 3] 4) ; Adds to the end
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; Adds to the beginning
(4 1 2 3)
user=> (conj #{1 2 3} 4) ; Unordered
#{1 4 3 2}
user=> (conj #{1 2 3} 3) ; Adding an already present entry does nothing
#{1 3 2}

disjを使用してセットからエントリを削除することもできます。 リストまたはベクターは厳密に順序付けられているため、これは機能しないことに注意してください。

user=> (disj #{1 2 3} 2) ; Removes the entry
#{1 3}
user=> (disj #{1 2 3} 4) ; Does nothing because the entry wasn't present
#{1 3 2}

マップへの新しい要素の追加は、assocを使用して行われます。 dissoc:を使用してマップからエントリを削除することもできます

user=> (assoc {:a 1 :b 2} :c 3) ; Adds a new key
{:a 1, :b 2, :c 3}
user=> (assoc {:a 1 :b 2} :b 3) ; Updates an existing key
{:a 1, :b 3}
user=> (dissoc {:a 1 :b 2} :b) ; Removes an existing key
{:a 1}
user=> (dissoc {:a 1 :b 2} :c) ; Does nothing because the key wasn't present
{:a 1, :b 2}

5.5. 関数型プログラミングコンストラクト

Clojureは、本質的に関数型プログラミング言語です。 この意味は多くの従来の関数型プログラミングの概念にアクセスできます。 マップ、フィルター、減らす。 これらは通常、他の言語と同じように機能します 。 ただし、正確な構文は少し異なる場合があります。

具体的には、これらの関数は通常、最初の引数として適用する関数と、2番目の引数として適用するコレクションを取ります。

user=> (map inc [1 2 3]) ; Increment every value in the vector
(2 3 4)
user=> (map inc #{1 2 3}) ; Increment every value in the set
(2 4 3)

user=> (filter odd? [1 2 3 4 5]) ; Only return odd values
(1 3 5)
user=> (remove odd? [1 2 3 4 5]) ; Only return non-odd values
(2 4)

user=> (reduce + [1 2 3 4 5]) ; Add all of the values together, returning the sum
15

6. 制御構造

すべての汎用言語と同様に、Clojureの機能は、条件やループなどの標準的な制御構造を必要とします。

6.1. 条件付き

条件はifステートメントによって処理されます。 これには、テスト、テストが true の場合に実行するブロック、およびテストがfalseの場合に実行するブロックの3つのパラメーターが必要です。 これらはそれぞれ、オンデマンドで評価される単純な値または標準リストにすることができます。

user=> (if true 1 2)
1
user=> (if false 1 2)
2

テストは、必要なものなら何でもかまいません。 true /falseの値である必要はありません。 また、必要な値を提供するために評価されるブロックにすることもできます。

user=> (if (> 1 2) "True" "False")
"False"

を含むすべての標準チェック =、>、 < 、ここで使用できます。 他のさまざまな理由で使用できる述語のセットもあります。たとえば、コレクションを見るときにすでにいくつか見ました。

user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

テストは任意の値を返すことができます。trueまたはfalseである必要はありません。 ただし、値がfalseまたはnil以外の場合は、trueと見なされます。 これは、JavaScriptの動作方法とは異なります。この場合、「true-y」と見なされるがtrueではないと見なされる値のセットが多数あります。

user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"

6.2. ループ

コレクションの機能サポートは、ループ作業の多くを処理します –コレクションにループを記述する代わりに、標準関数を使用して、言語に反復を実行させます。

これ以外では、ループは完全にrecursionを使用して実行されます。 再帰関数を作成することも、loopおよびrecurキーワードを使用して再帰スタイルループを作成することもできます。

user=> (loop [accum [] i 0]
  (if (= i 10)
    accum
    (recur (conj accum i) (inc i))
  ))
[0 1 2 3 4 5 6 7 8 9]

loop 呼び出しは、反復ごとに実行される内部ブロックを開始し、いくつかの初期パラメーターを設定することから開始します。 次に、 recur 呼び出しがループにコールバックし、反復に使用する次のパラメーターを提供します。 recur が呼び出されない場合、ループは終了します。

この場合、 i の値が10に等しくないたびにループし、10に等しくなるとすぐに、累積された数値のベクトルを返します。

7. 概要

この記事では、Clojureプログラミング言語の概要を説明し、構文がどのように機能するか、および構文で実行できるいくつかのことを示します。これは入門レベルにすぎず、その言語でできることはすべて。

しかし、それを手に取って、試してみて、それを使って何ができるか見てみませんか。