Clojure

Clojureを学ぶ - 構文

リテラル

以下は、Clojureにおける一般的なプリミティブのリテラル表現の例です。これらのリテラルはすべて有効なClojure式です。

;は行末までのコメントを作成します。ヘッダーコメントセクションを示すために複数のセミコロンが使用されることもありますが、これは単なる慣例です。

数値型

42        ; integer
-1.5      ; floating point
22/7      ; ratio

整数は、範囲内にある場合は固定精度64ビット整数として、それ以外の場合は任意精度として読み込まれます。末尾にNを使用すると、任意精度を強制できます。Clojureは、8進数(接頭辞0)、16進数(接頭辞0x)、および任意基数(接頭辞は基数、次にr、例えば2進数の場合は2r)整数のJava構文もサポートしています。比率は、分子と分母を組み合わせた独自の型として提供されます。

浮動小数点値は、倍精度64ビット浮動小数点数として、または末尾にMを付けて任意精度で読み込まれます。指数表記もサポートされています。特別なシンボル値##Inf##-Inf、および##NaNは、それぞれ正の無限大、負の無限大、および「非数」値を表します。

文字型

"hello"         ; string
\e              ; character
#"[0-9]+"       ; regular expression

文字列は二重引用符で囲まれ、複数行にまたがることができます。個々の文字は、先頭にバックスラッシュを付けて表されます。いくつかの特別な名前付き文字があります:\newline \space \tabなど。Unicode文字は\uNNNNまたは8進数で\oNNNで表すことができます。

リテラル正規表現は、先頭に#が付いた文字列です。これらはjava.util.regex.Patternオブジェクトにコンパイルされます。

シンボルと識別子

map             ; symbol
+               ; symbol - most punctuation allowed
clojure.core/+  ; namespaced symbol
nil             ; null value
true false      ; booleans
:alpha          ; keyword
:release/alpha  ; keyword with namespace

シンボルは、文字、数字、およびその他の句読点で構成され、関数、値、名前空間などの別の何かを参照するために使用されます。シンボルには、オプションで名前空間を含めることができ、名前からスラッシュで区切られます。

異なる型として読み込まれる3つの特別なシンボルがあります。nilはnull値であり、truefalseはブール値です。

キーワードは先頭にコロンで始まり、常にそれ自身に評価されます。Clojureでは、列挙値または属性名として頻繁に使用されます。

リテラルコレクション

Clojureには、4つのコレクション型の字句構文も含まれています。

'(1 2 3)     ; list
[1 2 3]      ; vector
#{1 2 3}     ; set
{:a 1, :b 2} ; map

これらについては後で詳しく説明します。今のところは、これらの4つのデータ構造を使用して複合データを作成できることを知っていれば十分です。

評価

次に、Clojureが式を読み取り評価する方法について検討します。

従来の評価(Java)

Java evaluation

Javaでは、ソースコード(.javaファイル)はコンパイラー(javac)によって文字として読み取られ、JVMによってロードできるバイトコード(.classファイル)が生成されます。

Clojureの評価

Clojure evaluation

Clojureでは、ソースコードはリーダーによって文字として読み取られます。リーダーは、.cljファイルからソースを読み取るか、一連の式を対話的に指定することができます。リーダーはClojureデータを生成します。次に、ClojureコンパイラーがJVMのバイトコードを生成します。

ここには2つの重要な点があります。

  1. ソースコードの単位は、Clojureのソースファイルではなく、Clojure式です。ソースファイルは、REPLで対話的に式を入力した場合と同じように、一連の式として読み取られます。

  2. リーダーとコンパイラーを分離することは、マクロの余地を作るための重要な分離です。マクロは、(データとしての)コードを取り、(データとしての)コードを出力する特別な関数です。評価モデルにマクロ展開のループを挿入できる場所がわかりますか?

構造 vs 意味

Clojure式を考えてみましょう。

Structure and semantics

この図は、緑色(リーダーによって生成されたClojureデータ構造)の構文と、青色(Clojureランタイムがそのデータをどのように理解するか)の意味の違いを示しています。

ほとんどのリテラルClojure形式は、シンボルとリストを除いて、それ自体に評価されます。シンボルは別の何かを参照するために使用され、評価されると、参照するものを返します。(図のように)リストは、呼び出しとして評価されます。

図では、(+ 3 4)は、シンボル(+)と2つの数値(3と4)を含むリストとして読み取られます。最初の要素(+が見つかる場所)は、「関数位置」、つまり呼び出すものを見つける場所と呼ぶことができます。関数は呼び出すものとしては明白ですが、ランタイム、マクロ、およびその他の少数の呼び出し可能なものにも知られているいくつかの特別な演算子があります。

上記の式の評価を考えると

  • 3と4はそれ自体(long)に評価されます。

  • +は、+を実装する関数に評価されます。

  • リストを評価すると、3と4を引数として+関数が呼び出されます。

多くの言語には、ステートフルな効果があるが値を返さないステートメントと式があります。Clojureでは、すべてが値に評価される式です。一部の式(ただし、ほとんどではない)には副作用もあります。

次に、Clojureで対話的に式を評価する方法を検討します。

クォーティングによる評価の遅延

特にシンボルとリストの場合、評価を中断すると便利な場合があります。シンボルが参照するものを検索せずに、単にシンボルである必要がある場合があります。

user=> 'x
x

また、リストが(評価するコードではなく)単にデータ値のリストである必要がある場合があります。

user=> '(1 2 3)
(1 2 3)

混乱する可能性のあるエラーの1つは、データリストをコードであるかのように評価しようとした結果です。

user=> (1 2 3)
Execution error (ClassCastException) at user/eval156 (REPL:1).
class java.lang.Long cannot be cast to class clojure.lang.IFn

今のところ、引用符についてあまり心配しないでください。ただし、シンボルまたはリストの評価を回避するために、これらの資料で時折見かけるでしょう。

REPL

Clojureを使用する場合のほとんどの場合、エディターまたはREPL(Read-Eval-Print-Loop)で使用します。REPLには次の部分があります。

  1. (文字の文字列である)式を読み取り、Clojureデータを生成します。

  2. #1から返されたデータを評価して、結果(これもClojureデータ)を生成します。

  3. データを文字に戻すことによって結果を出力します。

  4. 先頭に戻ります。

#2の重要な側面の1つは、Clojureが実行する前に常に式をコンパイルすることです。Clojureは常にJVMバイトコードにコンパイルされます。Clojureインタープリターはありません。

user=> (+ 3 4)
7

上のボックスは、式(+ 3 4)を評価して結果を受け取ることを示しています。

REPLでの探求

ほとんどのREPL環境は、対話的な使用を支援するためのいくつかのトリックをサポートしています。たとえば、いくつかの特別なシンボルは、最後の3つの式の評価結果を記憶しています。

  • *1(最後の結果)

  • *2(2つ前の式の結果)

  • *3(3つ前の式の結果)

user=> (+ 3 4)
7
user=> (+ 10 *1)
17
user=> (+ *1 *2)
24

さらに、標準のClojureライブラリに含まれているclojure.repl名前空間には、便利な関数が多数用意されています。そのライブラリをロードして、現在のコンテキストでその関数を使用できるようにするには、次を呼び出します。

(require '[clojure.repl :refer :all])

今のところ、それを魔法の呪文として扱うことができます。ポン!名前空間になったら、それを解き明かします。

これで、REPLで役立ついくつかの追加関数(docfind-docapropossource、およびdir)にアクセスできるようになりました。

doc関数は、任意の関数のドキュメントを表示します。+で呼び出してみましょう。

user=> (doc +)

clojure.core/+
([] [x] [x y] [x y & more])
  Returns the sum of nums. (+) returns 0. Does not auto-promote
  longs, will throw on overflow. See also: +'

doc関数は、有効な署名を含む、+のドキュメントを出力します。

doc関数はドキュメントを出力し、結果としてnilを返します。評価出力の両方で確認できます。

docをそれ自体で呼び出すこともできます。

user=> (doc doc)

clojure.repl/doc
([name])
Macro
  Prints documentation for a var or special form given its name

何かを何と呼ぶかわからない場合は、aproposコマンドを使用して、特定の文字列または正規表現に一致する関数を見つけることができます。

user=> (apropos "+")
(clojure.core/+ clojure.core/+')

また、find-docでドキュメント文字列自体を含めるように検索を拡大することもできます。

user=> (find-doc "trim")

clojure.core/subvec
([v start] [v start end])
  Returns a persistent vector of the items in vector from
  start (inclusive) to end (exclusive).  If end is not supplied,
  defaults to (count vector). This operation is O(1) and very fast, as
  the resulting vector shares structure with the original and no
  trimming is done.

clojure.string/trim
([s])
  Removes whitespace from both ends of string.

clojure.string/trim-newline
([s])
  Removes all trailing newline \n or return \r characters from
  string.  Similar to Perl's chomp.

clojure.string/triml
([s])
  Removes whitespace from the left side of string.

clojure.string/trimr
([s])
  Removes whitespace from the right side of string.

特定の名前空間の関数の完全なリストを表示する場合は、dir関数を使用できます。ここでは、clojure.repl名前空間で使用できます。

user=> (dir clojure.repl)

apropos
demunge
dir
dir-fn
doc
find-doc
pst
root-cause
set-break-handler!
source
source-fn
stack-element-str
thread-stopper

そして最後に、ランタイムからアクセスできる任意の関数のドキュメントだけでなく、基礎となるソースも確認できます。

user=> (source dir)

(defmacro dir
  "Prints a sorted directory of public vars in a namespace"
  [nsname]
  `(doseq [v# (dir-fn '~nsname)]
     (println v#)))

このワークショップを進めるにあたって、使用している関数のドキュメント文字列とソースを自由に調べてください。Clojureライブラリ自体の実装を調べることは、言語とその使用方法についてより多くを学ぶための優れた方法です。

Clojureチートシートのコピーを、Clojureを学習している間、開いておくこともお勧めです。チートシートは、標準ライブラリで使用できる関数を分類しており、非常に貴重な参考資料です。

それでは、Clojureの基本をいくつか検討して、始めましょう...

Clojureの基本

def

REPLで何かを評価しているときに、後で使用するためにデータを保存すると便利な場合があります。これはdefで行うことができます。

user=> (def x 7)
#'user/x

defは、現在の名前空間でシンボル(x)を値(7)に関連付ける特殊な形式です。このリンクはvarと呼ばれます。ほとんどの実際のClojureコードでは、varは定数値または関数を参照する必要がありますが、REPLで作業する場合の便宜のためにそれらを定義および再定義するのが一般的です。

上記の戻り値は#'user/xであることに注意してください。これは、varの文字列表現です。#'の後に名前空間付きのシンボルが続きます。userはデフォルトの名前空間です。

シンボルは、それが参照するものを調べることによって評価されることを思い出してください。したがって、シンボルを使用するだけで値を取得できます。

user=> (+ x x)
14

印刷

言語を学ぶときに最も一般的なことの1つは、値を出力することです。Clojureには、値を出力するためのいくつかの関数が用意されています。

人間向け データとして可読

改行付き

println

prn

改行なし

print

pr

人間が読める形式は、特殊な印刷文字(改行やタブなど)を印刷された形式に変換し、文字列の引用符を省略します。多くの場合、printlnを使用して関数をデバッグしたり、REPLで値を出力したりします。printlnは任意の数の引数を取り、各引数の印刷された値の間にスペースを挿入します。

user=> (println "What is this:" (+ 1 2))
What is this: 3

println関数には副作用(印刷)があり、結果としてnilを返します。

上記の「これは何ですか?」は、周囲の引用符を出力せず、リーダーがデータとして再度読み取ることができる文字列ではないことに注意してください。

その目的のためには、prnを使用してデータとして印刷します。

user=> (prn "one\n\ttwo")
"one\n\ttwo"

これで、印刷された結果は、リーダーが再度読み取ることができる有効な形式になります。コンテキストによっては、人間形式またはデータ形式のいずれかを好む場合があります。

知識をテストする

  1. REPLを使用して、7654と1234の合計を計算します。

  2. 次の代数式をClojure式として書き直してください: ( 7 + 3 * 4 + 5 ) / 10

  3. REPLのドキュメント機能を使用して、rem関数とmod関数のドキュメントを見つけてください。提供された式の結果をドキュメントに基づいて比較してください。

  4. find-docを使用して、最新のREPL例外のスタックトレースを出力する関数を見つけてください。