コーディング · プログラミング言語
Clojure入門:初心者向けチュートリアル
結論
Clojureは、Java仮想マシン(JVM)上で動く動的な関数型Lispです(JavaScript向けにはClojureScriptがあります)。コードはかっこで囲んだリストとして書き、データはデフォルトでイミュータブルで、ベクター、マップ、集合に対する小さな関数を組み合わせてプログラムを作ります。Clojure CLIをインストールし、cljでREPLを起動し、clojure -M hello.cljでファイルを実行します。
Clojureとは何か、誰が使うのか
Clojureは、Rich Hickeyが作り、2007年に最初にリリースされました。Lispの方言で、すべての式は、最初の要素が関数または演算子であるリストです。たとえば(+ 1 2)のようになります。JVMのバイトコードにコンパイルされるので、あらゆるJavaライブラリを直接呼び出せます。ClojureScriptは、同じ言語をブラウザーやNode.js向けにJavaScriptへコンパイルします。
Clojureは、バックエンドのWebサービス、データ処理パイプライン、社内の業務システムに使われています。特に、イミュータブルなデータとインタラクティブな開発を重視するチームに向いています。開発者はたいてい、エディターにつないだREPL(read-eval-print loop)の中で、プログラムを動かしたまま1つずつ式を評価しながら作業します。
Clojureのインストールとhello, worldの実行
Javaランタイム(JDK 17以降なら安心です)をインストールし、clojure.orgからClojure CLIツールをインストールします(macOSはHomebrew、Linuxはインストールスクリプト、Windowsはインストーラー)。CLIには2つのコマンドがあります。行編集ができるREPLのcljと、プログラムを実行するclojureです。
;; hello.clj
(println "Hello, world!")clojure -M hello.clj
# Hello, world!
clj
# user=> (+ 1 2)
# 3実際のプロジェクトでは、依存関係をdeps.ednファイルに、コードをsrc/の下に置き、1ファイルにつき1つの名前空間を割り当てます。Leiningen(lein)は、project.cljファイルを使う、より古いものの今でも人気のあるビルドツールです。
値・型・名前
Clojureは動的型付けです。defはグローバルな名前(var)を束縛し、letはローカルな名前を束縛します。値は作成後に変化しません。代入文はありません。
| 型 | 例 | 補足 |
|---|---|---|
| Integer | 42 | 64ビットのlong。42NはBigIntです |
| Floating point | 3.14 | Javaのdouble |
| Ratio | (/ 1 3) | 0.333ではなく、正確な分数1/3 |
| String | "hello" | JavaのString。二重引用符のみ |
| Character | \a | 1文字 |
| Keyword | :name | 高速で自己評価されるラベル。マップのキーによく使います |
| Boolean / nil | true, false, nil | 偽になるのはfalseとnilだけです |
(def pi-approx 3.14159)
(let [r 2
area (* pi-approx r r)]
(println "Area:" area))
;; Area: 12.56636
(str "a" 1 :b) ;; => "a1:b"制御構文
Clojureでは、ifを含めてすべてが値を返す式です。ifは「then」と「else」の式をちょうど1つずつ取ります。elseのない複数の副作用にはwhenを、複数の条件にはcondを使います。
(defn grade [score]
(cond
(>= score 90) "A"
(>= score 80) "B"
:else "C"))
(println (grade 85)) ;; B
(when (pos? 5) (println "positive") :done)
(doseq [x [1 2 3]] (println (* x x))) ;; 1 4 9
(for [x (range 5) :when (odd? x)] (* x 10)) ;; => (10 30)
(loop [i 0 acc 0]
(if (< i 5) (recur (inc i) (+ acc i)) acc)) ;; => 10書き換えるカウンターを持つforループはありません。doseqは各要素に対して副作用を実行し、forは遅延シーケンス(リスト内包表記)を作り、loop/recurはコールスタックを増やさずに繰り返しを表します。
関数
名前付き関数はdefnで定義します。関数は複数のアリティ(引数の数)を持てます。#(...)は小さな無名関数の短縮形で、%が引数です。
(defn greet
([] (greet "world"))
([name] (str "Hello, " name "!")))
(greet) ;; => "Hello, world!"
(greet "Ana") ;; => "Hello, Ana!"
(defn sum-all [& nums] (reduce + nums))
(sum-all 1 2 3) ;; => 6
(map #(* % %) [1 2 3]) ;; => (1 4 9)
(filter even? (range 10)) ;; => (0 2 4 6 8)関数は値です。map、filter、reduceに渡したり、別の関数から返したり、マップに入れたりできます。
コレクション:リスト・ベクター・マップ・集合
Clojureの4つの中核コレクションは、イミュータブルで永続的です。「変更」すると、古いものと構造の大半を共有する新しいバージョンが返されるので、更新は低コストです。
| コレクション | リテラル | 主な用途 |
|---|---|---|
| List | '(1 2 3) | コードと、先頭に追加する順序付きデータ |
| Vector | [1 2 3] | 添字でアクセスできる列。末尾に追加します |
| Map | {:name "Ana" :age 30} | キーと値のレコード |
| Set | #{:red :green} | 重複のない値、所属の判定 |
(def user {:name "Ana" :age 30})
(:name user) ;; => "Ana"
(assoc user :email "[email protected]") ;; new map with :email
(update user :age inc) ;; => {:name "Ana", :age 31}
(conj [1 2] 3) ;; => [1 2 3]
(contains? #{:red :green} :red) ;; => true
(get-in {:a {:b 1}} [:a :b]) ;; => 1
user ;; unchanged: {:name "Ana", :age 30}特徴的な機能:スレッディングマクロ・アトム・マクロ
スレッディングマクロを使うと、データのパイプラインを上から下へ読めます。->は前の結果を次の式の第1引数として挿入し、->>は最後の引数として挿入します。
(->> (range 1 11)
(filter odd?)
(map #(* % %))
(reduce +)) ;; => 165
(def counter (atom 0))
(swap! counter inc)
(swap! counter + 10)
@counter ;; => 11
(defmacro unless [test & body]
`(if ~test nil (do ~@body)))
(unless false (println "runs"))アトムは変化しうる状態を保持します。swap!は関数をアトミックに適用するため、複数のスレッドから安全に使えます。そのほかの並行処理にはrefとエージェントがあります。コードはデータ(リスト)なので、マクロはコンパイル前にコードを変換できます。when、cond、->もマクロです。
エラー処理
ClojureはJVMの例外をtry、catch、finallyで扱います。エラーにデータを付けるにはex-infoを投げ、ex-dataでそのデータを取り出します。
(defn parse-age [s]
(let [n (Long/parseLong s)]
(when (neg? n)
(throw (ex-info "Age must not be negative" {:value n})))
n))
(try
(parse-age "-5")
(catch clojure.lang.ExceptionInfo e
(println (ex-message e) (ex-data e)))
(catch NumberFormatException _
(println "not a number")))
;; Age must not be negative {:value -5}小さな完成プログラム:単語の出現頻度
(ns wordcount
(:require [clojure.string :as str]))
(defn word-counts [text]
(->> (str/split (str/lower-case text) #"\W+")
(remove str/blank?)
frequencies
(sort-by val >)))
(doseq [[word n] (take 2 (word-counts "The cat and the hat and the bat"))]
(println word n))
;; the 3
;; and 2このプログラムは、テキストを小文字にし、正規表現リテラル#"\W+"で単語以外の文字で分割し、frequencies(マップを返します)で各単語を数え、回数の多い順にエントリーを並べ替えます。
次に学ぶこと
- clojure.orgの公式ガイド:Getting Started、Learn Clojure、deps.ednガイド。
- リーダー構文、特殊形式、データ構造に関するClojureのリファレンスページ。
- REPLに接続できるエディターを整える(たとえばVS Code向けのCalva、Emacs向けのCIDER、IntelliJ向けのCursive)。
- ブラウザーを対象にしたいなら、ClojureScriptを試す。
よくある質問
Clojureは関数型言語ですか?
はい。Clojureは純粋関数とイミュータブルなデータを重視しますが、実用的でもあります。副作用を許し、アトム、ref、エージェントによって管理された変更可能な状態も提供しています。
Clojureを学ぶのにJavaの知識は必要ですか?
いいえ。Javaを知らなくても言語は学べますが、Javaランタイムがインストールされている必要があります。スタックトレースを読んだり、Javaライブラリを使ったりするときには、JVMについて少し知っていると役立ちます。
Clojureのリストとベクターの違いは何ですか?
リスト'(1 2 3)は先頭に要素を追加し、主にコードに使われます。ベクター[1 2 3]は末尾に追加し、添字による高速なアクセスをサポートします。Clojureのプログラムのデータの大半は、ベクターとマップに保存されます。
Clojureはなぜかっこが多いのですか?
ClojureのコードはLispのデータ構造として書かれるため、すべての呼び出しがリストになります。この一貫した構文がマクロを可能にしており、構造的な編集に対応したエディターを使えば、かっこの管理は簡単になります。
ClojureとClojureScriptの違いは何ですか?
同じ言語が異なるホストで動くものです。ClojureはJVM上で動き、ClojureScriptはJavaScriptにコンパイルされます。ホストとの連携は異なりますが、中核の関数とデータ構造は同じです。