前回までは、Lisp 言語の特徴である「リスト」を基本にして、簡単なプログラムを作ってきました。Lisp 言語には、リストのほかにもおもしろい機能や特徴があります。最近の手続き型言語、とくにスクリプト言語は Lisp 言語からいろいろな機能を取り込んでいるので、以前に比べあっと驚くようなことは少なくなりましたが、それでも Lisp 言語から学ぶことはまだまだあると思っています。今回は関数型言語でよく使われる「高階関数」を中心に、Clojure らしい機能を説明します。
C言語や C++ が「手続き型言語」と呼ばれるのに対し、Lisp 言語は「関数型言語」と呼ばれています。関数型言語は、データに関数を適用していくことでプログラムを実行します。たとえば、数学の関数を考えてください。
f(x, y) = 2x + 3y
関数 f(x, y) は引数 x と y を与えると値を返します。そして、同じ値の引数を与えれば、結果はいつも同じ値になります。関数型言語の基本的な考え方は、このような数学の関数と同じです。
ところが、Lisp 言語は完全な関数型言語ではありません。引数以外の変数を定義して、そこに値を代入することができるからです。つまり、変数の値を書き換えることでプログラムを実行する「手続き型言語」の機能もあるわけです。とくに Common Lisp の場合、手続き型言語からいろいろな機能が持ち込まれたため、ほかの関数型言語に比べると不純度の高い関数型言語になっています。
それから、関数型言語の場合、関数はほかのデータと同等に取り扱うことができます。つまり、関数を変数に代入したり、引数として渡すことができるのです。また、値として関数を返すこともできるので、関数を作る関数を定義することができます。関数を引数として受け取る関数を「汎関数 (functional)」とか「高階関数 (higher order function)」と呼びます。
Clojure などの関数型言語にとって、高階関数は特別なものではありません。Clojure には便利な高階関数がいろいろと用意されていますし、私たちユーザーが定義することも簡単にできるのです。最初に、マップ関数から説明しましょう。
関数型言語の場合、繰り返し処理といえば「再帰定義」ですね。単純な繰り返しならば、末尾再帰 (Clojure であれば defn / recur や loop / recur) を使うことができます。このほかに、Lisp 言語には「マップ関数 (mapping function)」という便利な関数が用意されています。
簡単な例題として、リストの要素どうしを足し算して、その結果をリストに格納する関数を作ってみましょう。つまり、次のような処理を行います。
( 1 2 3 4 5) + (10 20 30 40 50) --------------------- (11 22 33 44 55) 図 : 要素どうしの足し算
これを再帰定義でプログラムすると、次のようになるでしょう。
リスト : リストの要素を足し算する (defn add-element [xs ys] (if (and (seq xs) (seq ys)) (cons (+ (first xs) (first ys)) (add-element (rest xs) (rest ys))) '()))
関数名は add-element としました。ところで、そろそろ「再帰定義」にも慣れてきたと思いますが、add-element の処理内容は理解できたでしょうか。よく分からない方は、次の処理手順を見て考えてください。
│(1 2 3 4) (10 20 30 40) │ = = ─────→ (11 22 33 44) 再 ↓ ↑ 帰 │(2 3 4) (20 30 40) │ 呼 │ = = ─────→ (22 33 44) び ↓ ↑ 出 │(3 4) (30 40) │ し │ = = ─────→ (33 44) ↓ ↑ │(4) (40) │ │ = = ─────→ (44) ↓ ↑ () () ───→ () ─┘ 図 : add-element の処理手順
実は、この処理はマップ関数 map だけで実現できるのです。まずは、実行例を見てください。
user=> (add-element '(1 2 3 4 5) '(10 20 30 40 50)) (11 22 33 44 55) user=> (map + '(1 2 3 4 5) '(10 20 30 40 50)) (11 22 33 44 55)
+ は足し算をする関数でした。関数の処理内容はシンボルに格納されています。つまり、map には + に格納されている関数を渡しているのです。+ の値を表示すると次のようになります。
user=> + #object[clojure.core$_PLUS_ 0x298d9a05 "clojure.core$_PLUS_@298d9a05"]
Clojure の場合、プリミティブの関数は上記のように表示されます。このように、関数を引数として渡すことができるのが Clojure の特徴のひとつです。map は渡された関数をリストの各要素に適用して、その結果をリストに格納して返します [*1]。もし、各要素に対して乗算 * を適用したい場合は、関数 * を map に渡します。
user=> (map * '(1 2 3 4 5) '(10 20 30 40 50)) (10 40 90 160 250)
もちろん、私たちが定義した関数を渡すこともできます。もし、リストの要素を 2 乗したい場合は、数を 2 乗する関数 square を定義して、それを map に渡せばいいのです。
user=> (defn square [x] (* x x)) #'user/square user=> (map square '(1 2 3 4 5)) (1 4 9 16 25)
map を使う場合、渡された関数にとって必要なだけの引数を用意しないと、当然のことですがエラーになります。引数のリストの長さが異なる場合は、短いリストの要素がなくなった時点で処理を終了します。
user=> (map cons '(a b c d) '((1) (2) (3) (4) (5))) ((a 1) (b 2) (c 3) (d 4))
なお、特殊形式の関数を map に渡すことはできません。その場合はエラーになります。
それではここでマップ関数を作ってみましょう。プログラムを簡単にするため、マップ関数に渡すことができる関数は引数をひとつだけ取るものに限定します。プログラムは次のようになります。
リスト : マップ関数 (defn mapcar [func ls] (if-not (seq ls) '() (cons (func (first ls)) (mapcar func (rest ls)))))
関数名は mapcar としました。名前は Common Lisp から拝借しました。引数 func はリストの要素に適用する関数です。func を呼び出す方法は、今までの関数呼び出しとまったく同じで、リストの先頭の要素に func を書くだけです。
func に格納された値は関数なので、func を評価するとその「関数」が得られます。リストの先頭の要素が関数なので、Clojure は func に格納されている値を関数として呼び出すのです。これは Scheme でも同じです。もしも、func の値が関数でなければエラーになります。
ちなみに、Common Lisp の場合、変数 func に格納された関数をそのまま呼び出すことはできません。funcall という特別な関数を使って、(funcall func ...) というようにプログラムする必要があります。関数の扱い方は Clojure や Scheme の方が関数型言語らしくてすっきりしていると思います。
フィルター (filter) はリストの要素に述語 predicate を適用し、predicate が真を返す要素をリストに格納して返す関数です。ここでは Clojure の勉強のため、述語が真を返す要素を削除する関数 remove-if を作ってみましょう。名前は Common Lisp から拝借しました。
リスト ; 述語が真となる要素を削除する (defn remove-if [pred ls] (if-not (seq ls) '() (if (pred (first ls)) (remove-if pred (rest ls)) (cons (first ls) (remove-if pred (rest ls))))))
remove-if も簡単ですね。要素に述語 pred を適用して結果が真ならば要素をリストに加えません。結果が偽ならば要素をリストに加えるだけです。簡単な実行例を示しましょう。
user=> (remove-if odd? '(1 2 3 4 5)) (2 4) user=> (remove-if even? '(1 2 3 4 5)) (1 3 5)
remove-if に odd? を渡すとリストから奇数の要素を削除することができ、even? を渡すと偶数の要素を削除することができます。
なお、Clojure には同等の機能を持つ関数 remove や filter があります。簡単な実行例を示しましょう。
user=> (remove odd? '(1 2 3 4 5)) (2 4) user=> (filter odd? '(1 2 3 4 5)) (1 3 5)
2 つの引数を取る関数 f とリストを引数に受け取る関数 reduce を考えます。reduce はリストの各要素に対して関数 f を次のように適用します。
(1) (a1 a2 a3 ... an-1 an) => (f (f ... (f (f a1 a2) a3) ...) an-1) an) (2) (a1 a2 a3 ... an-1 an) => (f a1 (f a2 (f a3 ... (f an-1 an) ... )))
関数 f を適用する順番で 2 通りの方法があります。たとえば、関数 f が単純な加算関数とすると、reduce の結果はどちらの場合もリストの要素の和になります。
f(x, y) = x + y の場合 : reduce => a1 + a2 + a3 + ... + an-1 + an
このように、reduce はリストのすべての要素を関数 f を用いて結合します。この操作を「縮約」とか「畳み込み」といいます。また、reduce の引数に初期値 g を指定することがあります。この場合、reduce は次のような動作になります。
(1) (a1 a2 a3 ... an-1 an) => (f ... (f (f g a1) a2) ...) an-1) an) (2) (a1 a2 a3 ... an-1 an) => (f a1 (f a2 (f a3 ... (f an g) ...)))
Clojure には (1) の動作を行う関数 reduce が定義されているので、ここでは (1) の動作を行う関数 foldl と、(2) の動作を行う関数 foldr を作ってみましょう。プログラムは次のようになります。
リスト : 畳み込み (defn foldl [f a ls] (if-not (seq ls) a (recur f (f a (first ls)) (rest ls)))) (defn foldr [f a ls] (if-not (seq ls) a (f (first ls) (foldr f a (rest ls)))))
第 1 引数 f が適用する関数、第 2 引数 a が初期値、第 3 引数がリストです。最初に、ls が空リストかチェックします。これが再帰呼び出しの停止条件になりますが、foldl, foldr に空リストが与えられた場合にも対応します。この場合は初期値 a を返します。次の else 節で関数 f を適用して foldl, foldr を再帰呼び出しします。
foldl の場合、引数 a とリストの要素が関数 f の引数になり、その返り値が foldl を再帰呼び出しするときの第 2 引数 a になります。つまり、リストの要素が f の第 2 引数になり、第 1 引数にはこれまでの処理結果が渡されます。
foldr の場合は逆になり、関数 f の第 1 引数にリストの要素が渡されて、これまでの処理結果は第 2 引数に渡されます。foldr はリストの最後尾から処理を行うため、foldr を再帰呼び出しした結果を関数 f の第 2 引数に渡します。
たとえば、(foldr f g '(a1 a2 a3)) は次のように展開されます。
(foldr f g (a1 a2 a3)) ↓ (f a1 (foldr f g (a2 a3))) ↓ (f a1 (f a2 (foldr f g (a3)))) ↓ (f a1 (f a2 (f a3 (foldr f g ())))) ↓ (f a1 (f a2 (f a3 g))) 図 : foldr の動作
これで reduce の (2) の動作を実現することができます。それでは、簡単な実行例を示しましょう。
user=> (foldl + 0 '(1 2 3 4 5)) 15 user=> (foldr + 0 '(1 2 3 4 5)) 15 user=> (defn xcons [x y] (cons y x)) #'user/xcons user=> (foldl xcons '() '(1 2 3 4 5)) (5 4 3 2 1) user=> (foldr cons '() '(1 2 3 4 5)) (1 2 3 4 5) user=> (foldl list 0 '(1 2 3 4 5)) (((((0 1) 2) 3) 4) 5) user=> (foldr list 6 '(1 2 3 4 5)) (1 (2 (3 (4 (5 6)))))
cons や list を用いてリストの要素を結合してみると、畳み込みの動作や foldl と foldr の違いがよくわかると思います。
次は apply を説明します。
apply func args-list
apply は最初の引数 func を関数として呼び出します。このとき、第 2 引数のリストの要素が func の引数として渡されます。apply は func の評価結果を返します。簡単な使用例を示しましょう。
user=> (apply + '(1 2 3)) 6 user=> (apply first '((1 2 3))) 1
また apply は次のように、func と args-list の間に引数を与えることができます。
user=> (apply + 4 5 6 '(1 2 3)) 21
apply はリストに格納されている要素を引数として関数に渡す場合に便利です。
ところで、高階関数を使うようになると、数を 2 乗する square のような小さな関数を、いちいち定義するのが面倒になります。とくに、その高階関数でしか使わないのであれば、なおさらそう思うでしょう。
このような場合、関数型言語では「無名関数」を使うことができます。無名関数は名前のない関数のことで、匿名関数と呼ばれることもあります。Lisp / Scheme では「ラムダ式 (lambda expression)」といいます。ラムダはギリシャ文字の λ のことです。Clojure では無名関数を定義するのに fn を使います。次の例を見てください。
user=> (map (fn [x] (* x x)) '(1 2 3 4 5)) (1 4 9 16 25)
(fn [x] (* x x)) が無名関数です。このように、無名関数はリストで、かつ、その第 1 要素には fn というシンボルがセットされています。無名関数の構造は、関数を定義する defn とよく似ています。
(fn (defn <関数名> [<仮引数名> ...] [<仮引数名> ...] 処理1 処理1 処理2 処理2 ・・・ ・・・ 処理M) 処理M) (1) 無名関数 (2) defn による関数定義 図 : 無名関数の構文
defn と関数名の代わりに fn を書かけば、あとは defn と全く同じです。実は、defn で定義する関数の実体がこの無名関数なのです。次の例を見てください。
user=> (fn [x] (* x x)) #object[user$eval167$fn__168 0x46866946 "user$eval167$fn__168@46866946"] user=> (def square (fn [x] (* x x))) #'user/square user=> (square 10) 100
Clojure など関数型言語の場合、無名関数を評価すると関数を表すデータになります。 これを「クロージャ (closure)」といいます。クロージャには重要な働きがあるのですが、ここではクロージャは関数を表すデータ型のひとつと考えてください。クロージャについては次回で詳しく説明します。
そして、このクロージャを defn で変数に束縛すれば、関数を定義することができるのです。これは Scheme でも同じです。Scheme プログラマの中には、ラムダ式による関数定義を好む人も多いようです。また、次のように無名関数をリストの先頭要素にもってくれば、それを評価することができます。
user=> ((fn [x] (* x x)) 2) 4
ところで、Clojure には無名関数の略記法 #( ... )があります。#() の中に処理を記述します。この中で % から始まる記号が引数を表します。
簡単な例を示しましょう。
user=> (#(* % %) 10) 100 user=> (#(+ %1 %2) 10 20) 30 user=> (#(apply + %&) 10 20 30 40 50) 150
高階関数で無名関数を使うのは便利で、なおかつ、その場で実行される処理内容が確認できるという利点があります。しかし、同じ無名関数があちこちに現れるようでは問題があります。プログラムに変更はつきもので、もし、その処理を変更しようとした場合、該当するラムダ式をすべて修正しなくてはなりません。これは面倒なことですね。この場合は、defn で関数を定義した方がよいのです。そうすると、その関数を修正するだけで済みます。
Lisp では、2 つのデータが等しいか判定する述語に eq, eql, equal などがあります。Scheme では eq?, eql, equal? を使います。Clojure は = (not=), compare, identical? を使います。
identical? は 2 つの引数がまったく同じであるか調べる述語です。identical? は 2 つのデータが存在するメモリアドレスを比較します。Lisp の eq、Scheme の eq? と同じです。Lisp 言語の場合、データはすべてメモリ領域の中に格納されています。したがって、同じメモリアドレスであれば同一のデータであることがわかります。
Lisp / Scheme の場合、同じ名前のシンボルは、処理系の中でひとつしか存在しないので、eq (eq?) を使えば同じシンボルか判断することができます。ところが、Clojure のシンボルはそうではありません。たとえば、REPL で 'a と入力するたびに、a という名前のシンボルが新しく生成されるのです。実際に試してみましょう。
user=> (identical? 'a 'a) false user=> (def x 'a) #'user/x user=> (identical? x x) true
REPL で (identical? 'a 'a) を評価しても false になります。シンボル a を変数 x にセットし、(identical? x x) を評価すると、当然ですが true になります。
数値や文字列の場合、基本的には同じ値でも異なるデータが生成されるので、identical? は false を返します。ですが、Clojure は小さな整数値をキャッシングしたり、入力された文字列を再度使用することがあり、identical? が true を返す場合もあります。
user=> (identical? 1 1) true user=> (identical? 1.0 1.0) false user=> (identical? "abc" "abc") true user=> (str 1) "1" user=> (identical? (str 1) (str 1)) false
str は引数を文字列に変換する関数です。同じ名前のキーワードを identical? に渡すと、必ず true を返します。
(identical? :abc :abc) true (identical? :abc :xyz) false (identical? :xyz :xyz) true
述語 = は Scheme の equal? と似ています。= が真を返す条件を以下に示します。
簡単な例を示します。
(= 'a 'a) => true (= 'a 'b) => false (= 6.0 6.0) => true (= 6 6.0) => false (= '(a 1 2) '(a 1 2)) => true (= '(a 1 2) '(a 1 2.0)) => false (= "abcdef" "abcdef") => true (= "ABCDEF" "abcdef") => false
compare x y は、x < y ならば負の整数値を、x と y が等しい場合は 0 を、x < y ならば正の整数値を返します。x, y が数の場合は値を比較します。それ以外の場合、x, y のデータ型が異なったり、リスト同士の比較はエラーになります。ベクタ同士を比較することは可能です。
(compare 'a 'a) => 0 (compare 'a 'b) => -1 (compare 'b 'a) => 1 (compare 6.0 6.0) => 0 (compare 6 6.0) => 0 (compare "abcdef" "abcdef") => 0 (compare "ABCDEF" "abcdef") => -32 (compare [0 1 2] [0 1 2]) => 0 (compare [0 1 2] [0 1 2.0]) => 0 (compare [0 1 2] [0 1 2 3]) => -1 (compare 'a 1) => エラー (compare '(0 1 2) '(0 1 2)) => エラー
Lisp / Scheme の関数 member x xs は、リスト xs の中から x と等しい要素を探します。見つけた場合、その要素以降のリストを返します。見つからない場合は偽を返します。member は Lisp の伝統的な関数の一つです。member も末尾再帰で簡単にプログラムすることができます。
リスト : リストの探索 (defn member [x xs] (cond (not (seq xs)) false (= (first xs) x) xs :else (recur x (rest xs))))
再帰呼び出しするとき、引数 xs に rest を適用するところは今までと同じです。再帰呼び出しの停止条件は、xs が空リストか、それとも (first xs) が x と等しい場合です。等値の判定には述語 = を使っています。条件を満たしていたら引数 xs を返します。
それでは実行してみましょう。
user=> (member 'a '(a b c d e)) (a b c d e) user=> (member 'c '(a b c d e)) (c d e) user=> user=> (member 'e '(a b c d e)) (e) user=> (member 'f '(a b c d e)) false user=> (member '(c d) '((a b) (c d) (e f))) ((c d) (e f)) user=> (member '(c D) '((a b) (c d) (e f))) false
ところで、リストに同じ要素があるか調べる場合、ある条件を満たす要素を探したいこともあります。このような場合、リストを探索する関数を高階関数として定義しておくと便利です。Scheme の member には要素を比較する関数を第 3 引数に渡すことができます。ここでは Clojure の勉強のため、以下に示す関数を実際に作ってみましょう。
find-if pred xs => 要素 position-if pred xs => 位置 count-if pred xs => 個数
これらの関数は Common Lisp より拝借しました。最初は関数 find-if を作ります。find-if はリストの中から pred が真を返す最初の要素を返します。find はリストの先頭から順番に探索します。見つからない場合は false を返します。プログラムは次のようになります。
リスト : リストの探索 (defn find-if [pred xs] (cond (not (seq xs)) false (pred (first xs)) (first xs) :else (recur pred (rest xs))))
リスト xs の要素を先頭から順番に調べていきます。xs が空リストの場合は探索失敗なので false を返します。リストの要素に pred を適用して真となる場合はその要素を返します。そうでなければ次の要素を調べます。
簡単な使用例を示します。
user=> (find-if odd? '(1 2 3 4 5 6)) 1 user=> (find-if #(< 3 %) '(1 2 3 4 5 6)) 4 user=> (find-if neg? '(1 2 3 4 5 6)) false
次は find-if と似ていますが、見つけた要素の位置を返す関数 position-if を作ります。次のリストを見てください。
リスト : 見つけた要素の位置を返す (defn position-if [pred xs] (loop [xs xs i 0] (cond (not (seq xs)) -1 (pred (first xs)) i :else (recur (rest xs) (inc i)))))
position-if はリストの中から pred が真となる最初の要素の位置を返します。局所変数 i で要素の位置を求めます。pred が真となる要素を見つけたら i を返します。次の要素を調べるときは、変数 i の値を +1 することをお忘れなく。
簡単な使用例を示しましょう。
user=> (position-if even? '(1 2 3 4 5 6)) 1 user=> (position-if odd? '(1 2 3 4 5 6)) 0 user=> (position-if #(< 6 %) '(1 2 3 4 5 6)) -1
最後に、述語が真となる要素を数える関数 count-if を作ります。reduce を使わなくても末尾再帰でプログラムを作ることができます。次のリストを見てください。
リスト : 述語が真となる要素を数える (defn count-if [pred xs] (loop [xs xs c 0] (cond (not (seq xs)) c (pred (first xs)) (recur (rest xs) (inc c)) :else (recur (rest xs) c))))
pred が真となる要素を局所変数 c でカウントします。find-if, position-if と違って、リストを最後まで調べることに注意してください。リスト xs が空リストの場合、変数 c の値を返します。loop を再帰呼び出しする場合、pred が真のときは c を +1 して、そうでなければ c の値はそのままです。
簡単な使用例を示しましょう。
user=> (count-if even? '(1 2 3 4 5 6 7)) 3 user=> (count-if odd? '(1 2 3 4 5 6 7)) 4 user=> (count-if neg? '(1 2 3 4 5 6 7)) 0
ここでもうひとつ、Lisp / Scheme らしいデータ構造を紹介しましょう。「連想リスト (association list : alist)」はペア (2 つの要素を格納したリスト) を要素とするリストです。ペアの第 1 要素がキーで、第 2 要素がデータに対応します。次の図を見てください。
┌───┬───┬───┬──→ データ │ │ │ │ 連想リスト => ((a b) (c d) (e f) (g h)) │ │ │ │ └───┴───┴───┴──→ キー 図 : 連想リストの構造
上図の場合、a, c, e, g がキーで、b, d, f, h がデータとなります。キーやデータはシンボル以外の S 式でもかまいません。Lisp / Scheme で連想リストからデータを探索する関数が assoc です。
assoc key alist
assoc は member と同様に Lisp の伝統的な関数です。assoc は連想リスト alist から key と等しいキーを探します。見つからない場合は false を返します。等値関係のテストは = を用います。
Clojure には連想配列 (Map) というデータ構造があるので、連想リストを使用することはあまりないと思われます。実際、Clojure にも assoc がプリミティブとして定義されていますが、Lisp / Scheme の assoc とは異なる機能です。ここでは Clojure の勉強として、連想リスト用の関数を作ってみましょう。
リスト : 連想リストの探索 (defn my-assoc [key alist] (cond (not (seq alist)) false (= key (first (first alist))) (first alist) :else (recur key (rest alist))))
関数名は my-assoc としました。alist が空リストの場合は false を返します。次の条件部で key と連想リストのキー (first (first alist)) を述語 = で比較します。真を返す場合は (first alist) を返します。値ではなくペアを返すことに注意してください。最後に recur で次のペアを探します。
簡単な実行例を示しましょう。
user=> (def z '((a b) (c d) (e f) (g h))) #'user/z user=> (my-assoc 'a z) (a b) user=> (my-assoc 'g z) (g h) user=> (my-assoc 'h z) false
正常に動作していますね。
Clojure でリストを操作する場合、高階関数だけではなく「リスト内包表記 (List Comprehensions)」を使うことができます。基本的な構文を次に示します。
(for [v seq ...] expr)
内包表記はマクロ for 使います。for は seq から要素を順番に取り出して変数 v にセットし、最後の式 expr を評価した結果をリストにセットします。変数とシーケンスは doseq と同様に複数指定することができます。また、キーワードを使って内包表記の動作を制御することができます。よく使われるキーワードを以下に示します。
簡単な使用例を示します。
user=> (for [x '(1 2 3 4 5)] (* x x)) (1 4 9 16 25) user=> (for [x '(1 2 3 4 5) :when (even? x)] (* x x)) (4 16) user=> (for [x '(1 2 3 4 5) :let [y (* x x)] :when (odd? y)] y) (1 9 25) user=> (for [x '(1 2 3 4 5) :let [y (* x x)] :while (< y 10)] y) (1 4 9) user=> (for [x '(1 2 3) y '(a b c)] (list x y)) ((1 a) (1 b) (1 c) (2 a) (2 b) (2 c) (3 a) (3 b) (3 c))
今回はここまでです。簡単に復習しておきましょう。
次回は「クロージャ」について説明します。
次に示す関数を定義してください。
リスト : 関数 fn を適用した結果の合計値を求める (defn sum-of [fn s e] (loop [s s a 0] (if (> s e) a (recur (inc s) (+ a (fn s))))))
user=> (sum-of (fn [x] x) 1 10) 55 user=> (sum-of #(* % %) 1 10) 385 user=> (sum-of #(* % % %) 1 10) 3025
リスト : 関数 fn を適用した結果をリストに格納して返す (defn tabulate [fn s e] (if (> s e) '() (cons (fn s) (tabulate fn (inc s) e))))
user=> (tabulate (fn [x] x) 1 10) (1 2 3 4 5 6 7 8 9 10) user=> (tabulate #(* % %) 1 10) (1 4 9 16 25 36 49 64 81 100) user=> (tabulate #(* % % %) 1 10) (1 8 27 64 125 216 343 512 729 1000)
リスト : 関数 fn を適用するだけ (結果は捨てる) (defn for-each [fn xs] (when (seq xs) (fn (first xs)) (recur fn (rest xs))))
(for-each println '(1 2 3 4 5)) 1 2 3 4 5 nil
リスト : 関数 fn にリストを渡すマップ関数 (defn maplist [fn xs] (if-not (seq xs) '() (cons (fn xs) (maplist fn (rest xs))))) (define (maplist fn xs) (if (null? xs) '() (cons (fn xs) (maplist fn (cdr xs)))))
maplist は関数 fn にリスト xs をそのまま渡します。そして、再帰呼び出しするとき xs に rest を適用します。
user=> (maplist (fn [x] x) '(a b c d e)) ((a b c d e) (b c d e) (c d e) (d e) (e)) user=> (maplist count '(a b c d e)) (5 4 3 2 1)
リスト : 累積値リストの生成 (1) (defn scan-left [fn a xs] (if-not (seq xs) (list a) (cons a (scan-left fn (fn (first xs) a) (rest xs)))))
user=> (scan-left + 0 '(1 2 3 4 5 6 7 8 9 10)) (0 1 3 6 10 15 21 28 36 45 55) user=> (scan-left cons '() '(a b c d e)) (() (a) (b a) (c b a) (d c b a) (e d c b a)) user=> (scan-left * 1 '(1 2 3 4 5 6 7 8 9 10)) (1 1 2 6 24 120 720 5040 40320 362880 3628800)
scan-left はリストの最後の要素が最終の累積値になります。xs が空リストのとき、累積変数 a の値をリストに格納して返します。そうでなければ、scan-left を再帰呼び出しして、その返り値に累積変数 a の値を追加して返します。scan-left を再帰呼び出しするときは、関数 fn を呼び出して累積変数の値を更新することに注意してください。
リスト : 累積値リストの生成 (2) (defn scan-right [fn a xs] (if-not (seq xs) (list a) (let [ys (scan-right fn a (rest xs))] (cons (fn (first xs) (first ys)) ys))))
user=> (scan-right + 0 '(1 2 3 4 5 6 7 8 9 10)) (55 54 52 49 45 40 34 27 19 10 0) user=> (scan-right cons '() '(a b c d e)) ((a b c d e) (b c d e) (c d e) (d e) (e) ())
scan-right はリストの先頭の要素が最終の累積値、最後の要素が初期値になります。リスト xs が空リストの場合は (list a) を返します。そうでなければ、scan-right を再帰呼び出しします。このとき、累積変数 a の値は更新しません。返り値のリストは変数 ys にセットします。この ys の先頭要素が一つ前の累積値になるので、この値と xs の要素を関数 fn に渡して評価します。あとは、fn の返り値を ys の先頭に追加して返せばいいわけです。
リスト : 連想リストの生成 (defn pairlis [ks vs alist] (if (and (seq ks) (seq vs)) (cons (list (first ks) (first vs)) (pairlis (rest ks) (rest vs) alist)) alist)) (define (pairlis ks vs alist) (if (or (null? ks) (null? vs)) alist (cons (cons (car ks) (car vs)) (pairlis (cdr ks) (cdr vs) alist))))
user=> (pairlis '(a b c d e) '(1 2 3 4 5) '()) ((a 1) (b 2) (c 3) (d 4) (e 5)) user=> (pairlis '(a b c d e) '(1 2 3 4 5) '((x 10) (y 20))) ((a 1) (b 2) (c 3) (d 4) (e 5) (x 10) (y 20))