cv.cm

コーディング · プログラミング言語

Clojure入門:初心者向けチュートリアル

約10分で読めます

結論

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はローカルな名前を束縛します。値は作成後に変化しません。代入文はありません。

型例補足
Integer4264ビットのlong。42NはBigIntです
Floating point3.14Javaのdouble
Ratio(/ 1 3)0.333ではなく、正確な分数1/3
String"hello"JavaのString。二重引用符のみ
Character\a1文字
Keyword:name高速で自己評価されるラベル。マップのキーによく使います
Boolean / niltrue, 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にコンパイルされます。ホストとの連携は異なりますが、中核の関数とデータ構造は同じです。

コーディング講座の一覧