コンテンツにスキップ

Clojure

出典: フリー教科書『ウィキブックス(Wikibooks)』

情報技術 > プログラミング > Clojure



Wikipedia
Wikipedia
ウィキペディアClojureの記事があります。

この本は、プログラミング言語のClojureについての教科書です。ClojureはLisp方言の1つで、Javaプラットフォーム上で動作することができ、Javaのクラスやライブラリを直接使用することができます。また、動的な型付けを持つ関数型プログラミング言語であり、可変性の管理やコレクションの処理、マクロの定義などに優れています。Clojureの特徴の一つは、不変データ構造の使用であり、同時実行処理を容易にし、コードの信頼性や安全性を向上させることができます。Clojureはまた、シンプルで効率的なマクロシステムを備えており、コードの再利用性を高めることができます。Webアプリケーションや大規模システムの構築など、多くの領域でClojureが活用されており、プログラマがより生産的になることができるため、魅力的な選択肢の一つとなっています。

Clojureの紹介

[編集]

Clojureの歴史と哲学

[編集]

Clojureは、Rich Hickeyによって設計され、2007年に最初にリリースされました。 Clojureは、Lisp方言のプログラミング言語の1つで、Javaプラットフォームで動作するように設計されています。 Clojureの設計哲学は、簡単で正確なプログラミング、関数型プログラミング、並列プログラミングを可能にすることです。

Rich Hickeyは、Javaでの開発の複雑さに直面し、単純さと正確さが優れたソフトウェアの設計と実装に必要であると考えました。また、Javaのスレッドモデルに対する批判もありました。Rich Hickeyは、ClojureがJavaの機能を利用しながら、よりシンプルで正確な言語を提供することができると考えました。

Clojureは、Lisp方言の1つで、Lispの多くの特徴を引き継いでいます。Lispは、世界で最も古いプログラミング言語の1つであり、シンプルな構文と強力なマクロシステムを備えています。Clojureは、Lispの優れた機能に加えて、Javaの強力なエコシステムと統合されています。

Clojureのもう一つの設計哲学は、関数型プログラミングです。関数型プログラミングは、可変の状態を避け、関数の純粋性を維持することによって、プログラミングの複雑さを減らすことを目的としています。Clojureは、不変データ構造を提供し、多くの組み込みの高階関数を備えています。これらの機能により、Clojureは、関数型プログラミングの特性を簡単に利用できます。

Clojureの最後の設計哲学は、並列プログラミングです。Clojureは、Javaプラットフォームの並列処理機能を利用し、シンプルで安全な並列処理を提供します。Clojureは、多くの並列処理関数を備えており、並列化されたコードを書くことが容易になっています。

Clojureは、このような設計哲学に基づいて設計され、Javaのエコシステムとの統合が非常に強力です。

Clojureの特徴とメリット

[編集]

Clojureの特徴とメリット:

  1. 関数型プログラミング:Clojureは、関数型プログラミングを強くサポートし、変数の変更や状態の共有を最小限に抑え、副作用のない関数で構成されるプログラムを書くことを奨励します。
  2. Javaプラットフォームとのシームレスな統合:Clojureは、JVM上で動作するため、Javaのクラスやライブラリを利用することができます。これにより、Javaの大規模なエコシステムを活用できるため、開発効率が向上します。Clojureは、Javaとの相互運用性が高く、JavaからClojureの関数を呼び出すこともできます。
  3. コンカレントプログラミングのサポート:Clojureは、コンカレントプログラミングをサポートしています。これは、複数のスレッドやプロセスを使用して、処理を並行して実行することができることを意味します。Clojureの並列処理は、状態を変更しない純粋な関数型プログラミングのスタイルに合わせて設計されているため、競合状態やデッドロックなどの問題を回避できます。
  4. マクロ:Clojureは、マクロを使用して、自分自身の言語機能を拡張することができます。マクロは、Lispの特徴であり、Clojureでは、より高度なコントロールフローや言語機能を簡単に実現するために使用されます。
  5. データ駆動設計:Clojureは、データ駆動設計をサポートしており、データとデータの変換によってアプリケーションを構築することができます。Clojureのデータ構造は、Javaのオブジェクトと相互運用可能であり、Javaライブラリの使用も可能です。
  6. シンプルでエレガントな構文:Clojureの構文は、シンプルでエレガントであり、Lispの方言であるため、S式(式を括弧で囲む)を使用しているため、読み書きが容易で、保守性が高くなります。
  7. 高速でスケーラブル:Clojureは、JVM上で実行されるため、高速であり、スケーラビリティが高くなっています。Clojureは、軽量なスレッドモデルをサポートし、コンパイルされるため、高速な実行速度を実現することができます。
  8. REPL(Read-Eval-Print-Loop):Clojureは、強力なREPLを提供しており、開発中に即座にコードを試行し、テストすることができます

REPLの使い方

[編集]

ClojureのREPL(Read-Eval-Print Loop)は、プログラミング言語Clojureのインタラクティブな開発環境です。REPLを使用することで、Clojureの式を直接入力し、実行結果をリアルタイムで確認することができます。この節では、ClojureのREPLの使い方について説明します。

REPLの起動

[編集]

ClojureのREPLを起動するには、まずClojureの環境をインストールする必要があります。Clojureの環境は、LeiningenやBootなどのビルドツールを使用してインストールできます。Clojureの環境がインストールされたら、ターミナルウィンドウを開き、以下のコマンドを入力してREPLを起動します。

$ lein repl

REPLが起動すると、以下のようなプロンプトが表示されます。

user=>

これはClojureのREPLプロンプトで、ここでClojureの式を入力して実行することができます。

式の入力と実行

[編集]

ClojureのREPLで式を入力するには、プロンプトの後ろに式を入力し、Enterキーを押します。次に、Clojureは式を評価し、その結果をプロンプトの前に出力します。例えば、以下のような式を試してみましょう。

user=> (+ 1 2)
3

この式は、2つの整数1と2を加算するためにClojureの+関数を使用しています。Clojureはこの式を評価し、3を出力します。

REPLで式を試してみると、Clojureの構文や関数を直感的に理解することができます。また、コードのデバッグや実行結果の確認にも役立ちます。

履歴の操作

[編集]

ClojureのREPLは、直前に入力した式の履歴を保持しています。これにより、過去の式を再利用することができます。履歴を表示するには、以下のように入力します。

user=> (doc)

この式は、REPLの履歴を表示するためにClojureのdoc関数を使用しています。Clojureはこの式を評価し、直前に入力した式のリストを出力します。履歴から過去の式を再利用するには、Ctrl-Pキーを押して前の式を選択し、Enterキーを押します。

REPLの終了

[編集]

ClojureのREPLを終了するには、以下のように入力します。

user=> (exit)

Clojureの基本構文

[編集]

変数とデータ型

[編集]

Clojureでは、変数の宣言にdefという特別な関数を使用します。変数には、任意の値を割り当てることができます。

コード例
(def x 10)
上記の例では、xという変数に10という値を割り当てています。

Clojureには、いくつかのデータ型があります。以下は、Clojureで使用できるいくつかの主要なデータ型の一部です。

  • 数値:Clojureでは、整数と浮動小数点数の両方をサポートしています。
コード例
(def x 10)   ; 整数
(def y 3.14) ; 浮動小数点数
(def z 22/7) ; 有理数(分数)
  • 文字列:Clojureでは、二重引用符で囲まれた文字列を使用します。
コード例
(def name "John")
  • 真偽値:Clojureでは、真と偽を表すために、truefalseの2つのキーワードを使用します。
コード例
(def is_sunny true)
  • シンボル:Clojureでは、名前を表すためにシンボルを使用します。シンボルは、キーワードや変数のようなものですが、コードを記述するときにより明確な意味を持っています。
コード例
(def x 10)   ; 変数
(def my-keyword :hello) ; キーワード
(def my-symbol 'hello) ; シンボル

Clojureでは、基本的なデータ型だけでなく、コレクションも使用できます。以下は、Clojureで使用できるいくつかの主要なコレクションの一部です。

  • リスト:Clojureでは、リストを表すために、丸括弧で囲まれた要素のシーケンスを使用します。
コード例
(def my-list '(1 2 3))
  • ベクター:Clojureでは、ベクターを表すために、角括弧で囲まれた要素のシーケンスを使用します。
コード例
(def my-vector [1 2 3])
  • マップ:Clojureでは、マップを表すために、波括弧で囲まれたキーと値のペアのシーケンスを使用します。
コード例
(def my-map {:name "John" :age 30})

Clojureには、他にも多くのデータ型やコレクションがあります。これらのデータ型とコレクションを組み合わせることで、より複雑なデータ構造を作成することができます。

変数とデータ型のチートシート

[編集]
;; 変数の宣言
(def my-var 42)  ; 不変変数
(def ^:dynamic my-dynamic-var 100)  ; 動的変数

;; 変数の再代入
(set! my-var 24)

;; データ型
;; 数値
(def my-int 42)
(def my-float 3.14)
(def my-ratio 22/7)

;; 文字列
(def my-str "Hello, World!")
(def my-str2 (str "Hello, " "Clojure!"))

;; シーケンス
(def my-list '(1 2 3))
(def my-vec [4 5 6])
(def my-map {:a 1 :b 2 :c 3})

;; 関数
(defn my-func [x y]
  (+ x y))

;; 真偽値
(def my-true true)
(def my-false false)
Common LispとClojureの「変数とデータ型」の違い
Common LispとClojureの変数とデータ型の比較
変数宣言と束縛
Common Lisp
グローバル変数: defvardefparameterを使用
  (defvar *my-var* 42)    ; 既存の値を保持
  (defparameter *x* 100)  ; 常に新しい値で上書き
ローカル変数: letlet*を使用
  (let ((x 1) (y 2))
    (+ x y))
Clojure
イミュータブルな値の束縛: defを使用
  (def my-var 42)
ローカルな束縛: letを使用
  (let [x 1
        y 2]
    (+ x y))
変更可能性
Common Lisp
変数は基本的にミュータブル
値の変更にはsetfsetqを使用
  (setf x 10)
Clojure
デフォルトでイミュータブル
変更が必要な場合はatomrefなどの参照型を使用
  (def counter (atom 0))
  (swap! counter inc)
主要なデータ型
Common Lisp
数値型
  • 整数: fixnum, bignum
  • 浮動小数点: single-float, double-float
  • 有理数: ratio
文字列: 可変長
リスト: 連結リスト
配列: 固定長または可変長
ハッシュテーブル: キーと値のマッピング
Clojure
数値型
  • 整数: Long, BigInt
  • 浮動小数点: Double
  • 有理数: Ratio
文字列: Java.lang.String(イミュータブル)
リスト: 永続的データ構造
ベクター: 永続的データ構造
マップ: イミュータブルな連想配列
型システム
Common Lisp
動的型付け
オプショナルな型宣言が可能
CLOS(Common Lisp Object System)による強力なオブジェクト指向プログラミング
Clojure
動的型付け
Javaの型システムとの統合
任意の型ヒントが可能
プロトコルによる多相性
特徴的な違い
イミュータビリティ
  • Common Lisp: ミュータブルがデフォルト
  • Clojure: イミュータブルがデフォルト
名前空間
  • Common Lisp: パッケージシステム
  • Clojure: 名前空間システム
データ構造の実装
  • Common Lisp: 従来的なデータ構造
  • Clojure: 永続的データ構造
コード例:データ構造の操作
Common Lisp
;; リストの操作
(let ((lst '(1 2 3)))
  (setf (car lst) 10)    ; リストの先頭を変更
  lst)                   ; => (10 2 3)

;; ハッシュテーブル
(defvar *ht* (make-hash-table))
(setf (gethash 'key *ht*) 'value)
Clojure
;; イミュータブルなデータ構造
(let [lst '(1 2 3)
      new-lst (cons 10 (rest lst))]
  [lst new-lst])  ; => [(1 2 3) (10 2 3)]

;; マップ
(def m {:key "value"})
(assoc m :new-key "new-value")

関数定義と呼び出し

[編集]

関数

[編集]

Clojureの関数は、defnを使って定義することができます。関数の引数は、ベクターで表されます。関数内では、defを使って新しい変数を定義することができます。

コード例
(defn add [a b]
  (+ a b))

(println (add 1 2))
実行結果
3
common-lispとClojureの「関数定義と呼び出し」の違い
Common LispとClojureの関数定義と呼び出しの比較
基本的な関数定義
Common Lisp
;; defunを使用した関数定義
(defun add-numbers (a b)
  (+ a b))

;; 呼び出し
(add-numbers 3 4)  ; => 7

;; オプショナル引数
(defun greet (name &optional (greeting "Hello"))
  (format nil "~A, ~A!" greeting name))

(greet "Alice")        ; => "Hello, Alice!"
(greet "Bob" "Hi")     ; => "Hi, Bob!"

;; キーワード引数
(defun make-person (&key name (age 0) (city "Unknown"))
  (list :name name :age age :city city))

(make-person :name "Charlie" :age 25)
; => (:NAME "Charlie" :AGE 25 :CITY "Unknown")
Clojure
;; defnを使用した関数定義
(defn add-numbers [a b]
  (+ a b))

;; 呼び出し
(add-numbers 3 4)  ; => 7

;; オプショナル引数(多重アリティ)
(defn greet
  ([name] (greet name "Hello"))
  ([name greeting] (str greeting ", " name "!")))

(greet "Alice")        ; => "Hello, Alice!"
(greet "Bob" "Hi")     ; => "Hi, Bob!"

;; マップ解構築による名前付き引数
(defn make-person [{:keys [name age city]
                    :or {age 0 city "Unknown"}}]
  {:name name :age age :city city})

(make-person {:name "Charlie" :age 25})
; => {:name "Charlie" :age 25 :city "Unknown"}
無名関数
Common Lisp
;; lambdaを使用
(mapcar #'(lambda (x) (* x x)) '(1 2 3 4))
; => (1 4 9 16)

;; 短縮形(#'関数名 はFunction指定子)
(mapcar #'1+ '(1 2 3))  ; => (2 3 4)
Clojure
;; fnを使用
(map (fn [x] (* x x)) [1 2 3 4])
; => (1 4 9 16)

;; 短縮形
(map #(* % %) [1 2 3 4])
; => (1 4 9 16)
クロージャと環境
Common Lisp
(defun make-counter ()
  (let ((count 0))
    #'(lambda () (incf count))))

(defvar counter (make-counter))
(funcall counter)  ; => 1
(funcall counter)  ; => 2
Clojure
(defn make-counter []
  (let [count (atom 0)]
    #(swap! count inc)))

(def counter (make-counter))
(counter)  ; => 1
(counter)  ; => 2
マルチメソッドとディスパッチ
Common Lisp (CLOS)
(defgeneric describe-shape (shape)
  (:documentation "Shape description"))

(defclass shape () ())
(defclass circle (shape)
  ((radius :initarg :radius :reader radius)))
(defclass rectangle (shape)
  ((width :initarg :width :reader width)
   (height :initarg :height :reader height)))

(defmethod describe-shape ((shape circle))
  (format nil "Circle with radius ~A" (radius shape)))

(defmethod describe-shape ((shape rectangle))
  (format nil "Rectangle ~Ax~A" (width shape) (height shape)))
Clojure (Multimethods)
(defmulti describe-shape :type)

(defmethod describe-shape :circle [shape]
  (str "Circle with radius " (:radius shape)))

(defmethod describe-shape :rectangle [shape]
  (str "Rectangle " (:width shape) "x" (:height shape)))

(describe-shape {:type :circle :radius 5})
; => "Circle with radius 5"
主な違いのまとめ
引数処理
  • Common Lisp: &optional, &key, &restなどの引数指定子
  • Clojure: 多重アリティ、マップ解構築
無名関数の構文
  • Common Lisp: #'(lambda (x) ...)
  • Clojure: 短縮形 #(...)が一般的
多重ディスパッチ
  • Common Lisp: CLOSによる包括的なオブジェクトシステム
  • Clojure: multimethodsとプロトコル
クロージャの実装
  • Common Lisp: 可変な状態を直接扱える
  • Clojure: atomなどの参照型を使用
実践的な使用例
Common Lisp
;; 高階関数
(defun compose (f g)
  #'(lambda (x) (funcall f (funcall g x))))

(defvar double-and-inc (compose #'1+ #'(lambda (x) (* x 2))))
(funcall double-and-inc 3)  ; => 7

;; メモ化
(defun make-memoized (fn)
  (let ((cache (make-hash-table :test #'equal)))
    #'(lambda (&rest args)
        (multiple-value-bind (result exists)
            (gethash args cache)
          (if exists
              result
              (setf (gethash args cache)
                    (apply fn args)))))))
Clojure
;; 高階関数
(defn compose [f g]
  #(f (g %)))

(def double-and-inc (compose inc #(* % 2)))
(double-and-inc 3)  ; => 7

;; メモ化
(defn memoize [f]
  (let [cache (atom {})]
    (fn [& args]
      (if-let [e (find @cache args)]
        (val e)
        (let [ret (apply f args)]
          (swap! cache assoc args ret)
          ret)))))

制御構文

[編集]

Clojureは関数型プログラミング言語であり、制御構文は他の言語とは異なります。 Clojureには、ifwhencondなどの条件分岐があり、loop/recurをつかったループがあります。

条件分岐

[編集]

Clojureでは、ifcondという2つの条件分岐構文があります。

if

[編集]

ifは、以下の形式で使用できます。

コード例
(if test then else)

testがtrueの場合はthenが評価され、そうでない場合はelseが評価されます。

たとえば、以下のコードでは、xが正の場合にはpositive、0の場合にはzero、負の場合にはnegativeという文字列を返します。

コード例
(defn check-sign [x]
  (if (> x 0)
    "positive"
    (if (= x 0)
      "zero"
      "negative")))

cond

[編集]

condは、複数の条件をチェックし、最初に真となった条件に対応する式を評価する構文です。

コード例
(cond
  test1 expr1
  test2 expr2
  ...
  :else exprn)

test1がtrueの場合はexpr1が評価され、そうでなくてもtest2がtrueの場合はexpr2が評価され、以降、真となる最初の条件に対応する式が評価されます。条件が全て偽である場合は、:elseに対応する式exprnが評価されます。

たとえば、以下のコードは、xが正の場合にはpositive、0の場合にはzero、負の場合にはnegativeという文字列を返します。

コード例
(defn check-sign [x]
  (cond
    (> x 0) "positive"
    (= x 0) "zero"
    :else "negative"))

ループ

[編集]

Clojureには、looprecurを使用して、繰り返し処理を行うことができます。

loop/recur

[編集]

looprecurは、以下のように使用できます。

コード例
(loop [bindings]
  (if test
    (recur new-bindings)
    expr))

loopは、bindingsという名前のマップを受け取り、繰り返し処理を行います。testがtrueである場合、recurは新しいbindingsを受け取ってループを再開します。testがfalseである場合、exprが評価され、ループが終了します。

コード例
(defn sum-to-n [n]
  (loop [i 1
         sum 0]
    (if (> i n)
      sum
      (recur (inc i) (+ sum i)))))

(sum-to-n 10) ;=> 55

この例では、1 から n までの整数の合計を計算する関数 sum-to-n を定義しています。loop フォームを使って、変数 isum を初期化し、繰り返し処理を開始します。if フォームを使って、現在の値 i が n を超えたら、合計 sum を返します。そうでなければ、recur フォームを使って、i をインクリメントし、sumi を加えて、繰り返し処理を続けます。

looprecur を使うことで、関数の呼び出しスタックが増えないため、再帰を使う場合よりも効率的にコードを書くことができます。しかし、注意が必要な点もあります。recur フォームは、ループの先頭に戻るために使用されるため、ループの中でのみ使用できます。また、recur の引数は、ループ変数に対応するものでなければなりません。これにより、ループ変数が変更され、新しいループが開始されます。

common-lispとClojureの「制御構造」の違い
Common LispとClojureの制御構造の比較
条件分岐
Common Lisp
;; if(二股分岐)
(if (> x 0)
    "正"
    "負かゼロ")

;; when(条件が真のときのみ実行)
(when (> x 0)
  (print "xは正です")
  x)

;; unless(条件が偽のときのみ実行)
(unless (> x 0)
  (print "xは負かゼロです")
  x)

;; cond(多岐分岐)
(cond ((< x 0) "負")
      ((> x 0) "正")
      (t "ゼロ"))

;; case(値による分岐)
(case x
  ((1) "一")
  ((2) "二")
  (otherwise "その他"))
Clojure
;; if(二股分岐)
(if (> x 0)
  "正"
  "負かゼロ")

;; when(条件が真のときのみ実行)
(when (> x 0)
  (println "xは正です")
  x)

;; if-not/when-not(条件が偽のときのみ実行)
(when-not (> x 0)
  (println "xは負かゼロです")
  x)

;; cond(多岐分岐)
(cond
  (< x 0) "負"
  (> x 0) "正"
  :else "ゼロ")

;; case(値による分岐)
(case x
  1 "一"
  2 "二"
  "その他")

;; condp(述語による分岐)
(condp = x
  1 "一"
  2 "二"
  "その他")
繰り返し構造
Common Lisp
;; dolist(リストの走査)
(dolist (item '(1 2 3))
  (print item))

;; dotimes(回数指定の繰り返し)
(dotimes (i 3)
  (print i))

;; loop(汎用繰り返し)
(loop for i from 0 to 9
      collect (* i i))

;; do(一般的な繰り返し)
(do ((i 0 (1+ i))
     (sum 0 (+ sum i)))
    ((> i 10) sum)
  (print i))

;; mapcar(リストの変換)
(mapcar #'(lambda (x) (* x x))
        '(1 2 3 4))
Clojure
;; doseq(シーケンスの走査)
(doseq [item [1 2 3]]
  (println item))

;; dotimes(回数指定の繰り返し)
(dotimes [i 3]
  (println i))

;; for(リスト内包表記)
(for [i (range 10)]
  (* i i))

;; loop/recur(再帰的な繰り返し)
(loop [i 0
       sum 0]
  (if (> i 10)
    sum
    (do
      (println i)
      (recur (inc i) (+ sum i)))))

;; map(シーケンスの変換)
(map #(* % %) [1 2 3 4])
例外処理
Common Lisp
;; 基本的な例外処理
(handler-case
    (/ 1 0)
  (division-by-zero () "ゼロ除算エラー")
  (error () "その他のエラー"))

;; unwind-protect(finallyに相当)
(unwind-protect
     (progn (print "処理")
            (error "エラー発生"))
  (print "クリーンアップ"))

;; 条件システム
(handler-bind ((error #'(lambda (c)
                          (when (y-or-n-p "続行しますか?")
                            (continue c)))))
  (error "エラーが発生しました"))
Clojure
;; try/catch
(try
  (/ 1 0)
  (catch ArithmeticException e "ゼロ除算エラー")
  (catch Exception e "その他のエラー")
  (finally (println "クリーンアップ")))

;; throw
(try
  (throw (Exception. "エラーメッセージ"))
  (catch Exception e
    (.getMessage e)))
特殊なフロー制御
Common Lisp
;; block/return-from(名前付きブロックからの脱出)
(block my-block
  (dotimes (i 10)
    (when (= i 5)
      (return-from my-block i)))
  100)

;; tagbody/go(低レベルなジャンプ)
(tagbody
  (go point-b)
 point-a
  (print 'a)
  (go point-c)
 point-b
  (print 'b)
  (go point-a)
 point-c)
Clojure
;; 早期リターン(ループからの脱出)
(for [i (range 10)
      :while (< i 5)]
  (* i i))

;; トランスデューサー(データ変換のパイプライン)
(def xform
  (comp
    (filter even?)
    (map #(* % %))
    (take 5)))

(transduce xform conj (range 10))
マクロを使用した制御構造の拡張
Common Lisp
;; 独自の制御構造を定義
(defmacro while (test &body body)
  `(do ()
       ((not ,test))
     ,@body))

;; 使用例
(let ((i 0))
  (while (< i 3)
    (print i)
    (incf i)))
Clojure
;; マクロによる制御構造の拡張
(defmacro while [test & body]
  `(loop []
     (when ~test
       ~@body
       (recur))))

;; 使用例
(let [i (atom 0)]
  (while (< @i 3)
    (println @i)
    (swap! i inc)))
主な違いのまとめ
構文スタイル
  • Common Lisp: より冗長で明示的
  • Clojure: より簡潔で現代的
イミュータビリティの影響
  • Common Lisp: 可変状態を前提とした制御構造
  • Clojure: イミュータブルデータ構造に適した制御構造
例外処理
  • Common Lisp: 強力な条件システム
  • Clojure: Javaライクな例外処理
特殊なフロー制御
  • Common Lisp: より低レベルな制御が可能
  • Clojure: 関数型プログラミングに適した高レベルな抽象化

コレクション

[編集]

Clojureは、多くのプログラミング言語と同様に、様々な種類のコレクションをサポートしています。この章では、Clojureで使用できる主要なコレクションであるリスト、ベクター、マップ、およびセットについて説明します。

Clojureにおける不変と可変
Clojureにおける不変性と可変性
不変(イミュータブル)データ構造
基本的なデータ構造
;; ベクター
(def numbers [1 2 3 4 5])

;; リスト
(def names '("Alice" "Bob" "Charlie"))

;; マップ
(def person {:name "David" :age 30})

;; セット
(def unique-nums #{1 2 3 4 5})
イミュータブルな操作
;; 新しいベクターを作成(元のベクターは変更されない)
(def numbers [1 2 3 4 5])
(def new-numbers (conj numbers 6))

(println numbers)      ; [1 2 3 4 5]
(println new-numbers)  ; [1 2 3 4 5 6]

;; マップの更新
(def person {:name "Alice" :age 25})
(def updated-person (assoc person :age 26))

(println person)         ; {:name "Alice" :age 25}
(println updated-person) ; {:name "Alice" :age 26}
パフォーマンスと実装
;; 構造共有による効率的な実装
(def large-vector (vec (range 1000000)))
(def updated-vector (assoc large-vector 999999 :new-value))

;; 上記の操作は高速で、メモリ効率も良い
;; なぜなら、ほとんどのデータが2つのベクター間で共有されるため
可変(ミュータブル)な参照型
atom
;; アトムの作成と使用
(def counter (atom 0))

;; 値の取得
@counter  ; => 0

;; 値の更新
(swap! counter inc)
@counter  ; => 1

;; 条件付き更新
(compare-and-set! counter 1 2)  ; => true
@counter  ; => 2

;; 監視関数の追加
(add-watch counter :watcher
  (fn [key atom old-state new-state]
    (println "Counter changed from" old-state "to" new-state)))
ref(ソフトウェアトランザクショナルメモリ)
;; refの作成
(def account1 (ref 1000))
(def account2 (ref 500))

;; トランザクション内での更新
(dosync
  (alter account1 - 100)
  (alter account2 + 100))

@account1  ; => 900
@account2  ; => 600

;; refの結合
(def combined-balance (ref 0))
(add-watch combined-balance :total
  (fn [_ _ _ new-value]
    (println "Total balance:" new-value)))

(defn update-total []
  (dosync
    (ref-set combined-balance (+ @account1 @account2))))
agent(非同期更新)
;; エージェントの作成
(def delayed-counter (agent 0))

;; 非同期更新
(send delayed-counter inc)
(send delayed-counter inc)

;; 値の取得(更新が反映されるまで待つ必要がある)
(Thread/sleep 100)  ; 実際のコードでは避けるべき
@delayed-counter    ; => 2

;; エラーハンドリング
(set-error-handler! delayed-counter
  (fn [agent exception]
    (println "Error in agent:" (.getMessage exception))))

;; エージェントの状態監視
(set-error-mode! delayed-counter :continue)
(add-watch delayed-counter :watcher
  (fn [key agent old-state new-state]
    (println "Agent state changed from" old-state "to" new-state)))
実践的なパターン
イミュータブルなデータと可変な参照の組み合わせ
;; イミュータブルなデータ構造を持つアトム
(def app-state (atom {:users {}
                      :settings {:theme :light}}))

;; ネストされたデータの更新
(swap! app-state update-in [:settings :theme] {:theme :dark})

;; 複数の更新を一度に行う
(swap! app-state
       (fn [state]
         (-> state
             (assoc-in [:users "user1"] {:name "Alice"})
             (update-in [:settings :version] inc))))
キャッシュの実装
(defn create-memoized-function [f]
  (let [cache (atom {})]
    (fn [& args]
      (if-let [e (find @cache args)]
        (val e)
        (let [ret (apply f args)]
          (swap! cache assoc args ret)
          ret)))))

(def slow-calculation
  (create-memoized-function
    (fn [x]
      (Thread/sleep 1000)  ; 遅い計算のシミュレーション
      (* x x))))
ベストプラクティス
デフォルトは不変
  • 可能な限りイミュータブルなデータ構造を使用
  • 変更が必要な場合のみ、適切な参照型を選択
参照型の選択基準
  • atom: 単純な可変状態
  • ref: 複数の値を調整して更新する必要がある場合
  • agent: 非同期の更新が必要な場合
パフォーマンスの考慮
;; 大量のデータを扱う場合の例
(defn process-large-data [data]
  (let [result (atom [])]
    (doseq [chunk (partition-all 1000 data)]
      (swap! result into (map process-item chunk)))
    @result))
副作用の隔離
;; 純粋な関数と副作用を分離
(defn pure-calculation [data]
  (map #(* % %) data))

(defn side-effect-wrapper [data]
  (let [result (atom [])]
    (doseq [x (pure-calculation data)]
      (swap! result conj x)
      (println "Processed:" x))
    @result))
一般的な落とし穴と解決策
過剰な可変性
;; 悪い例
(def state (atom {}))
(defn update-state [key value]
  (swap! state assoc key value))

;; 良い例
(defn update-state [current-state key value]
  (assoc current-state key value))
不適切なトランザクション
;; 悪い例
(def a (ref 0))
(def b (ref 0))
(future (dosync (alter a inc)) (ref-set b @a))

;; 良い例
(future
  (dosync
    (let [new-a (alter a inc)]
      (ref-set b new-a))))
不必要な同期
;; 悪い例
(def slow-counter (atom 0))
(defn slow-inc []
  (Thread/sleep 1000)
  (swap! slow-counter inc))

;; 良い例
(def fast-counter (agent 0))
(defn fast-inc []
  (send fast-counter
        (fn [n]
          (Thread/sleep 1000)
          (inc n))))

リスト

[編集]

リストは、Clojureで最も基本的なコレクションの一つです。リストは、要素を保持する単方向の連結リストとして実装されています。リストは、(list ...)関数を使用して作成できます。

コード例
;; リストを作成する方法
(def my-list (list 1 2 3))
;; => (1 2 3)

リストは、要素を追加することができますが、そのためには常に新しいリストを作成する必要があります。そのため、リストは可変ではなく、不変です。

コード例
;; リストを作成する方法
;; リストに要素を追加する方法
(def new-list (conj my-list 4))
;; => (4 1 2 3)

ベクター

[編集]

ベクターは、リストと同様に、要素のコレクションを保持することができます。ただし、ベクターは、要素のインデックスを使用してアクセスできるように、より高速に実装されています。ベクターは、[...]構文を使用して作成できます。

コード例
;; ベクターを作成する方法
(def my-vector [1 2 3])
;; => [1 2 3]

ベクターは、リストと同様に、要素を追加することができますが、そのためには常に新しいベクターを作成する必要があります。そのため、ベクターは可変ではなく、不変です。

コード例
;; ベクターに要素を追加する方法
(def new-vector (conj my-vector 4))
;; => [1 2 3 4]

マップ

[編集]

マップは、キーと値のペアを保持することができます。マップは、{...}構文を使用して作成できます。

コード例
;; マップを作成する方法
(def my-map {:name "Alice", :age 30})
;; => {:name "Alice", :age 30}

マップに新しい要素を追加する場合、assoc関数を使用します。

コード例
;; マップに新しい要素を追加する方法
(def new-map (assoc my-map :address "Tokyo"))
;; => {:name "Alice", :age 30, :address "Tokyo"}


セット

[編集]

Clojureのセットは、#{}構文を使用して作成します。要素は空白で区切ります。

上記のコードでは、my-setという名前のセットを作成しています。要素は1、2、3、4で構成されています。

コード例
(def my-set #{1 2 3 4})

もう一つの方法は、hash-set関数を使用してセットを作成することです。

コード例
(def my-set (hash-set 1 2 3 4))
my-setという名前のセットを作成しています。要素は1、2、3、4で構成されています。

セットの操作

[編集]

Clojureのセットは、通常の数学のセットと同じように操作できます。以下は、Clojureでセットを操作するためのいくつかの基本的な関数です。

  • conj: セットに要素を追加する。
コード例
(conj my-set 5)
;=> #{1 2 3 4 5}
conj関数を使用して、my-setセットに5を追加しています。
  • disj: セットから要素を削除する。
コード例
(disj my-set 4)
;=> #{1 2 3}
disj関数を使用して、my-setセットから4を削除しています。
  • intersection: セットの共通要素を取得する。
コード例
(intersection #{1 2 3 4} #{3 4 5 6})
;=> #{3 4}
上記のコードでは、intersection関数を使用して、2つのセットの共通要素を取得しています。
  • difference: セットの差分を取得する。
コード例
(difference #{1 2 3 4} #{3 4 5 6})
;=> #{1 2}
difference関数を使用して、2つのセットの差分を取得しています。
  • union: 2つのセットを結合する。
コード例
(union #{1 2 3 4} #{3 4 5 6})
;=> #{1 2 3 4 5 6}
union関数を使用して、2つのセットを結合しています。


コレクションのチートシート

[編集]
;; リスト
;; 空のリストを作成
(def empty-list (list))

;; 要素を持つリストを作成
(def list-with-elements (list 1 2 3))

;; リストの要素を先頭から順に取得
(first list-with-elements)

;; リストの先頭を除いた残りの要素を取得
(rest list-with-elements)

;; リストの最後の要素を取得
(last list-with-elements)

;; ベクター
;; 空のベクターを作成
(def empty-vector (vector))

;; 要素を持つベクターを作成
(def vector-with-elements (vector 1 2 3))

;; ベクターの要素をインデックスを指定して取得
(nth vector-with-elements 0)

;; ベクターの要素を末尾に追加して新しいベクターを作成
(conj vector-with-elements 4)

;; マップ
;; 空のマップを作成
(def empty-map (hash-map))

;; キーと値を指定してマップを作成
(def map-with-entries (hash-map :a 1 :b 2 :c 3))

;; マップの値をキーを指定して取得
(get map-with-entries :a)

;; マップにキーと値を追加して新しいマップを作成
(assoc map-with-entries :d 4)

;; セット
;; 空のセットを作成
(def empty-set (hash-set))

;; 要素を持つセットを作成
(def set-with-elements (hash-set 1 2 3))

;; セットに要素を追加して新しいセットを作成
(conj set-with-elements 4)

;; セットの要素を取得
(get set-with-elements 2)
Common LispとClojureの「コレクション」の違い
Common LispとClojureのコレクションの比較
基本的なコレクション型
Common Lisp
;; リスト
(defvar *list-example* '(1 2 3 4 5))

;; ベクター(配列)
(defvar *vector-example* #(1 2 3 4 5))

;; ハッシュテーブル
(defvar *hash-example* (make-hash-table))
(setf (gethash 'key *hash-example*) 'value)

;; 集合(Common Lispには組み込みの集合型はない)
Clojure
;; リスト
(def list-example '(1 2 3 4 5))

;; ベクター
(def vector-example [1 2 3 4 5])

;; マップ
(def map-example {:key "value" :another-key "another-value"})

;; セット
(def set-example #{1 2 3 4 5})
コレクションの特徴
Common Lisp
;; リストは連結リスト
(defvar *list* '(1 2 3))
(cons 0 *list*)        ; => (0 1 2 3)
(car *list*)           ; => 1 (先頭要素の取得)
(cdr *list*)           ; => (2 3) (残りのリスト)

;; ベクターはランダムアクセス可能
(defvar *vec* #(1 2 3 4 5))
(aref *vec* 2)         ; => 3 (インデックスによるアクセス)
(setf (aref *vec* 2) 10) ; ベクターの要素を変更

;; ハッシュテーブルは可変
(defvar *hash* (make-hash-table))
(setf (gethash 'key *hash*) 'value)
(gethash 'key *hash*)  ; => VALUE, T
Clojure
;; すべてのコレクションはイミュータブル
;; リストは連結リスト
(def lst '(1 2 3))
(cons 0 lst)           ; => (0 1 2 3)
(first lst)            ; => 1
(rest lst)             ; => (2 3)

;; ベクターは永続的データ構造
(def vec [1 2 3 4 5])
(get vec 2)            ; => 3
(assoc vec 2 10)       ; => [1 2 10 4 5] (新しいベクターを返す)

;; マップも永続的データ構造
(def map-example {:a 1 :b 2})
(assoc map-example :c 3) ; => {:a 1 :b 2 :c 3}
(get map-example :a)     ; => 1
コレクションの操作
Common Lisp
;; リストの操作
(defvar *list* '(1 2 3 4 5))
(length *list*)        ; => 5
(nth 2 *list*)         ; => 3
(member 3 *list*)      ; => (3 4 5)

;; シーケンス関数(リストとベクターで共通)
(find 3 *list*)        ; => 3
(count 3 *list*)       ; => 1
(position 3 *list*)    ; => 2

;; マッピング
(mapcar #'1+ *list*)   ; => (2 3 4 5 6)

;; 畳み込み
(reduce #'+ *list*)    ; => 15

;; ハッシュテーブルの操作
(maphash #'(lambda (k v)
             (format t "~A => ~A~%" k v))
         *hash-example*)
Clojure
;; シーケンス抽象化(すべてのコレクションで共通)
(def numbers [1 2 3 4 5])

;; マッピング
(map inc numbers)      ; => (2 3 4 5 6)

;; フィルタリング
(filter odd? numbers)  ; => (1 3 5)

;; 畳み込み
(reduce + numbers)     ; => 15

;; コンプリヘンション
(for [x numbers :when (odd? x)]
  (* x x))             ; => (1 9 25)

;; トランスデューサー
(def xform
  (comp
    (filter odd?)
    (map #(* % %))))
    
(transduce xform conj numbers) ; => [1 9 25]
パフォーマンスの特徴
Common Lisp
;; リストは先頭への追加が高速
(defun add-to-list (n)
  (let ((lst nil))
    (dotimes (i n)
      (push i lst))
    lst))

;; ベクターは要素アクセスが高速
(defun vector-access (vec n)
  (dotimes (i n)
    (aref vec (mod i (length vec)))))

;; ハッシュテーブルはキーによるアクセスが高速
(defun hash-access (hash n)
  (dotimes (i n)
    (gethash i hash)))
Clojure
;; ベクターは末尾への追加が高速
(defn add-to-vector [n]
  (loop [i 0 v []]
    (if (< i n)
      (recur (inc i) (conj v i))
      v)))

;; 永続的データ構造による効率的な更新
(defn update-vector [v]
  (assoc v 1000 :new-value))

;; マップのネストされた更新も効率的
(def deep-map {:a {:b {:c 1}}})
(update-in deep-map [:a :b :c] inc) ; 効率的
イディオマティックな使用例
Common Lisp
;; パターンマッチング(オプショナル)
(defpackage :optima-example (:use :cl :optima))
(in-package :optima-example)

(defun process-data (data)
  (match data
    ((list 1 2 3) "Found 1 2 3")
    ((vector 4 5 6) "Found vector 4 5 6")
    ((hash-table ('key value)) (format nil "Found value: ~A" value))
    (_ "No match")))

;; CLOS with collections
(defclass user ()
  ((name :initarg :name :accessor user-name)
   (friends :initform nil :accessor user-friends)))

(defmethod add-friend ((user user) friend)
  (push friend (user-friends user)))
Clojure
;; データ指向プログラミング
(defprotocol Processable
  (process [this]))

(defrecord User [name friends]
  Processable
  (process [this]
    (str "Processing user: " name)))

(defn process-data [data]
  (condp = (type data)
    clojure.lang.PersistentVector  (str "Vector: " (count data))
    clojure.lang.PersistentHashMap (str "Map: " (keys data))
    clojure.lang.PersistentList    (str "List: " (first data))))

;; スレッディングマクロの使用
(-> {:user "Alice"
     :friends ["Bob" "Charlie"]}
    (update :friends conj "David")
    (assoc :status :active))
主な違いのまとめ
イミュータビリティ
  • Common Lisp: デフォルトで可変
  • Clojure: デフォルトで不変
データ構造の種類
  • Common Lisp: 主にリスト、配列、ハッシュテーブル
  • Clojure: リスト、ベクター、マップ、セット(すべて永続的)
抽象化レベル
  • Common Lisp: より低レベルな操作が可能
  • Clojure: 高レベルな抽象化(シーケンス、トランスデューサーなど)
パフォーマンス最適化
  • Common Lisp: 可変性を活用した最適化
  • Clojure: 構造共有による効率的な永続的データ構造
ベストプラクティス
Common Lisp
;; 適切なデータ構造の選択
(defun process-large-data (data)
  (let ((result (make-array (length data) :adjustable t :fill-pointer 0)))
    (loop for item across data
          when (valid-p item)
          do (vector-push-extend item result))
    result))

;; キャッシュの実装
(defvar *cache* (make-hash-table :test #'equal))
(defun cached-computation (key)
  (or (gethash key *cache*)
      (setf (gethash key *cache*)
            (expensive-computation key))))
Clojure
;; イミュータブルデータの効率的な操作
(defn process-large-data [data]
  (->> data
       (filter valid?)
       (into [])))

;; メモ化による最適化
(def cached-computation
  (memoize
    (fn [key]
      (expensive-computation key))))

関数型プログラミングの基本的な考え方

[編集]

Clojureの関数型プログラミングでは、以下のような考え方があります。

  • 関数はデータの変換を行う
  • 変更可能な状態を持たない(純粋な)関数を使う
  • 関数を合成することで、複雑な処理を構築する

この考え方に基づいて、Clojureでは、数学的な関数のように、入力値に対して決定論的な値を返す純粋関数を中心にプログラミングを行います。

また、Clojureでは、関数をデータと同様に扱うことができるため、関数自体も変数に代入したり、関数を引数として渡したり、関数を戻り値として返したりすることができます。これにより、関数を合成したり、より高次の関数を作成したりすることができます。

純粋関数の例

[編集]

以下は、2つの整数の和を返す純粋関数の例です。

コード例
(defn add [a b]
  (+ a b))
この関数は、入力のabを足し合わせて、その結果を返します。この関数は純粋であるため、同じ入力に対して常に同じ結果を返します。

関数の合成の例

[編集]

以下は、2つの純粋関数を合成して新しい関数を作成する例です。

コード例
(defn add-one [n]
  (+ n 1))

(defn square [n]
  (* n n))

(def add-one-and-square (comp square add-one))
add-one関数は、入力の整数に1を足した値を返します。
square関数は、入力の整数を2乗した値を返します。
add-one-and-square関数は、add-one関数とsquare関数を合成して作成された関数で、入力の整数に1を足し、その結果を2乗した値を返します。
コード例
(add-one-and-square 3) ; 16

カリー化の例

[編集]

以下は、カリー化を使って関数を作成する例です。

コード例
(defn add-two [a b]
  (+ a b 2))

(def add-two-curried (partial add-two 5))

(add-two-curried 3) ; 10
add-two関数は、2つの整数に2を加えた値を返します。
add-two-curried関数は、partial関数を使って、add-two関数を部分適用します。
partial関数は、ある関数を部分的に適用した新しい関数を返すための便利な関数で、第1引数に適用する関数、第2引数以降に適用する引数を指定します。この場合、add-two関数には第1引数に5を渡し、残りの引数を部分適用したadd-two-curried関数を生成しています。
add-two-curried関数に引数3を渡して呼び出し、結果として10が返されます。add-two-curried関数は、add-two関数に最初の引数として5を適用しているため、実際には引数3を受け取って、(+ 5 3 2)という式を評価して10を返します。

Javaとの統合

[編集]

ClojureはJavaプラットフォーム上で動作するLisp方言であり、Javaとのシームレスな統合が可能です。この節では、ClojureとJavaの統合について、次のようなトピックを取り上げます。

  • Javaのクラスの使用

ClojureはJavaのクラスを直接使用することができます。Javaのクラスは、Clojureの名前空間内でimportされる必要があります。Clojureの名前空間内でJavaのクラスを使用するには、Javaのクラス名を修飾された形式で記述します。例えば、以下のようなClojureのコードで、Javaのjava.util.Dateクラスを使用して現在の日時を取得することができます。

コード例
(import 'java.util.Date)
(def today (Date.))
  • Javaのメソッドの呼び出し

ClojureはJavaのメソッドを呼び出すことができます。Javaのメソッドは、Clojureの関数のように呼び出すことができます。ただし、Javaのメソッド名はClojureの関数名として使用できないため、Javaのメソッド名を修飾された形式で記述する必要があります。例えば、以下のようなClojureのコードで、JavaのMathクラスのabsメソッドを使用して、-1の絶対値を求めることができます。

コード例
(def x -1)
(Math/abs x)
  • Javaのインタフェースの実装

ClojureはJavaのインタフェースを実装することができます。Javaのインタフェースを実装するには、ClojureのオブジェクトにJavaのインタフェースを実装するよう指定する必要があります。例えば、以下のようなClojureのコードで、JavaのRunnableインタフェースを実装するオブジェクトを作成することができます。

コード例
(defn my-run []
  (println "Hello from Clojure"))
(def my-runnable (proxy [Runnable] [] (run [] (my-run))))
(.start (Thread. my-runnable))
  • Javaのクラスの拡張

JavaのクラスをClojureから利用する方法の一つは、gen-classマクロを使ってJavaのクラスを継承することです。gen-classマクロは、Javaのクラスを継承するClojureのクラスを生成し、それをClojureから利用できるようにします。以下は、gen-classマクロを使ってJavaのクラスを継承するClojureのクラスを生成する例です。

コード例
(ns myapp.core
  (:gen-class
    :extends java.util.ArrayList))

(defn -main []
  (let [list (myapp.core.)]
    (.add list "foo")
    (.add list "bar")
    (.add list "baz")
    (doseq [item list]
      (println item))))
myapp.coreクラスには、-mainメソッドが含まれており、それはClojureアプリケーションのエントリーポイントとなります。
-mainメソッドでは、myapp.coreクラスからオブジェクトを生成し、java.util.ArrayListのメソッドを呼び出しています。

Javaのクラスを継承するClojureのクラスを定義する場合、以下のような注意点があります。

  • Clojureのクラス名には、Javaのクラス名と同じ名前を使用することができます。
  • Clojureのクラス名は、ハイフンで始まることができます。
  • Clojureのクラス名には、単一のドット(.)を含むことができます。ただし、これはJavaのパッケージとは無関係であることに注意してください。
  • Javaのメソッド名とClojureの関数名は異なる構文を持ちます。Clojureの関数名は、ハイフンで区切られた単語で表され、Javaのメソッド名はキャメルケースで表されます。
  • -mainメソッドは、必ずしも必要ではありませんが、Clojureアプリケーションのエントリーポイントとして慣習的に使用されます。

マクロ

[編集]

マクロはClojureのコードを変換する強力なメタプログラミング機能です。マクロを使用することで、言語自体を拡張し、新しい制御構造や特殊形式を作成できます。マクロはコンパイル時に実行され、生成されたコードはプログラムの一部として組み込まれます。

マクロの概要

[編集]

マクロはコードをデータとして扱い、そのデータを操作して新しいコードを生成します。ClojureのマクロシステムはS式に基づいており、コードとデータの境界を曖昧にすることで強力な抽象化を可能にします。

マクロの主な用途:

  • 新しい制御構造の作成
  • ボイラープレートコードの削減
  • ドメイン特化言語(DSL)の実装
  • 既存の構文の拡張
例:
;; when マクロの簡略化された実装
(defmacro my-when [test & body]
  `(if ~test
     (do ~@body)))

;; 使用例
(my-when (= 1 1)
  (println "True!")
  (println "Still true!"))

マクロの定義と呼び出し

[編集]

マクロはdefmacroを使用して定義します。マクロは通常の関数と同様に引数を取りますが、その引数は評価されずにマクロ展開時にそのままマクロに渡されます。

マクロ定義の主な要素:

  • シンタックスクォート(`):テンプレートコードの作成
  • アンクォート(~):テンプレート内での値の評価
  • スプライシングアンクォート(~@):シーケンスの展開
例:
(defmacro unless [test & body]
  `(when (not ~test)
     ~@body))

;; 使用例
(unless false
  (println "This will be printed"))

;; マクロ展開の確認
(macroexpand '(unless false
                (println "This will be printed")))
;; 展開結果:
;; (when (clojure.core/not false)
;;   (println "This will be printed"))

マクロ定義のベストプラクティス:

  • マクロは必要な場合のみ使用する(通常の関数で解決できない場合)
  • マクロの中では可能な限り関数を使用する
  • シンボルの衝突を避けるためにジェネレーティッドシンボルを使用する

マクロ展開の仕組み

[編集]

マクロ展開は、マクロ呼び出しを実際のClojureコードに変換するプロセスです。このプロセスはコンパイル時に行われます。

マクロ展開の段階:

  1. マクロ呼び出しの検出
  2. マクロ関数の実行
  3. 生成されたコードの評価

マクロ展開のデバッグツール:

  • macroexpand:一段階のマクロ展開
  • macroexpand-1:完全なマクロ展開
例:
;; -> スレッディングマクロの展開
(macroexpand '(-> 5
                  (+ 3)
                  (* 2)))
;; 展開結果:
;; (* (+ 5 3) 2)

;; 独自マクロの定義と展開
(defmacro my-do-twice [& body]
  `(do
     ~@body
     ~@body))

(macroexpand '(my-do-twice
                (println "Hello")))
;; 展開結果:
;; (do
;;   (println "Hello")
;;   (println "Hello"))

マクロ展開時の注意点:

  • シンボルの衝突を避ける
  • 評価の順序を考慮する
  • 無限展開を防ぐ

上級テクニック:

  • ジェネレーティッドシンボル(gensym)の使用
  • 条件付きマクロ展開
  • マクロの合成
例:
;; シンボルの衝突を避けるためのgensymの使用
(defmacro with-gensyms [[& names] & body]
  `(let [~@(mapcat (fn [name]
                     [name `(gensym (str ~name "-"))])
                   names)]
     ~@body))

(defmacro my-when-let [bindings & body]
  (with-gensyms [temp]
    `(let [~temp ~(second bindings)]
       (when ~temp
         (let [~(first bindings) ~temp]
           ~@body)))))

並列処理

[編集]

Clojureは、Java仮想マシンの並列処理機能に依存しており、Javaの並列処理APIと同様に、複数のスレッドを使って処理を並列化することができます。Clojureには、並列処理のための独自の機能もあります。この節では、Clojureで並列処理を行う方法について説明します。

スレッドの生成

[編集]

Clojureでは、Javaのスレッド生成APIを使用してスレッドを生成することができます。例えば、以下のように、Threadクラスを使用して新しいスレッドを生成することができます。

コード例
(defn worker []
  (println "Worker started")
  (Thread/sleep 5000)
  (println "Worker finished"))

(defn -main []
  (let [t (Thread. #(worker))]
    (.start t)
    (.join t)))

上記の例では、worker関数を別のスレッドで実行するために、Thread.()コンストラクタを使用して新しいスレッドを生成しています。そして、.startメソッドを呼び出して、スレッドを開始しています。.joinメソッドは、スレッドの終了を待つために使用されます。

パラレルマップ

[編集]

Clojureには、pmap関数という便利な関数があります。この関数は、マップ関数とコレクションを受け取り、コレクション内の各要素に対してマップ関数を並列に適用します。結果は、新しいシーケンスとして返されます。以下は、pmap関数を使用する例です。

コード例
(defn slow-square [x]
  (Thread/sleep 1000)
  (* x x))

(defn -main []
  (let [result (pmap slow-square [1 2 3 4 5])]
    (println result)))

上記の例では、slow-square関数を並列に呼び出して、シーケンス[1 2 3 4 5]の各要素を2乗します。pmap関数は、各要素の処理を並列に行うため、計算時間を短縮することができます。

アトミックオペレーション

[編集]

Clojureにおける並列処理は、アトミックオペレーションと呼ばれる機能を使用して実現されます。アトミックオペレーションは、複数のスレッドやプロセスが同時に同じ変数を変更しようとしても、競合状態を回避するための仕組みです。

Clojureでは、atomrefagentといった3つのアトミック操作を提供しています。それぞれの違いは、以下のとおりです。

  • atom:一度に1つのスレッドしか変更できない単一の値を表します。値の変更はswap!関数を使用して行います。
  • ref:複数の値をトランザクションの中で変更できます。値の変更はalter関数を使用して行います。トランザクションはdosyncマクロで定義されます。
  • agent:一度に複数のスレッドが値を変更できます。値の変更はsend関数を使用して行います。

これらの操作を使用することで、Clojureの並列処理では、常に正確な結果を得ることができます。ただし、アトミック操作を使用することで、処理速度が低下する場合があるため、必要に応じて最適な方法を選択する必要があります。

Webアプリケーション開発

[編集]

ClojureでのWebアプリケーション開発は、主にRing(低レベルのWebアプリケーションインターフェース)とCompojure(ルーティングライブラリ)を組み合わせて行います。これらのライブラリを使用することで、シンプルかつ強力なWebアプリケーションを構築できます。

RingとCompojureの紹介

[編集]

Ring

[編集]

RingはClojureのWebアプリケーションの標準的な抽象化層です。HTTPリクエストとレスポンスを単純なClojureのデータ構造として扱います。

主な特徴:

  • HTTPハンドラ、ミドルウェア、アダプタの概念
  • シンプルなリクエスト/レスポンスモデル
  • 豊富なミドルウェアエコシステム
基本的な使用例:
(require '[ring.adapter.jetty :as jetty])

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello, World!"})

(defn -main []
  (jetty/run-jetty handler {:port 3000}))

Compojure

[編集]

CompojureはRing上に構築されたルーティングライブラリで、URLパターンとハンドラ関数を結びつけます。

主な機能:

  • 宣言的なルーティング
  • パラメータの抽出
  • レスポンスヘルパー
基本的な使用例:
(require '[compojure.core :refer :all]
         '[compojure.route :as route])

(defroutes app
  (GET "/" [] "Hello World")
  (GET "/user/:id" [id] (str "User " id))
  (route/not-found "Page not found"))

ハンドラの定義とルーティング

[編集]

ハンドラの定義

[編集]

ハンドラはHTTPリクエストを受け取り、レスポンスを返す関数です。

ハンドラの基本構造:

(defn my-handler [{:keys [uri request-method headers params]}]
  {:status 200
   :headers {"Content-Type" "application/json"}
   :body (str "Handling " request-method " request to " uri)})

リクエストマップの主要なキー:

  • :uri - リクエストのURI
  • :request-method - HTTPメソッド(:get、:post等)
  • :headers - リクエストヘッダー
  • :params - クエリパラメータとフォームパラメータ

ルーティングパターン

[編集]

Compojureでの一般的なルーティングパターン:

(defroutes app-routes
  ;; 基本的なGETルート
  (GET "/" [] "Welcome!")
  
  ;; パスパラメータ
  (GET "/user/:id" [id] 
    (str "User profile: " id))
  
  ;; クエリパラメータ
  (GET "/search" [q]
    (str "Search results for: " q))
  
  ;; POSTハンドリング
  (POST "/submit" [name email]
    (str "Form submitted for " name))
  
  ;; 正規表現ルート
  (GET #"/items/\d+" []
    "Numeric item ID")
    
  ;; 404ハンドリング
  (route/not-found "Not Found"))

;; ミドルウェアの適用
(def app
  (-> app-routes
      wrap-params
      wrap-keyword-params))

テンプレートエンジンの利用

[編集]

ClojureのWebアプリケーションでは、主にHiccup(Clojureデータ構造でHTMLを生成)やSelmer(Djangoスタイルのテンプレート)が使用されます。

Hiccup

[編集]

Hiccupは、ClojureのベクターとマップをHTMLに変換します。

特徴:

  • Clojureネイティブ
  • 高性能
  • 簡潔な構文
使用例:
(require '[hiccup.core :refer [html]]
         '[hiccup.page :refer [html5]])

(defn index-page [user]
  (html5
    [:head
     [:title "Welcome"]]
    [:body
     [:h1 (str "Hello, " user "!")]
     [:div.content
      [:p "Welcome to our website"]
      [:ul
       (for [item ["One" "Two" "Three"]]
         [:li item])]]]))

(defroutes app
  (GET "/" []
    {:status 200
     :headers {"Content-Type" "text/html"}
     :body (index-page "Guest")}))

Selmer

[編集]

Selmerは、より伝統的なテンプレートエンジンアプローチを提供します。

特徴:

  • Djangoライクな構文
  • ファイルベースのテンプレート
  • 条件分岐とループ
使用例:
(require '[selmer.parser :as selmer])

;; テンプレートの定義
(selmer/set-resource-path! "templates")

(defn render-page [user]
  (selmer/render-file "index.html"
                      {:user user
                       :items ["One" "Two" "Three"]}))

;; HTML テンプレート (templates/index.html):
;; <html>
;;   <head><title>Welcome</title></head>
;;   <body>
;;     <h1>Hello, {{user}}!</h1>
;;     <ul>
;;     {% for item in items %}
;;       <li>{{item}}</li>
;;     {% endfor %}
;;     </ul>
;;   </body>
;;  </html>

(defroutes app
  (GET "/" []
    {:status 200
     :headers {"Content-Type" "text/html"}
     :body (render-page "Guest")}))

ベストプラクティス

[編集]
  • レイアウトテンプレートを使用してコードの重複を避ける
  • クロスサイトスクリプティング(XSS)対策のためにエスケープ処理を行う
  • パフォーマンスのためにテンプレートをキャッシュする
  • 大規模なアプリケーションではコンポーネントベースのアプローチを検討する

ClojureScript

[編集]

ClojureScriptの紹介

[編集]

ClojureScriptは、ClojureのJavaScript版であり、ブラウザやNode.js上で動作することを目的とした言語です。Clojureの強力な機能とシンプルな構文を持ちながら、JavaScript環境にコンパイルされ、既存のJavaScriptエコシステムと統合できます。ClojureScriptは、関数型プログラミング、遅延評価、イミュータブルデータ構造など、Clojureの利点をそのままJavaScript環境に持ち込むことが可能です。

ClojureScriptとJavaScriptの違い

[編集]

ClojureScriptとJavaScriptの大きな違いは、言語のパラダイムです。ClojureScriptは、関数型プログラミングを強くサポートし、状態の管理にイミュータブルなデータ構造を使用します。一方、JavaScriptはオブジェクト指向プログラミングと手続き型プログラミングを主に採用しており、ミュータブルなデータ構造が一般的です。また、ClojureScriptはREPL(Read-Eval-Print Loop)を活用して、インタラクティブな開発体験を提供しますが、JavaScriptではこのようなリアルタイムな開発環境は標準では提供されていません。

ReagentとReactの紹介

[編集]

Reagentは、ClojureScriptでReactを使いやすくするための軽量なライブラリです。Reagentは、ReactのコンポーネントシステムをClojureScriptのスタイルで扱えるようにし、データの流れやコンポーネントの状態をシンプルに管理するための便利なツールを提供します。Reactは、JavaScriptのUIライブラリとして広く普及していますが、Reagentを使うことで、ClojureScriptの簡潔な構文と組み合わせて、より効率的にReactベースのユーザインターフェースを構築できます。

その他のトピック

[編集]

テスト

[編集]

ロギング

[編集]

データベースアクセス

[編集]

デプロイメント

[編集]

脚註

[編集]


外部リンク

[編集]