Clojure初学者のための関数型プログラミング入門 - コードの読み方から始めよう
この記事では、Clojureのコードがどのように構成されているか、どの部分が関数で、どの部分が引数なのかを詳しく解説します。関数型プログラミングが初めての方でも、コードが読めるようになることを目標にします。
1. Clojureの基本構文 - すべては括弧から始まる
関数呼び出しの基本ルール
Clojureではすべての処理が括弧で囲まれ、最初の要素が関数、残りが引数という規則があります。
;; 基本的な構文
(関数名 引数1 引数2 引数3...)
;; 具体例
(+ 1 2 3)
;; ↑ ↑ ↑ ↑
;; │ │ │ └── 引数3(数値の3)
;; │ │ └──── 引数2(数値の2)
;; │ └────── 引数1(数値の1)
;; └──────── 関数(足し算の+)
;; 結果: 6
重要: 他の言語と違い、関数名が最初に来ます!
他の言語 | Clojure |
---|---|
add(1, 2, 3) |
(+ 1 2 3) |
Math.max(5, 10) |
(max 5 10) |
"hello".length() |
(count "hello") |
練習: 基本的な関数を読んでみよう
;; 例1: 数値の計算
(* 4 5)
;; 読み方: 「*関数に、4と5を引数として渡す」
;; 意味: 4 × 5 = 20
;; 例2: 文字列の長さ
(count "hello")
;; 読み方: 「count関数に、"hello"を引数として渡す」
;; 意味: "hello"の文字数 = 5
;; 例3: 最大値を求める
(max 10 5 8 3)
;; 読み方: 「max関数に、10、5、8、3を引数として渡す」
;; 意味: 最大値 = 10
2. データ構造の理解 - マップとベクター
マップ(Map)- キーと値のペア
;; マップの定義
{:name "太郎" :age 25}
;; ↑ ↑ ↑ ↑
;; │ │ │ └── 値(数値の25)
;; │ │ └────── キー(:age)
;; │ └──────────── 値(文字列の"太郎")
;; └─────────────────── キー(:name)
マップの構成要素:
-
{}
: マップを表す括弧 -
:name
,:age
: キーワード(必ず:
で始まる) -
"太郎"
,25
: 値
ベクター(Vector)- 順序のあるリスト
;; ベクターの定義
["りんご" "みかん" "バナナ"]
;; ↑ ↑ ↑
;; │ │ └── インデックス2の要素
;; │ └─────────── インデックス1の要素
;; └─────────────────── インデックス0の要素
ベクターの構成要素:
-
[]
: ベクターを表す括弧 - インデックス : 0から始まる位置番号
3. サンプルデータの完全解析
(def company {:name "ABC Corp"
:employees [{:name "taro" :dept :engineering}
{:name "花子" :dept :sales}]})
この1行を詳しく分解してみましょう
(def company {:name "ABC Corp" :employees [...]})
;; ↑ ↑ ↑
;; │ │ └── 引数2: マップ(会社のデータ)
;; │ └─────────── 引数1: シンボル(変数名)
;; └───────────── 特殊形式: def(変数定義)
シンボル(Symbol)とは何か?
シンボルは、Clojureにおける「名前」を表すデータ型です。変数名や関数名として使われます。
;; シンボルの例
company ;; これがシンボル(クォートなし)
my-name ;; これもシンボル
user-data ;; これもシンボル
;; シンボル vs 文字列の違い
company ;; シンボル(変数名として使われる)
"company" ;; 文字列(ただのテキストデータ)
シンボルの特徴:
- クォート(
"
)で囲まない - 変数名や関数名として使用される
- ハイフン(
-
)を含むことができる - 数字で始めることはできない
def
でのシンボルの役割
(def company {:name "ABC Corp" :employees [...]})
;; ↑ ↑ ↑
;; │ │ └── 引数2: マップ(会社のデータ)
;; │ └─────────── 引数1: company(シンボル = 変数名)
;; └───────────── 特殊形式: def(変数定義)
重要: def
の最初の引数は必ずシンボルでなければならない!
;; ✅ 正しい(シンボルを使用)
(def my-data {:x 1})
;; ❌ 間違い(文字列を使用)
(def "my-data" {:x 1}) ;; エラー!
;; ❌ 間違い(数字で始まる)
(def 123data {:x 1}) ;; エラー!
シンボルの命名規則
良い例 | 悪い例 | 理由 |
---|---|---|
user-name |
userName |
Clojureはケバブケース推奨 |
total-count |
total_count |
アンダースコアよりハイフン |
api-key |
123key |
数字で始めてはいけない |
is-valid? |
isValid |
真偽値には? をつける慣習 |
クォート '
の詳細解説
'
(クォート)の役割
クォート = 「評価を停止する」記号
;; 通常(評価される)
company
;; => {:name "ABC Corp", :employees [...]}
;; 意味: companyシンボルから値を取得
;; クォート付き(評価されない)
'company
;; => company
;; 意味: companyシンボル自体を返す(値を取得しない)
評価される vs 評価されない
書き方 | 動作 | 結果 | 用途 |
---|---|---|---|
company |
シンボルを評価(値を取得) | {:name "ABC Corp"...} |
変数の値を使いたい時 |
'company |
シンボルを評価しない | company |
シンボル自体を扱いたい時 |
具体的な例で理解する
;; 変数定義
(def my-name "太郎")
;; パターン1: 評価する(通常の使用)
my-name
;; => "太郎"
;; 動作: my-nameから値"太郎"を取得
;; パターン2: 評価しない(クォート使用)
'my-name
;; => my-name
;; 動作: シンボルmy-name自体を返す
;; パターン3: quote関数(同じ意味)
(quote my-name)
;; => my-name
;; 動作: 'my-nameと完全に同じ
リストでのクォートの威力
;; 通常のリスト(関数として評価される)
(+ 1 2 3)
;; => 6
;; 動作: +関数を実行
;; クォートしたリスト(データとして扱われる)
'(+ 1 2 3)
;; => (+ 1 2 3)
;; 動作: リスト自体を返す(実行しない)
;; より複雑な例
'(def company {:name "ABC"})
;; => (def company {:name "ABC"})
;; 動作: defを実行せず、リスト自体を返す
なぜクォートが必要なのか?
Clojureの評価ルール:
- 数値・文字列・キーワード → そのまま
- シンボル → 値を取得しようとする
- リスト → 関数として実行しようとする
;; 問題例: 存在しない変数
undefined-var
;; => エラー: Unable to resolve symbol: undefined-var
;; 解決: クォートで評価を停止
'undefined-var
;; => undefined-var (エラーにならない)
実用的な使用場面
1. メタプログラミング
;; マクロでコードを生成する時
(defmacro my-def [name value]
`(def ~name ~value)) ;; バッククォートとアンクォート
;; 関数名をデータとして扱う
(def function-names ['+ '- '* '/])
2. 動的なシンボル操作
;; シンボルから文字列に変換
(name 'company)
;; => "company"
;; 文字列からシンボルに変換
(symbol "company")
;; => company
;; シンボルかどうか判定
(symbol? 'company)
;; => true
(symbol? company)
;; => false(値なので)
3. データ構造でのシンボル保存
;; 関数名のリスト
(def operations ['+ '- '* '/])
;; 変数名のリスト
(def variable-names ['x 'y 'z])
;; 設定データ
(def config {'database-host "localhost"
'database-port 5432})
クォートの種類
記号 | 名前 | 機能 | 例 |
---|---|---|---|
' |
クォート | 評価停止 |
'(+ 1 2) → (+ 1 2)
|
` |
バッククォート | テンプレート | `(+ 1 ~x) |
~ |
アンクォート | 評価再開 | `(+ 1 ~(+ 2 3)) |
~@ |
スプライシング | リスト展開 | `(list ~@[1 2 3]) |
デバッグでの活用
;; コードをそのまま表示したい時
(println '(-> company :employees second :name))
;; => (-> company :employees second :name)
;; 動作: コード自体を出力(実行結果ではない)
;; 関数定義を確認
(meta #'company)
;; => {:line 1, :column 1, :file "user.clj", :name company, :ns user}
よくある間違いと対処法
;; ❌ 間違い: クォートのつけ忘れ
(def symbols [x y z])
;; エラー: x, y, zの値を取得しようとする
;; ✅ 正しい: シンボル自体を保存
(def symbols ['x 'y 'z])
;; ❌ 間違い: 不要なクォート
(def result '(+ 1 2))
;; => (+ 1 2) (計算されない)
;; ✅ 正しい: 計算したい場合
(def result (+ 1 2))
;; => 3
マクロでのクォート使用例
;; 条件付きデバッグマクロ
(defmacro debug [expr]
`(let [result# ~expr]
(println "Debug:" '~expr "=>" result#)
result#))
;; 使用例
(debug (+ 1 2 3))
;; 出力: Debug: (+ 1 2 3) => 6
;; 戻り値: 6
まとめ:クォートの使い分け
いつクォートを使うか:
-
シンボル自体を扱いたい時:
'company
-
コードをデータとして扱いたい時:
'(+ 1 2 3)
- メタプログラミング: マクロやコード生成
- 設定やメタデータ: シンボルをキーとして使用
覚え方:
- クォートなし: 「この変数の値をください」
- クォートあり: 「この名前(シンボル)をそのままください」
def
の役割:
- 特殊形式:
def
- 引数1:
company
(シンボル = 変数名) - 引数2:
{:name "ABC Corp" ...}
(保存する値) - 意味: 「
company
というシンボル(名前)で、マップデータを保存する」
シンボルの実際の使用例
;; 様々なシンボルの定義
(def user-count 100) ;; user-countというシンボル
(def is-active? true) ;; is-active?というシンボル
(def api-endpoint "localhost") ;; api-endpointというシンボル
;; シンボルを使って値を取得
user-count ;; => 100
is-active? ;; => true
api-endpoint ;; => "localhost"
;; 関数定義でもシンボルを使用
(defn calculate-total [items] ;; calculate-totalというシンボル
(reduce + items))
;; 関数呼び出し
calculate-total ;; => #function[...] (関数オブジェクト)
(calculate-total [1 2 3]) ;; => 6 (関数を実行)
シンボル vs その他のデータ型
;; シンボル(変数名)
my-var
;; 文字列(テキストデータ)
"my-var"
;; キーワード(マップのキーなどに使用)
:my-var
;; 数値
42
;; 実際の使い分け例
(def user-name "太郎") ;; user-name: シンボル、"太郎": 文字列
(def user {:name "太郎"}) ;; user: シンボル、:name: キーワード、"太郎": 文字列
{:name "ABC Corp"
:employees [{:name "taro" :dept :engineering}
{:name "花子" :dept :sales}]}
構造の解説:
マップ全体
├── キー: :name
│ └── 値: "ABC Corp" (文字列)
└── キー: :employees
└── 値: [...] (ベクター)
├── インデックス0: {:name "taro" :dept :engineering} (マップ)
└── インデックス1: {:name "花子" :dept :sales} (マップ)
4. データアクセスの仕組み
基本的なマップアクセス
(:name company)
;; ↑ ↑
;; │ └── 引数: company(マップが入った変数)
;; └──────── 関数: :name(キーワード)
重要: キーワード(:name
)は関数として使える!
;; これらは同じ意味
(:name company)
(get company :name)
;; 読み方の比較
;; ↓ 「:name関数に、companyを引数として渡す」
(:name company)
;; ↓ 「get関数に、companyと:nameを引数として渡す」
(get company :name)
なぜキーワードが関数になるの?
Clojureでは、キーワードは「そのキーの値を取得する関数」として動作します。
;; キーワード関数の例
(:name {:name "太郎" :age 25}) ;; => "太郎"
(:age {:name "太郎" :age 25}) ;; => 25
(:height {:name "太郎" :age 25}) ;; => nil(存在しないキー)
5. ベクターアクセス関数の詳細
first
, second
, last
の仕組み
;; サンプルベクター
["A" "B" "C" "D"]
;; first関数の詳細
(first ["A" "B" "C" "D"])
;; ↑ ↑
;; │ └── 引数: ベクター
;; └──────── 関数: first(最初の要素を取得)
;; 結果: "A"
;; second関数の詳細
(second ["A" "B" "C" "D"])
;; ↑ ↑
;; │ └── 引数: ベクター
;; └───────── 関数: second(2番目の要素を取得)
;; 結果: "B"
;; last関数の詳細
(last ["A" "B" "C" "D"])
;; ↑ ↑
;; │ └── 引数: ベクター
;; └─────── 関数: last(最後の要素を取得)
;; 結果: "D"
より詳しいベクターアクセス
;; nth関数(n番目の要素を取得)
(nth ["A" "B" "C" "D"] 2)
;; ↑ ↑ ↑
;; │ │ └── 引数2: インデックス(2番目)
;; │ └─────────────────── 引数1: ベクター
;; └─────────────────────── 関数: nth
;; 結果: "C"(0から数えて2番目)
;; get関数(安全なアクセス)
(get ["A" "B" "C" "D"] 10)
;; ↑ ↑ ↑
;; │ │ └── 引数2: インデックス(存在しない)
;; │ └─────────────────── 引数1: ベクター
;; └─────────────────────── 関数: get
;; 結果: nil(エラーにならない)
6. スレッディングマクロ->
の完全解説
従来の書き方の問題点
;; 2番目の従業員の名前を取得したい
;; 従来の書き方(右から左に読む必要がある)
(:name (second (:employees company)))
;; ↑ ↑ ↑
;; │ │ └── 3番目: :employees関数でcompanyから従業員リストを取得
;; │ └─────────── 2番目: second関数で2番目の要素を取得
;; └────────────────── 1番目: :name関数で名前を取得
読み方が不自然: 実際の処理順序と逆順で書かれている!
->
マクロの革命
;; スレッディングマクロ(左から右に読める)
(-> company :employees second :name)
;; ↑ ↑ ↑ ↑ ↑
;; │ │ │ │ └── 4番目: :name関数で名前を取得
;; │ │ │ └─────── 3番目: second関数で2番目の要素を取得
;; │ │ └────────────────── 2番目: :employees関数で従業員リストを取得
;; │ └────────────────────────── 1番目: company(開始データ)
;; └───────────────────────────── マクロ: ->(スレッディング)
読み方: 「companyから始めて、employeesを取得し、その2番目を取得し、その名前を取得する」
->
マクロの展開過程
->
マクロは、実際には以下のように段階的に変換されます:
;; 元のコード
(-> company :employees second :name)
;; ステップ1: companyを:employeesの引数にする
(-> (:employees company) second :name)
;; ステップ2: (:employees company)をsecondの引数にする
(-> (second (:employees company)) :name)
;; ステップ3: (second (:employees company))を:nameの引数にする
(:name (second (:employees company)))
結果: 最終的に従来の書き方と同じになる!
7. 実際の実行過程を追ってみよう
データの準備
;; まずデータを定義
(def company {:name "ABC Corp"
:employees [{:name "taro" :dept :engineering}
{:name "花子" :dept :sales}]})
ステップバイステップ実行
;; ステップ1: 元データ
company
;; => {:name "ABC Corp", :employees [{:name "taro", :dept :engineering} {:name "花子", :dept :sales}]}
;; ステップ2: :employees関数を適用
(:employees company)
;; ↑ ↑
;; │ └── 引数: company(マップ)
;; └───────────── 関数: :employees(キーワード)
;; => [{:name "taro", :dept :engineering} {:name "花子", :dept :sales}]
;; ステップ3: second関数を適用
(second [{:name "taro", :dept :engineering} {:name "花子", :dept :sales}])
;; ↑ ↑
;; │ └── 引数: ベクター(従業員リスト)
;; └───────── 関数: second
;; => {:name "花子", :dept :sales}
;; ステップ4: :name関数を適用
(:name {:name "花子", :dept :sales})
;; ↑ ↑
;; │ └── 引数: マップ(花子のデータ)
;; └──────── 関数: :name(キーワード)
;; => "花子"
->
を使った場合の実行
;; 一度に実行
(-> company :employees second :name)
;; => "花子"
;; 途中経過も確認できる
(-> company :employees) ;; => [{...} {...}]
(-> company :employees second) ;; => {:name "花子", :dept :sales}
(-> company :employees second :name) ;; => "花子"
8. より複雑な例で理解を深める
詳細なデータ構造
(def detailed-company
{:name "ABC Corp"
:founded 2020
:employees [{:name "taro"
:dept :engineering
:skills ["Java" "Clojure"]
:contact {:email "taro@abc.com" :phone "090-1234-5678"}}
{:name "花子"
:dept :sales
:skills ["営業" "マーケティング"]
:contact {:email "hanako@abc.com" :phone "090-8765-4321"}}]})
深いネストへのアクセス解析
;; 最初の従業員のメールアドレスを取得
(-> detailed-company :employees first :contact :email)
;; 完全分解
;; ステップ1
detailed-company
;; => {:name "ABC Corp", :founded 2020, :employees [...]}
;; ステップ2: :employees関数適用
(-> detailed-company :employees)
;; 内部的には (:employees detailed-company) と同じ
;; => [{:name "taro", :dept :engineering, ...} {...}]
;; ステップ3: first関数適用
(-> detailed-company :employees first)
;; 内部的には (first (:employees detailed-company)) と同じ
;; => {:name "taro", :dept :engineering, :skills [...], :contact {...}}
;; ステップ4: :contact関数適用
(-> detailed-company :employees first :contact)
;; 内部的には (:contact (first (:employees detailed-company))) と同じ
;; => {:email "taro@abc.com", :phone "090-1234-5678"}
;; ステップ5: :email関数適用
(-> detailed-company :employees first :contact :email)
;; 内部的には (:email (:contact (first (:employees detailed-company)))) と同じ
;; => "taro@abc.com"
9. 関数の引数が複数ある場合
nth
関数を->
で使う
;; 3番目の従業員を取得したい場合
;; 従来の書き方
(nth (:employees company) 2)
;; ↑ ↑ ↑
;; │ │ └── 引数2: インデックス
;; │ └───────────────────── 引数1: ベクター
;; └─────────────────────── 関数: nth
;; ->を使った書き方
(-> company :employees (nth 2))
;; ↑ ↑ ↑ ↑
;; │ │ │ └── 引数2: インデックス
;; │ │ └─────── 関数: nth
;; │ └─────────────── 1番目の処理: :employees
;; └────────────────────────── データの出発点: company
重要: 引数が複数ある関数は括弧で囲む!
括弧の使い分け
;; 引数が1つの場合(括弧不要)
(-> data :key func)
;; 引数が複数の場合(括弧必要)
(-> data :key (func arg1 arg2))
;; 具体例
(-> company :employees (nth 1)) ;; nth関数は引数2つ
(-> company :employees (get 1)) ;; get関数は引数2つ
(-> company :employees count) ;; count関数は引数1つ
(-> company :employees (take 2)) ;; take関数は引数2つ
補足: def
の正体と役割
def
は関数ではなく「特殊形式(Special Form)」
(def company {:name "ABC Corp" :employees [...]})
;; ↑ ↑ ↑
;; │ │ └── 引数2: 保存する値
;; │ └─────────── 引数1: 変数名(シンボル)
;; └───────────── 特殊形式: def
重要: def
は関数ではありません!**特殊形式(Special Form)**と呼ばれる、Clojureの基本的な構文要素です。
関数 vs 特殊形式の違い
種類 | 評価のタイミング | 例 |
---|---|---|
関数 | 引数を先に評価してから関数を実行 |
(+ 1 2) → まず1と2を評価 → +を実行 |
特殊形式 | 引数を評価せずに直接処理 |
(def x 10) → xを評価せず、そのまま変数名として使用 |
def
の具体的な動作
;; defの基本構文
(def 変数名 値)
;; 具体例1: 数値の保存
(def my-number 42)
;; 動作: 「my-number」という名前で「42」という値を保存
;; 具体例2: 文字列の保存
(def my-name "太郎")
;; 動作: 「my-name」という名前で「"太郎"」という文字列を保存
;; 具体例3: マップの保存
(def person {:name "花子" :age 30})
;; 動作: 「person」という名前で「{:name "花子" :age 30}」というマップを保存
なぜdef
は特殊形式なのか?
;; もしdefが普通の関数だったら...(これは実際には動かない例)
(def company {:name "ABC"})
;; 普通の関数なら、引数が先に評価される
;; 1. company → "companyという変数の値を取得しようとする" → エラー!
;; 2. {:name "ABC"} → マップを評価
;; 3. def関数を実行
;; でも実際のdefは特殊形式なので
;; 1. companyを「変数名」として認識(評価しない)
;; 2. {:name "ABC"}を評価
;; 3. companyという名前で値を保存
Clojureの特殊形式一覧
Clojureにはdef
以外にも特殊形式があります:
特殊形式 | 役割 | 例 |
---|---|---|
def |
変数定義 | (def x 10) |
if |
条件分岐 | (if true "yes" "no") |
let |
ローカル変数 | (let [x 5] (+ x 2)) |
fn |
関数定義 | (fn [x] (* x 2)) |
quote |
評価停止 |
(quote (+ 1 2)) → (+ 1 2)
|
def
の実際の動作を確認
;; ステップ1: 変数定義前
company
;; => エラー: Unable to resolve symbol: company
;; ステップ2: defで変数を定義
(def company {:name "ABC Corp"})
;; => #'user/company (変数が作成されたことを示す)
;; ステップ3: 変数定義後
company
;; => {:name "ABC Corp"} (値が取得できる)
;; ステップ4: 変数の存在確認
(bound? #'company)
;; => true (companyという変数が存在する)
def
と関数の組み合わせ
;; 関数の結果をdefで保存
(def sum-result (+ 1 2 3))
;; 動作:
;; 1. (+ 1 2 3) を評価 → 6
;; 2. sum-resultという名前で6を保存
;; 確認
sum-result
;; => 6
;; 他の例
(def employee-count (count (:employees company)))
;; 動作:
;; 1. (:employees company) を評価 → ベクター
;; 2. (count ベクター) を評価 → 数値
;; 3. employee-countという名前で数値を保存
defn
の正体
実はdefn
(関数定義)もdef
を使って作られています!
;; この書き方
(defn greet [name]
(str "こんにちは、" name "さん!"))
;; は実際にはこれと同じ
(def greet
(fn [name]
(str "こんにちは、" name "さん!")))
;; つまり
;; defn = def + fn の組み合わせ
メモリでの動作イメージ
;; defが実行されるとメモリ上で...
(def company {:name "ABC Corp"})
;; メモリ上:
;; company → {:name "ABC Corp"}
(def person {:name "太郎"})
;; メモリ上:
;; company → {:name "ABC Corp"}
;; person → {:name "太郎"}
;; 変数を使用すると
(:name company)
;; 動作:
;; 1. companyという名前からメモリ上の値を取得
;; 2. {:name "ABC Corp"} を取得
;; 3. :name関数で "ABC Corp" を取得
def
使用時の注意点
;; ❌ 同じ名前で再定義(混乱の原因)
(def x 10)
(def x 20)
x ;; => 20 (前の値は消える)
;; ❌ letの中でdef(推奨されない)
(let [y 5]
(def z y)) ;; グローバル変数になってしまう
;; ✅ 適切な使い方
(def config {:db-host "localhost" :db-port 5432})
(def users [{:name "太郎"} {:name "花子"}])
まとめ:def
の特徴
- 特殊形式: 関数ではなく、Clojureの基本構文
- 変数作成: グローバルな変数を作成する
- 評価制御: 最初の引数(変数名)は評価しない
- 永続的: 一度定義すると、そのREPLセッション中はずっと使える
- 上書き可能: 同じ名前で再定義できる(注意が必要)
覚え方:
-
def
= 「このデータに名前をつけて保存する」命令 - 関数 = 「データを受け取って何かを返す」処理