ハツカネズミの恋

Lisp のもろもろ,おぼえがき

Clojure ことはじめ② - 関数 -

関数

Lisp

Lisp という単語が,これまで特になんの説明もなく,幾度か登場した.Lisp とは何か,という問いに率直に答えるのは難しい.Lisp に限らず,x とは何か,という問いに率直に答えるのはいつだって難しい.だからここでは,以降の内容を読み進める上で最低限知っておいた方がいいと判断した部分について,Lispを簡単に紹介しておこうと思う. まず,大方お察しのことと思うが,Lisp とはプログラミング言語だ.ただし,Lisp という名前のプログラミング言語が唯一の実体として存在するわけではない.強いて言えば 純Lisp と呼ばれるものがそれにあたるが,実用性に乏しく,Lisp の代表とするには心もとない.一般に言われる Lisp とはいわば,プログラミング言語の,ひとつのパラダイムだ.Lisp という名前は List Processer をもじったもので,もともとは単に表記法として発明された.それは,コンピュータのプログラムを数学的な正確さを失わないように記述するための表記法だった.Clojure のコードでも散々目にして,そして,とても大切な役割を担ってきた,カッコを駆使した表記法.特に’S式’と呼ばれるあの表記法こそが,当初は Lisp の本体だった. Lisp はその後すぐにプログラミング言語として実装され,冒頭で紹介した Common Lisp や,Emacs で大活躍するEmacs Lisp,よりコンパクトな仕様を持ち関数型の傾向が強い Scheme など,数々の子供たちを生んだ.Lisp の主要なアイデアを継承したこれらの派生言語を,Lisp 方言と呼ぶ.もちろん,ClojureLisp 方言だ.ではそのLisp 方言に共通する主要なアイデアとはどんなものだろう.様々な意見があるだろうが,多くの Lisper が挙げるのは,およそ以下のようなものだろう:

  • 統一的な文法の基礎としてのS式
  • マクロに代表される強力なメタプログラミングシステム
  • コードとデータを区別しない
  • リスト処理が豊富である
  • 関数型(的)プログラミングスタイル

Lisp の歴史は古く,当時MITで教鞭を執るジョン・マッカーシーの手によって生み出されたのは1958年,現役で動いているプログラミング言語としては Fortran と並んで最長老の一角だ.しかし,Lisp は流行とは無縁な言語だった.その言語仕様の簡潔さや表現力,アイデアのるつぼのような斬新さは一部の人々 (オタク) を惹きつけたが,ついぞ広く大衆に浸透する機会はなかった.その理由はいくつかあるが,カッコの多さで目がチカチカしてしまう,というのも一因かもしれない.Lisp はアカデミックな対象と見なされ,’黒板上で最も実装されている言語’ なんていう皮肉まであったほどだ.逆に言えば,仕組みや理論を奉じるオタク達からの支持は厚く,Lisp を素材にした素晴らしいコンピュータサイエンスの教科書も数多く生まれた.そして,こんな風に今も我々プログラマの目の前で元気に動いている.それも,登場した時の美しい姿を保ったままで.

関数

Lisp に心惹かれるプログラマの多くは,そのシンプルさに魅入られている.たぶん.というのは,一見して複雑な処理をしているプログラムでも,そのビルディングブロックとなる関数自体は非常にシンプルだからだ.シンプルと言えば,Clojure の作者である Rich Hickey による 'Simple Made Easy' という有名な講演がある.Clojure の設計思想に触れることのできる素晴らしい内容なのでぜひ一見をおすすめする.シンプルさを矜持とする彼が Lisp に目をつけたのは決して偶然ではないと確信するだろう.

関数呼び出し

まず,あらゆる Clojure のオペレーションは統一されたシンタックスを持っていることを思い出そう.復習だ.カッコを開いて,直後にオペレータ,続いてオペランドを並べ,カッコを閉じる.このルールは関数呼び出しの時も変わらない.なぜって,Clojure における関数呼び出しというのは,単に関数,もしくは関数式をオペレータとするオペレーションに過ぎないからだ.引数も同様に,オペレータが関数の時のオペランドを指す言葉だ. 関数式というのは,文字通り関数を返す式のこと.例えば,以下のオペレーションは関数式だ.

(or * + -)

ちょっと見た目が変に見えるけど,それは単に見慣れないというだけ.正しく評価されるし,値も返す.返ってくる値は掛け算の文字列表現だ.

(or * + -)
;=> #object[clojure.core$_STAR_ 881975570 "clojure.core$_STAR_@3491e112"]

or はオペレーション内で最初に真に評価された値を返すんだった.だから,こんなことができる.

((or * + -) 3 4)
;=> 12

この例で,(or * + -) 全体の評価結果は,最初の真値である * だ.すなわち,(* 3 4) という式が生成され,これを評価して12という結果を得ている.こうしたネスト構造は,オペレーションのシンタックスに従う限りいくらでも深くすることが可能だ.

((and (= "yes" "yes") (or * + - nil "no")) 5 6)
;=> 30

複雑化しているものの,and オペレーションの全体はあくまでも一つ外側のカッコにおけるオペレータの位置に収まっている.このことは,直後に数値リテラルオペランドが続いていることからも見て取ることができる. では,今度はあえて,オペレーションのシンタックスから外れた書き方をしてみよう.どうなるか.

("This" :shall :not :pass)
;=> Execution error (ClassCastException) 

この手のエラーはしょっちゅう目にすることになる.これは関数のつもりでオペレータの位置に置いたものが,実際は関数でなかった時に投げられる例外だ.

第一級関数

Clojure における関数の扱いは非常に柔軟だ.上で見た関数式もその一例だが,それだけには止まらない.Clojure の関数は,引数として関数を受け取り,さらに,関数を返すことができる.こうした関数のことを一般に高階関数という.こうした高階関数の扱いが可能なプログラミング言語は,関数を第一級オブジェクトとしてサポートしている,とよく言われる.第一級オブジェクトというのは要するに,言語の基本的な操作を無制限に適用できるオブジェクトのことだ.Clojure では例えば,数値リテラル,ヴェクタなどが第一級オブジェクトで,つまり関数はこれらの値と同じように処理される.ちなみに,第一級オブジェクトとオブジェクト指向のオブジェクト,これらは同じ言葉でも意味が異なるので混同しないように注意しよう. ここでは,map という関数を例にとって,Clojure が関数をどんなふうに扱っているのか観察してみよう.その前に,map と協働してもらう inc 関数を最初に紹介する.

(inc 1)
;=> 2

(inc 2.5)
;=> 3.5

簡単.引数の数値に1を足した値を返すだけだ.さて,これを map と組み合わせる.

(map inc [0 1 2 3 4])
;=> (1 2 3 4 5)

お分かりだろうか.ヴェクタ内の各要素に inc が適用され,その結果が返されて……おや,何かおかしい.map の二つめの引数はヴェクタだったはずなのに,返された値はええと……リスト? と,訝しんでいる人がいるかもしれない.でも,大丈夫,結果はこれで正しい.なぜ map が素直にヴェクタを返さないのか,その理由はあとでちゃんと説明する.きっと,そこでClojure の柔軟さに感じ入るはずなので,今はいったん’そういうもの’,として受け入れて欲しい. map はヴェクタやリストといったコレクションの変更を一般化した関数で,あらゆる関数をあらゆるコレクションに適用することができる.つまりmap を使えば,コレクションの各要素に,任意の関数をいっぺんに適用して,新しいコレクションを生成することができるんだ.戦隊モノのヒーローがヴィランを前にして一斉に変身する場面なんかを思い浮かべてみて欲しい.あれも map で表現できる.

関数定義

さて,完全な思いつきだがせっかくなので,関数を定義する上での例として戦隊ヒーローを取り上げてみたい.ちょっとこじ付けっぽい部分が出てくるかもしれないけど,そこにはどうか目を瞑って欲しい. ではまず,Clojure で任意の関数を定義するための基本的な方法から確認しよう.

(defn name
 "docstring (optional)"
  [params]
  body)

defn は関数を定義するためのマクロだ.マクロが何かは,まだ気にしなくていい.name の部分は関数の名前としてシンボルが入る."docstring (optional)" は関数に関する説明を文字列で記述する場所で,あってもなくてもいい.params はパラメータ,その関数が受け取る値の名前だ.パラメータはいくつあってもいい.body は文字通り,関数の本体を記述する.具体的なオペレーションをここに書く. 何はともあれ,まずはやってみないと始まらない.早速ひとつ,関数を定義してみよう.

(defn transform
  "transform a human into a brave soldier"
  [member]
  (str member "-Ranger"))

 (transform "Rich")
;=> "Rich-Ranger"

我々はこうしてリッチ・ヒッキーをリッチレンジャーへと変身させることに成功した.この関数は,ご覧の通り,あるメンバーを一人文字列として受け取って,その文字列に "-Ranger" という新たな文字列を合成した値を返す.では,リッチ・ヒッキーにはここでいったんご退場いただいて,悪と戦う勇敢な隊員たちを新たに定義しよう.定義にはもちろんdef を使う.

(def fighters ["Red" "Blue" "Green" "Yellow" "Pink"])

古き良きゴレンジャースタイルだ. さて,今定義したヴェクタ fighterstransformmap してみる.

(map transform fighters)
;=>("Red-Ranger" "Blue-Ranger" "Green-Ranger" "Yellow-Ranger" "Pink-Ranger")

よし!変身完了!五人揃ってゴレンジャーだ!

(def go-ranger (map transform fighters))

(println go-ranger)
;=> (Red-Ranger Blue-Ranger Green-Ranger Yellow-Ranger Pink-Ranger)

アリティ

Clojure の関数は0個以上の引数によって定義される.この,関数に渡される引数の数をアリティ (arity) と言う.Clojure ではアリティによって,関数の挙動を変えるような書き方ができる.例えばこんな感じ.

(defn zero-arity
  []
  "I take no params!")

(defn one-arity
  [x]
  (str "I take only one params :" x))

(defn two-arity
  [x y]
  (str x " and " y " are our params"))

さらに,Clojure の関数はアリティをオーバーロードすることもできる.これはつまり,引数の数(アリティ)によって実際に実行する関数の本体を場合分けできるということだ.具体例から説明すればきっとピンとくるだろう.

(defn transforming
  ([name type]
   (str "The brave fighter " name " has been transformed into " type "!!")) ;; body-1
  ([name]
   (transforming name "Rich Hickey"))) ;; body-2

(transforming "Hayata" "Ultra-Man")
;=> "The brave fighter Hayata has been transformed into Ultra-Man!!"

(transforming "Hayata")
;=> "The brave fighter Hayata has been transformed into Rich Hickey!!"

ここで定義した関数 transformimg はアリティによって呼び出す関数の本体を変えている.2アリティの場合,つまり,nametype という2つの引数が渡された場合,transformimg は body-1 を評価する.1アリティの場合は body-2 を評価する. Clojure では,任意個のパラメータを引数にとるような関数も自然に書くことができる.’これ以降の引数はリストとして処理してね’ という印として,& を用いる.さっきゴレンジャーのくだりで書いた transform をすこし融通して書き直そう.

(defn transform-all
  [& fighters]
  (map transform fighters)

こうすれば,fighter に幾つでも要素を入れられる.そして,それらの要素はリストとして処理され,まとめて fighter という名前がつけられるので,一気に処理することができる.もちろん,通常のパラメータとリスト化パラメータを混ぜて使っても問題ない.

test

実は,Clojure にはパラメータを定義するための,もっと冴えた方法がある.それは,分配束縛 (distracting) と呼ばれる方法だ.

分配束