Programmer's Note

コード読み書きの備忘録。

recordを使ってデータ構造をつくる

SICPまじ楽しいな。この本は、一読して内容を理解するだけじゃなくて、手を動かしてコードを写経したり練習問題をやると、純粋にプログラミングの楽しさを味わえる。本質的な部分(パラダイム)を濃密に扱っているので、数学と一緒で、手を動かして思考を追う過程に悦びがある。

まだ2章の途中だが、データ構造をLISPの流儀で定義するところが、考え方としてはすごくシンプルでエレガント。 しかし、何度味わっても美味しい。(いや、逆にシンプルゆえに美味しいのかもしれない)

手続き型言語オブジェクト指向言語みたいにstructやclassを使ってデータ構造そのものを定義するのではなく、関数というインタフェースだけを使って、データの抽象概念を見せる。

さて、練習問題2.2では、線分を作ってその線分の中点を求めるために、点、線分を扱う関数を定義する。 使う側からすると以下のようになる。

(def seg (make-segment (make-point 10 20)
                       (make-point 20 30)))

(x (midpoint-segment seg)) ; -> 15
(y (midpoint-segment seg)) ; -> 25

OOPっぽく書けば

seg = Segment.new( Point.new(10,20), Point.new(20, 30))

seg.midpoint-segment.x  # -> 15
seg.midpoint-segment.y  # -> 25

(x-point (midpoint-segment seg))seg.midpoint-segment.x-pointは順番が逆になっただけで、記述量に違いはない。 しかし、考え方が真逆なのが、やはり面白い。

関数の定義は、SICP流に従ってClojureで書くと下記のとおりになる。

(defn make-point [x y] (list x y))
(defn x [p] (first p))
(defn y [p] (last p))

(defn make-segment [start end] [start end])
(defn start [s] (first s))
(defn end [s] (last s))

(defn midpoint-segment [s]
  (let [ss (start s)
        es (end s)]
    (make-point (/ (+ (x ss) (x es)) 2)
                (/ (+ (y ss) (y es)) 2))))

これをClojureのrecordと使うと、ぐっと簡単に書ける。 midpoint-segmentはデストラクテャリングを使って記述を短くしている。

(defrecord Point [x y])
(defrecord Segment [start end])

(defn midpoint-segment 
  [{:keys [start end]}]
    (->Point (/ (+ (:x start) (:x end)) 2)
             (/ (+ (:y start) (:y end)) 2)))

recordは内部的にはJavaのclassそのもので、データ構造を定義できて、なおかつClojureで定義したものをJavaの世界からでも参照できる。 まさにJavaとのinteroperabilityのための機能だ。

recordのいいところはmapと同等の機能を持つ点で、要素を取り出すための関数定義xystartendが不要で、 defrecord時の引数に渡す要素名がそのままkeyとして使える。

さらに、生成関数を自前で定義する必要ないという点もある。 (->Segment)と書けばSegment.new()に相当する。(Segment.)と書いても動作する。

このJavaからのいいとこ取り、しかもwell-designedなところが、Clojureのセクシーなところだ。

最初の使用例を書き直すと下記になる。

(def seg (->Segment (->Point 10 20)
                    (->Point 20 30)))

(:x (midpoint-segment seg)) ;-> 15
(:y (midpoint-segment seg)) ;-> 25

最後の2行は、スレッディングマクロを使えば、さらにOOPっぽくかける。

(-> seg midpoint-segment :x) ;-> 15
(-> seg midpoint-segment :y) ;-> 25

以上。

SICP第一章読了

SICP第1章を読了した。演習問題もコンプリート! 言語はSchemeではなくClojureを使ったのでClojureのいい練習にもなった。

振り返ると、プログラミングの教材として、構成が実によく考えられていて感動すら覚える。 一級品の少年漫画やRPGなみのストーリー展開を思わせる(笑)。 序盤の伏線がクライマックスで花開く、みたいな。

しょっぱなの入り口では、平方根を求める関数を、ニュートン法の考え方を使って、 丁寧に要素に分解して関数を階層化してプログラムを作っていく。 関数型プログラミングのまさにお手本を示している。

クライマックスにおいては、まったく同じ問題を、高階関数を使うことでより高いレベルで解いている。 プログラミングの道具としては「関数を引数に渡す」「関数を返り値にする」機能を使うことで、 もののみごとに平方根を求める処理が、汎用的な抽象概念の組み合わせによって表現できることを示す。

平方根をもとめることとは何か? それは、y = x/yの平均減衰(average damp)を使って不動点(fixed point)を求めるに等しい。

(defn sqrt [x]
  (fixed-point (average-damp (fn [y] (/ x y)))
               1.0))

まさにコードが体を表している。

上記はSchemeではなくClojureで書いたコードだが、 この本がプログラミング言語LISPを選んだ理由がよくわかる。 問題を解くための手続きを、これ以上直接的かつ簡潔に表現できる言語は他には無いと思う。 余計なものはいっさいない。表現したいことをダイレクトに表現できる。

読んでいると、この本のプログラムで何気なくやっていることが、他の言語だと難しいだろうなと感じることも多々ある。 Schemeは実にシンプルで強力だな!とほれぼれする。

いまのところClojure以外はやる気はしないのだが、Schemeがそれに匹敵するものだとよく分かった。 結局、動作環境(プラットホーム)が一番重要なんだな。ClojureJVMを選んで大正解だと思う。

しかし、SICPClojureで十分幸せなプログラミングの時間を過ごせるな。SICPで数学自体の面白さも味わえるし。 まあ、この本はむしろ数学のバックボーンがしっかりしている学生にプログラミングを教えるための教材ではあるが。

Clojureでパスカルの三角形

SICP遅々と読む。練習問題にパスカルの三角形の要素を計算せよ(再帰を使って)、というのがある。 フィボナッチ数列の次くらいに手軽な題材でいろいろ試せて遊べるなと。

Clojureらしい解き方は、やっぱ遅延シーケンスを作るやり方だろうなと。 出力形式は問われていないから、任意の行数までの計算結果を返すとしよう。

=> (take 5 (pascal-triangle))
([1] [1 1] [1 2 1] [1 3 3 1] [1 4 6 4 1])

=> (nth (pascal-triangle) 5)
[1 5 10 10 5 1]

コード:

(defn next-row
    [row]
    (letfn [(add-1-both-ends [coll] (concat [1] coll [1]))]
      (->> (partition 2 1 row)
           (map (fn [[a b]] (+ a b)))
           add-1-both-ends
           vec)))

(defn pascal-triangle
  ([] (pascal-triangle [1]))
  ([col] (lazy-seq (cons col (pascal-triangle (next-row col))))))

next-rowパスカル三角形の特定の行の数列が与えられたとして、その次の行の数列を返す。

=> (next-row [1 1])
[1 2 1]
=> (next-row [1 2 1])
[1 3 3 1]

この関数の実装方法としては、以前「プログラミングClojure」で衝撃的だったフィボナッチ数列の解き方に習って、 いったん与えられた数列の形を扱いやすい形に変えている。

[1 3 3 1] → [[1 3] [3 3] [3 1]]

これで再帰を使わなくてもmapを使って要素の計算ができる。 partitionという関数を使えば、上記は簡単に出来る。

=> (partition 2 1 [1 3 3 1])
((1 3) (3 3) (3 1))

仮にcore関数のpartitionを使わずに、似たことをやるとしたら以下の関数を作れば良い。

(defn my-partition
   [[a b :as col]]
   (when (and a b)
     (cons [a b] (my-partition (rest col)))))

デストラクチャリングを使って要素を取り出して再帰するClojureのエレガントさを味わえるので、これはこれで楽しい。

pascal-triangle関数も、遅延シーケンスを使わずに再帰を使って、しかもStack Overflowを起こさない末尾最適可能なように作るとしたら、下記になるかなと。(中で使っているnext-rowは変わらない)

(defn pascal-triangle
  ([n] (pascal-triangle n [1] []))
  ([n a b]
    (if (zero? n)
      b
      (recur (dec n) (next-row a) (conj b a)))))

遅延シーケンスほど使い回しは効かなくなるが、

=> (pascal-triangle 5)
([1] [1 1] [1 2 1] [1 3 3 1] [1 4 6 4 1])

という感じになる。

この小さい題材の実装でもClojureの以下の機能が便利すぎて、

  • シーケンスを扱う高階関数(map, partition)
  • 引数個数ごとに関数の処理を分けて定義できる(multi-arity)
  • デストラクチャリングによる引数の分解
  • スレッディングマクロ(->>)

これらが無い世界で暮らすのが辛い(笑)。

続Clojureのdestructuringメモ

前回「 Clojureのdestructuringメモ - Programmer's Note 」の続き。

マップのdestructuring

文字列をキーに使った場合を試す
(def mab {"a" 1 "b" 2})
(let [{:strs [a b]} mab]
    (println b "," a))
; 結果 => 2,1

Goood!!

下記非短縮形もOK

(def mab {"a" 1 "b" 2})
(let [{x "a" y "b"} mab]
  (println x y))
シンボルがキーの場合
(def mcd {'c 1 'd 2})
(let [{:syms [c d]} mcd]
    (println c "," d))
; 結果 => 1,2

Goooooood!!

下記非短縮形もOK

(def mcd {'c 1 'd 2})
(let [{x 'c y 'd} mcd]
  (println y "," x))
さて、マップの入れ子構造も試してみる
(def dat {:name "boo"
          :body {:weight 60
                 :height 160 }
          :money 100})
(let [{{h :height} :body} dat]
  (println h))
; 結果 => 160
ベクターとマップの組み合わせ!

マップの中にベクターがある場合。

(def dat {:name "boo"
          :body [60 160]
          :money 100})
(let [{[_ height] :body} dat]
  (println height))
; 結果 => 160

ベクターの中にマップがある場合。

(def dat ["boo"
          {:weight 60 :height 160}
          100])
(let [[_ {h :weight} _] dat]
  (println h))
; 結果 => 60

なんつービューティフルな言語なんだ!

関数に引数をマップで与える場合の省略形

たとえば関数にパラメータでマップを渡したい場合、

(boo "foo" {:weight 50 :height 120})

にするが、これを

(boo "foo" :weight 50 :height 120)

のように書ける!

前者の関数定義は

(defn boo [name opts]
  (let [{w :weight h :height} opts]
    (println name ": (" w "," h ")")))

で、後者は

(defn boo [name & opts]
  (let [{w :weight h :height} opts]
    (println name ": (" w "," h ")")))

違いは引数の定義に&を使っているだけ! defnは実はマクロで、& optsと定義したときは、残りの引数をまとめてシーケンスに入れてくれる。 さらにシーケンスはベクターにもマップにも展開できる。 シーケンスという一個上位の抽象概念をかますと、物事をシンプルに扱える。美しい。

destructuringは関数の引数定義[]でも同じように使えるから、後者はこのようにも書ける。

(defn boo [name & {w :weight h :height}]
    (println name ": (" w "," h ")"))

いやあClojure最高です。

Clojureのdestructuringメモ

Clojureのdestructuringはベクターやmapの中から要素を取り出すのに、とても便利な機能。

さて、Clojure - Destructuring in Clojureに書いてあることなのだが、手を動かして確かめてみる。

使えるところ

関数

(defn foo [] (...))

無名関数

(fn [] (...))

let文

(let [] (...))

引数がベクターのとき

通常の使用方法
(def a [1 2])
(let [[x y] a]
      (println y "," x))
; 結果 => 2,1
展開もとの要素数が足りない場合は、nilが代入される
(def b [1])
(let [[x y] b]
      (println y "," x))
; 結果 => nil,1
_を使って一部のみを取り出す
(def a [1 2])
(let [[_ y] a]
      (println y))
; 結果 => 2
ベクターの入れ子もOK
(def abc [1 [2 3]])
(let [[x [y z]] abc]
  (println z "," y "," x))
; 結果 => 3,2,1
ってどこまでOKなんだろう?
(def abcdefg [1 [2 3 [4 5 [6 7]]]])
(let [[_ [_ _ [_ _ [x y]]]] abcdefg]
  (println  x "," y))
; 結果 => 6,7

ひゅ〜、かっこいいね。

:asを使って全体も扱う
(def abc [1 2 3])
(let [[x y :as z] abc]
    (println  x "," y "<=" z))
; 結果 => 1,2 <=[1,2,3]
&を使って要素の一部を残りとして扱う
(def abc [1 2 3])
(let [[x & y] abc]
    (println  x "," y))
; 結果 => 1,(2 3)

引数がmapのとき

個別にkeyを指定して取り出す
(def mab {:a 1 :b 2})
(let [{x :a y :b} mab]
  (println y "," x))
; 結果 => 2,1

展開時の{}の書き方は、keyと要素が逆順、おしゃれ。

個別にkeyを指定して取り出す、特定のものだけ
(def mab {:a 1 :b 2})
(let [{z :b} mab]
  (println z))
; 結果 => 2

ふーむ、便利。

存在しないkeyを指定した場合はnilが代入される
(def mab {:a 1 :b 2})
(let [{z :c} mab]
  (println z))
; 結果 => nil
短縮形
(def mab {:a 1 :b 2})
(let [{:keys [a b]} mab]
  (println a "," b))
; 結果 => 1,2

:keysにkeywordの一覧を渡す。keywordから':‘を除いたものだね。これは要素がたくさんあるときは簡潔に表現できて便利。

mapのkeyにkeywordを使った場合はこのkeysで指定できるが、keyに文字列やシンボルを使った場合は、:strs:symsを使えば同じようにできる。すばらしすぎるな。

デフォルト値を入れることができる!
(def mab {:a 1 :b 2})
(let [{x :a, y :b, z :c :or {z 2}} mab]
    (println x "," y, "," z))
; 結果 => 1,2,2

:cはもともと定義したmapにはない要素だが、デフォルト値2を設定。

短縮形では、

(def mab {:a 1 :b 2})
(let [{:keys [a b c] :or {c 2}} mab]
  (println a "," b, "," c))
; 結果 => 1,2,2

さらにmapの場合にも:asが使える。

今日はここまで。

世界観と世界を変える一冊

ほぼ一年ぶりの更新か^^;

さて、「7つの言語7つの世界」を読了。一通り全言語のエッセンスは学べたかな。(Prolog , Haskellは最後はちょっと飛ばしたが、もう少し知りたい時に読めばいいかなと)
章順ではなく気になる言語から読んでいって、途中ブランクがだいぶ空いて、気分が向いたら別の言語を読んでみる。というやり方で足がけ1年くらいかかっただろうか。
全部読み通した今、改めてこの本は素晴らしい本だなと思う。プログラマとして確実にレベルアップできる貴重な教材だ。
冒頭を読み返すと、最初はピンと来なかった概要が実によく分かるようになったし、構成も実はOOPから関数型プログラミングへ、一本の道筋になっている。というところに感動する。
今まさに、この本をガイドに未知の土地に旅した後、その余韻に浸っている感じだ。
最後まで楽しく読めたのは、やはり著者の力量が大きい。それぞれの言語の良い点悪い点が実に分かりやすく的確に書いている。映画の人物になぞらえたメタファも楽しい。
この本でClojureを知ったおかげで、関数型プログラミングLISPの世界に入れて随分と視野が広がった。
7つの言語それぞれユニークなポジションにいるが、言語パラダイムの観点からあえて3つ選ぶとしたら、Ruby(OOP)、Prolog(論理型)、Clojure(関数型)かな。ここはもう個人の好みだが。
実はすでに次作の「Seven more languages in seven weeks」を購入したが、冒頭のElixir言語作者のprefaceを読んで驚いた。Elixirはこの本がなかったら生まれていなかった。
一冊の本が新しい言語を生んだのか。確かにこの本を読んで世界観は変わるが、実際に世界をも変えていたとは!

RubyとLispとClojureのコードの読みやすさ

なぜRubyは許容可能なLISPなのか - 翡翠はコンピュータに卵を生むか」という記事が面白かった。

この記事の中でサンプルコードを使って、 プログラムの読みやすさを対比させていた部分があったので、 Clojureでやってみたくなった。

Ruby
[1,2,3].map {|n| n*n }.reject {|n| n%3==1 }
LISP
(remove-if (lambda (n) (= (mod n 3) 1))
           (mapcar (lambda (n) (* n n))
                   '(1 2 3)))

(Clojure以外のLisp系言語はよく知らないが、たぶんこれはCommon Lisp)

同じことをClojureで記述してみる

Clojureのlambdaは無名関数なので、同じことをやると以下のようになる。

(remove (fn [n] (= (mod n 3) 1)) 
                (map (fn [n] (* n n))
                     [1 2 3]))

上記LISPよりかは読みやすいが、ほぼ同等だな。

リーダーマクロ#()を使ってみると。

(remove #(= (mod % 3) 1) 
        (map #(* % %)
             [1 2 3]))

ぐっと簡潔になった。 さらに、スレッディングマクロを使うと、 Rubyメソッドチェーンのように処理の流れの通りに書ける。

(->> [1 2 3]
     (map #(* % %))
     (remove #(= (mod % 3) 1)))

一行の場合

(->> [1 2 3] (map #(* % %)) (remove #(= (mod % 3) 1)))

スレッディングマクロは、かなりプログラムの読みやすさに貢献している。 慣れてしまえばこれはRubyに匹敵する読みやすさになる。

とはいえ、しょっぱなはかなり戸惑ったけどね…。