0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Common Lisp ベーシックマスター Level 6 (再帰と反復)

Last updated at Posted at 2025-12-05

1. 反復とは何か

反復(iteration) は、計算を以下の形式で行う:

  1. 初期状態の設定
  2. 条件評価
  3. 状態の更新
  4. 条件が満たされるまで継続

このプロセスは一般に、「時系列で変化し続ける状態(state)」に依存する。

1-1. Lisp における反復構文の種類

構文 特徴 想定する思考モデル 使用推奨場面
loop 汎用制御構文 C言語に近い命令的思考 状態を細かく制御する処理
dotimes 回数反復 カウンタベース 指定回数の繰り返し
dolist リスト走査 データ列反復 データ解析・集計
mapcar 関数適用 変換適用の宣言的思考 関数型データ処理
reduce 集約操作 演算子による統合 総計・評価器作成

2. loop ― 汎用で柔軟な反復

loop は最も自由度が高い反復構文である。変数更新・条件付き break などが書きやすく、C言語の for に近い。

2-1. 基本的な loop

;; 1〜5 を表示
(loop for i from 1 to 5
      do (format t "~A " i))
;; 1 2 3 4 5
;; → NIL
;; リスト要素を順番に表示
(loop for x in '(apple banana cherry)
      do (format t "~A~%" x))
;; apple
;; banana
;; cherry

2-2. 条件付き処理

;; 偶数だけを表示
(loop for n from 1 to 10
      when (evenp n)
      do (format t "~A " n))
;; 2 4 6 8 10

2-3. 集約句(collect, sum など)

loop には値を集める便利な句がある。

;; リストの合計を求める
(loop for n in '(3 8 2 7 5)
      sum n)
;; → 25

;; 2倍した値を収集
(loop for n in '(1 2 3 4 5)
      collect (* n 2))
;; → (2 4 6 8 10)

主な集約句:

意味 初期値
collect 値をリストに集める () (collect x)
sum 合計値を計算 0 (sum x)
count 条件成立回数を数える 0 (count (evenp x))
maximize 最大値を求める -∞ (maximize x)
minimize 最小値を求める +∞ (minimize x)

集約句(accumulation clause)は、Common Lisp の仕様上 loop マクロ専用 です。他の構文では使用できません。ただし、同様の 集約機能 を持つ関数は存在します。

2-4. let と incf

loop の中でよく使う構文を確認する。

let:ローカル変数の宣言

(let ((変数 初期値)
      (変数2 初期値2))
  本体の式...)
(let ((x 10)
      (y 20))
  (+ x y))
;; → 30

incf:変数を増加させる

(let ((x 5))
  (incf x 3)  ; x = x + 3
  x)
;; → 8

(let ((x 5))
  (incf x)    ; 省略すると 1 増加
  x)
;; → 6

2-5. loop で合計を求める(手動版)

(defun sum-with-loop (lst)
  (let ((result 0))
    (loop for n in lst
          do (incf result n))
    result))

(sum-with-loop '(3 8 2 7 5))
;; → 25

3. dotimes ― 回数指定の反復

3-1. dotimes の構文

(dotimes (変数 回数 [結果])
  本体...)
  • 変数は 0 から 回数-1 まで変化
  • 結果を省略すると NIL を返す

3-2. 基本例

;; 0〜4 を表示
(dotimes (i 5)
  (format t "~A " i))
;; 0 1 2 3 4
;; → NIL

;; 合計値を求める(結果を指定)
(let ((sum 0))
  (dotimes (i 5 sum)
    (incf sum i)))
;; → 10(0+1+2+3+4)

3-3. 配列のアクセス

;; 配列の要素を表示
(let ((arr #(10 20 30 40)))
  (dotimes (i (length arr))
    (format t "~A " (aref arr i))))
;; 10 20 30 40

aref:配列の要素を取得

(aref #(10 20 30) 1)
;; → 20(インデックス1の要素)

3-4. 配列とリストの違い

項目 配列(vector) リスト(list)
内部構造 連続したメモリ領域 連結リスト(consセルの鎖)
アクセス速度 ランダムアクセスが速い 先頭から順次たどる
要素数 固定長が基本 可変(追加・削除が容易)
書き方 #(1 2 3) '(1 2 3)
適する用途 数値処理・高速アクセス データ構造・再帰処理

3-5. dotimes でリストを作る

(let ((result '()))
  (dotimes (i 5 (nreverse result))
    (push (* i i) result)))
;; → (0 1 4 9 16)

push と nreverse:

push は 先頭に追加するため、処理途中のresult(16 9 4 1 0) の様に逆順になっている。そのため、最後に nreverse で 正しい順序に並び替えて返している。
nreverse は リストを破壊的に逆順にする関数 。元のリスト構造を変更して要素の順番を反転し、新しい先頭リストを返す。nreverse は 高速・省メモリ。元のリストを保持したい場合は reverse を使用する。

4. dolist ― リスト走査の基本

4-1. dolist の構文

(dolist (変数 リスト [結果])
  本体...)

「順に取り出して処理する」という思考をそのまま書ける、最も自然なリスト反復方法。

4-2. 基本例

;; リストの要素を順に表示
(dolist (x '(apple banana cherry))
  (format t "~A~%" x))
;; apple
;; banana
;; cherry

;; 合計値を求める
(let ((sum 0))
  (dolist (n '(3 8 2 7 5) sum)
    (incf sum n)))
;; → 25

4-3. 条件付き処理

;; 偶数だけ表示
(dolist (n '(1 2 3 4 5))
  (when (evenp n)
    (format t "~A " n)))
;; 2 4

4-4. 新しいリストを作る

(let ((result '()))
  (dolist (x '(1 2 3 4 5) (nreverse result))
    (push (* x x) result)))
;; → (1 4 9 16 25)

4-5. return で途中終了

return は 現在実行中のループやフォームを即座に終了し、指定した値を返す。主に do 系ループ、dolistdotimesloop の中で使用される。

;; 最初の奇数を見つけたら終了
(dolist (x '(2 4 6 7 8))
  (when (oddp x)
    (return x)))
;; → 7

4-6. 最大値を求める

(defun max-with-dolist (lst)
  (let ((max (car lst)))
    (dolist (x lst max)
      (when (> x max)
        (setf max x)))))

(max-with-dolist '(3 8 2 7 5))
;; → 8

5. mapcar と reduce ― 関数型アプローチ

5-1. mapcar:変換専用

「新しいリストを作る」場合に最適。副作用なしで変換処理に徹する。

;; 各数値を2倍
(mapcar (lambda (x) (* x 2)) '(1 2 3 4 5))
;; → (2 4 6 8 10)

;; 文字列を大文字に
(mapcar #'string-upcase '("apple" "banana" "cherry"))
;; → ("APPLE" "BANANA" "CHERRY")

;; 2つのリストの要素を加算
(mapcar #'+ '(1 2 3) '(10 20 30))
;; → (11 22 33)

5-2. reduce:集約専用

構文
(reduce function sequence
        :initial-value init
        :from-end t)
  • :initial-value : 最初の累積値を指定(省略時は最初の要素が使われる)
  • :from-end t : 末尾から畳み込みを行う(右折りたたみになる)

reduce は、シーケンス(リスト・ベクターなど)の全要素を、指定した関数で “1つの値” に畳み込む(まとめる)関数である。

;; リストの合計
(reduce #'+ '(1 2 3 4))
;; → 10

;; 最大値
(reduce #'max '(3 9 2 7 5))
;; → 9

;; 初期値を指定
(reduce #'+ '(1 2 3) :initial-value 100)
;; → 106

5-3. 反復構文の比較

処理手法 記述量 分かりやすさ 関数型度 適した処理
loop 多い 複雑な条件付き処理
dotimes 少ない 回数指定の繰り返し
dolist 少ない 最高 教育用途・汎用
mapcar 少ない 単純変換処理
reduce 少ない 最高 集約・統計

6. 再帰とは何か

再帰(recursion) とは、自分自身を用いて自分を定義する手法である。

6-1. 再帰定義の3原則

原則 内容 欠けた場合の失敗
基底条件(停止条件) 最終的に終了する条件 無限再帰
問題の縮小 データを小さくする 状態が変化せず進まない
自己適用 同じ規則を再利用 別処理に変化してしまう

6-2. 反復と再帰のイメージ

リスト (10 7 4) の合計を考える。

反復のイメージ(横方向):

start -> [10] -> [7] -> [4] -> end
         sum=10  sum=17 sum=21

再帰のイメージ

6-3. 実装比較 ― 3つのアプローチ

手続き型(dolist):

(defun sum-loop (lst)
  (let ((result 0))
    (dolist (x lst result)
      (incf result x))))

(sum-loop '(1 2 3 4 5))
;; → 15

再帰型:

(defun sum-rec (lst)
  (if (null lst)
      0
      (+ (car lst)
         (sum-rec (cdr lst)))))

(sum-rec '(1 2 3 4 5))
;; → 15

関数型(reduce):

(defun sum-reduce (lst)
  (reduce #'+ lst :initial-value 0))

(sum-reduce '(1 2 3 4 5))
;; → 15

目的は同じでも 表現の思想が異なる

7. 再帰の基本パターン

7-1. 階乗

(defun fact (n)
  (if (<= n 1)
      1
      (* n (fact (1- n)))))

(fact 5)
;; → 120(5×4×3×2×1)

7-2. フィボナッチ数

(defun fib (n)
  (if (< n 2)
      n
      (+ (fib (- n 1))
         (fib (- n 2)))))

(fib 10)
;; → 55

7-3. リストの長さ

(defun length-rec (lst)
  (if (null lst)
      0
      (1+ (length-rec (cdr lst)))))

(length-rec '(a b c d e))
;; → 5

7-4. リストの反転

(defun reverse-rec (lst)
  (if (null lst)
      nil
      (append (reverse-rec (cdr lst))
              (list (car lst)))))

(reverse-rec '(1 2 3 4 5))
;; → (5 4 3 2 1)

7-5. 再帰を書くときの注意点

チェックリスト

チェック項目 確認内容
基底条件があるか (null lst)(<= n 1) など終了条件
基底条件を先に書いているか if の then 部分に終了処理
データが縮小しているか (cdr lst)(1- n) を渡しているか
空リスト/ゼロで動くか (func '())(func 0) でテスト

よくある間違い

;; NG:データが縮小していない
(defun wrong (lst)
  (if (null lst) 0
      (+ (car lst) (wrong lst))))   ; lst のまま渡している

;; OK:cdr で縮小
(defun correct (lst)
  (if (null lst) 0
      (+ (car lst) (correct (cdr lst)))))

再帰のテンプレート

;; リスト再帰
(defun リスト処理 (lst)
  (if (null lst)
      基底値
      (結合 (car lst) (リスト処理 (cdr lst)))))

;; 数値再帰
(defun 数値処理 (n)
  (if (<= n 1)
      基底値
      (結合 n (数値処理 (1- n)))))

trace でデバッグ

(trace sum-rec)       ; トレース開始
(sum-rec '(1 2 3))    ; 呼び出しの様子が表示される
(untrace sum-rec)     ; トレース解除

8. 尾再帰最適化

8-1. 尾再帰とは

尾再帰(tail recursion) は、関数の最後で再帰呼び出しを行う形式。処理系によってはループに変換され、スタックを消費しない。

尾再帰ではない:

(defun sum-rec (lst)
  (if (null lst)
      0
      (+ (car lst) (sum-rec (cdr lst)))))
;; ↑ + の計算が残っている → 尾再帰ではない

尾再帰:

(defun sum-tail (lst acc)
  (if (null lst)
      acc
      (sum-tail (cdr lst) (+ acc (car lst)))))
;; ↑ 再帰呼び出しが最後 → 尾再帰
;; 使い方
(sum-tail '(1 2 3 4 5) 0)
;; → 15

8-2. 尾再帰の例

階乗(尾再帰版):

(defun fact-tail (n acc)
  (if (<= n 1)
      acc
      (fact-tail (1- n) (* acc n))))

(fact-tail 5 1)
;; → 120

フィボナッチ(尾再帰版):

(defun fib-tail (n a b)
  (if (= n 0)
      a
      (fib-tail (1- n) b (+ a b))))

(fib-tail 10 0 1)
;; → 55

リストの長さ(尾再帰版):

(defun length-tail (lst acc)
  (if (null lst)
      acc
      (length-tail (cdr lst) (1+ acc))))

(length-tail '(a b c d e) 0)
;; → 5

8-3. 尾再帰の注意点

  • Common Lisp の標準では尾再帰最適化は 保証されていない
  • ただし多くの処理系(SBCL など)は最適化を行う
  • 実用上はループ(loop, dolist)がよく使われる
  • 尾再帰は「理論価値」+「関数型思考訓練」として習得する

9. 再帰が有効な場面

9-1. 木構造の処理

入れ子のリスト(木構造)は再帰で自然に処理できる。

;; 入れ子リストの要素数を数える
(defun deep-count (tree)
  (cond
    ((null tree) 0)
    ((atom tree) 1)
    (t (+ (deep-count (car tree))
          (deep-count (cdr tree))))))

(deep-count '(a (b c) ((d)) e))
;; → 5

9-2. 再帰が自然な領域

分野 用例 再帰が自然な理由
木構造解析 XML, JSON, AST 分割統治型
言語処理 文解析、依存構造 階層表現
画像処理 フラクタル生成 自己相似性
AI探索 深さ優先探索、minimax 状態展開
DSL 構文規則展開 関数による記述

10. 練習課題

課題1:dolist で最大値

以下のリストの最大値を dolist で求めよ。

'(8 4 19 7 3 11)

解答

(defun max-dolist (lst)
  (let ((max (car lst)))
    (dolist (x (cdr lst) max)
      (when (> x max)
        (setf max x)))))

(max-dolist '(8 4 19 7 3 11))
;; → 19

課題2:再帰で最大値

同じリストの最大値を再帰で求めよ。

解答

(defun max-rec (lst)
  (if (null (cdr lst))
      (car lst)
      (let ((rest-max (max-rec (cdr lst))))
        (if (> (car lst) rest-max)
            (car lst)
            rest-max))))

(max-rec '(8 4 19 7 3 11))
;; → 19

課題3:リストの反転(再帰)

再帰を使ってリストを反転する my-reverse を作れ。

解答

(defun my-reverse (lst)
  (if (null lst)
      nil
      (append (my-reverse (cdr lst))
              (list (car lst)))))

(my-reverse '(1 2 3 4 5))
;; → (5 4 3 2 1)

課題4:尾再帰でリスト反転

尾再帰を使ってリストを反転する my-reverse-tail を作れ。

解答

(defun my-reverse-tail (lst acc)
  (if (null lst)
      acc
      (my-reverse-tail (cdr lst) (cons (car lst) acc))))

(my-reverse-tail '(1 2 3 4 5) nil)
;; → (5 4 3 2 1)

課題5:deep-count

入れ子リストの要素数を数える deep-count を作れ。

(deep-count '(a (b c) ((d)) e))
;; → 5

解答

(defun deep-count (tree)
  (cond
    ((null tree) 0)
    ((atom tree) 1)
    (t (+ (deep-count (car tree))
          (deep-count (cdr tree))))))

(deep-count '(a (b c) ((d)) e))
;; → 5

課題6:木構造の最大深度

任意のネストリストの最大深度を求める max-depth を作れ。

(max-depth '(a (b (c (d)))))
;; → 4

解答

(defun max-depth (tree)
  (if (atom tree)
      0
      (1+ (reduce #'max
                  (mapcar #'max-depth tree)
                  :initial-value 0))))

(max-depth '(a (b (c (d)))))
;; → 4

(max-depth '((a b) (c (d e (f)))))
;; → 4

11. まとめ

反復構文の選び方

再帰の3原則

  1. 基底条件 - 停止する条件を必ず書く
  2. 問題の縮小 - データを小さくして渡す
  3. 自己適用 - 同じ関数を呼び出す

尾再帰のパターン

(defun 関数名 (データ 累積値)
  (if (終了条件)
      累積値
      (関数名 (縮小したデータ) (更新した累積値))))

いつ何を使うか

状況 推奨
単純なリスト走査 dolist
回数指定の繰り返し dotimes
リストの変換 mapcar
値の集約 reduce
複雑な制御 loop
木構造・再帰的データ 再帰
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?