図書館で Scheme修行
を借りてきた。
冒頭に出てくる two-in-a-row? 関数(リストの中にあるアトムが2回連続して出てくるかをチェックする関数)を参考に、同じことをする関数でもいろいろと書き方があるってのを見てみよう。
Case A
;; 同じリストの中に同じアトムが2回連続して現れるか ;; two-in-a-row-a? が判定のために is-first-a? を使うバージョン ;; 再起するのは two-in-a-row? だけ (defun two-in-a-row-a? (lat) (cond ((null lat) nil) ((is-first-a? (car lat) (cdr lat)) t) (t (two-in-a-row-a? (cdr lat))))) (defun is-first-a? (a lat) (cond ((null lat) nil) (t (equal (car lat) a))))
これが多分最初にぱっと思いつくやり方。
補助関数を書いてコードをスッキリさせるというのはよいことだ。
Case B
;; is-first-b? に判断させて、 ;; 必要なときだけ two-in-a-row-b? に戻るバージョン ;; どちらの関数も自己再帰しない (defun two-in-a-row-b? (lat) (cond ((null lat) nil) (t (is-first-b? (car lat) (cdr lat))))) (defun is-first-b? (a lat) (cond ((null lat) nil) (t (or (equal (car lat) a) (two-in-a-row-b? lat)))))
自己再帰を避けて、お互いに引数のリストを消化しながら投げ合うバージョン。
これは個人的に結構面白い発想だと思う。
Case C
;; two-in-a-row-c? は two-in-a-row-d? を ;; 2引数で呼び出すだけ ;; two-in-a-row-d? が自己再帰して答えを出すバージョン (defun two-in-a-row-c? (lat) (cond ((null lat) nil) (t (two-in-a-row-d? (car lat) (cdr lat))))) (defun two-in-a-row-d? (a lat) (cond ((null lat) nil) (t (or (equal (car lat) a) (two-in-a-row-d? (car lat) (cdr lat))))))
丸投げ方式。
最初に呼ばれる two-in-a-row-c? は受付だけして、実際の仕事は全部 two-in-a-row-d? にやってもらう。
Case ひとまとめ
;; two-in-a-row-c?/two-in-a-row-d? を元に ;; labels を使ってひとつに纏めてみる ;; (labels を使ってるのは自己再帰する関数を定義するため) (defun two-in-a-row-l? (lat) (labels ((fn (a lat) (cond ((null lat) nil) (t (or (equal (car lat) a) (fn (car lat) (cdr lat))))))) (fn (car lat) (cdr lat))))
Common Lisp の仕様で、関数内に自己再帰型の関数を定義するときは labels を使う。
関数定義をネストさせないなら、
;; 引数の順番は逆になっちゃうけど、 ;; &optional を使って自己再帰だけで定義する方法 (defun two-in-a-row-op? (lat &optional (a '())) (cond ((null lat) nil) ((equal (car lat) a) t) (t (two-in-a-row-op? (cdr lat) (car lat)))))
さらに、
;; cadr = (car (cdr lat)) = second を使って ;; シンプルにまとめたバージョン (defun two-in-a-row-cadr? (lat) (cond ((null lat) nil) ((equal (cadr lat) (car lat)) t) (t (two-in-a-row-cadr? (cdr lat)))))
car を first、cdr を rest、cadr を second に書き直すとより意味が通じやすくなる。
(defun two-in-a-row-nth? (lat) (cond ((null lat) nil) ((equal (second lat) (first lat)) t) (t (two-in-a-row-nth? (rest lat)))))
実行結果は皆同じ
全ての Case で実行結果はこうなる。
(two-in-a-row-*? '(1 2 3 4 5)) => NIL (two-in-a-row-*? '(1 2 3 3 4 5)) => T
コードは全て Common Lisp で書いてみたけど、「Scheme手習い」「Scheme修行」共に中身は Scheme で書いてあって、Common Lisp で言語仕様が異なる部分だけ注釈が入るという形式を採っている。
Lisp を練習するならすごく勉強になる本なので、Schemmer も Lisper も一読する価値はあると思う。
余談
結果が同じになる(所謂、正規化された)仕事でも、任せる人によってやり方は様々だ。
ある人は効率化を追い求め、ある人は単純だがベタなやり方を好み、ある人は生産性よりもわかり易さやメンテナンスのし易さにこだわるかもしれない。
大事なのは、結果的に得られるものが正確で、ちゃんと期待した時間内に成果が上がるなら、仕事を任せられた人それぞれのやり方の違いには寛容になるべきだ。
細かなエラーまで忌み嫌って、仕事のやり方をいちいちルールで縛り付けていたら、多様性が失われると同時に創造性も失われてしまう(平たく言えば仕事が楽しいものではなくなってしまう。仕事は楽しいから続けられるのだ!)。
見たこともないやり方だからという偏見だけで「仕事ができない奴」「空気を読めない奴」なんていうレッテルを貼らないでほしい。
「偏見」とは実体や本質や結果を見届けずに勝手な判断を下してしまうことだ。
仕事を他人に任せることができるっていう、つまり責任ある立場に立ったなら、任せた仕事の成果にたどり着くまでの多様なやり方については広い心を持って受け入れよう。