CLOJURE for the BRAVE and TRUEでClojure学習 part3

はじめに

Clojure の学習備忘録。 今回は、Chapter 4: Core Functions in Depthの残り箇所。

今回のあつかうテーマは、以下の通り。

  • 遅延シーケンス
  • 無限シーケンス
  • コレクション関数(conj, into)
  • 代表的な高階関数(apply, partial, complement)

遅延シーケンスといっても、実際に要素にアクセスするまで全く計算しないではなく、 パフォーマンス上先行するいくつか要素も計算してしまうというのは面白い。

Lazy Seqs

mapは他のシーケンス関数と同様、引数のコレクションにseqを適用するだけではなく、遅延シーケンス を返す。
遅延シーケンスとは、要素に実際にアクセスするまで計算を行わないシーケンスのことである。 それに対してシーケンスの全要素が計算されているものは、リアライズシーケンスと呼ぶ。 必要になるタイミングまで計算を遅延させることは、より効率的なプログラミングを可能にし、無限シーケンスが構築できるようになる。

Demonstrating Lazy Seq Effciency

遅延シーケンスについて理解するために、次の例を考えよう。あなたはヴァンパイアを特定するタスクに参加しており、1 億人の容疑者の中からヴァンパイアを特定しないといけない。ヴァンパイアデータベースのサブセットはスタブ化して、次の通りである。

(def vampire-database
  {0 {:makes-blood-puns? false, :has-pulse? true  :name "McFishwich"}
    1 {:makes-blood-puns? false, :has-pulse? true  :name "McMackson"}
    2 {:makes-blood-puns? true,  :has-pulse? false :name "Damon Salvatore"}
    3 {:makes-blood-puns? true,  :has-pulse? true  :name "Mickey Mouse"}})

(defn vampire-related-details
  [social-security-number]
  (Thread/sleep 1000)
  (get vampire-database social-security-number))

(defn vampire?
  [record]
  (and (:makes-blood-puns? record)
        (not (:has-pulse? record))
        record))

(defn identify-vampire
  [social-security-numbers]
  (first (filter vampire?
                  (map vampire-related-details social-security-numbers))))

vampire-related-details は 1 秒でデータベースからレコードを見つける関数である。 vampire? はレコードがヴァンパイアかどうかを判定する関数。 identify-vampireマイナンバーとデータベースのレコードをマッピングして、はじめに一致したレコードを返す。

これらの関数の処理時間を計測するには、 time を使用すればよい。

(time (vampire-related-details 0))
; => "Elapsed time: 1001.373258 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

1 行目には、関数処理にかかった時間が出力される。
2 行目は、関数の戻り値である、データベースのレコードが出力される。

遅延が考慮されていない map ならば、全要素に対して vampire-related-details 関数を適用し、その後、 filter を適用することになる。1 億人の容疑者からヴァンパイアを見つけるならば、全要素なめるのに 1 億秒つまり 12 日必要となり、街の半分の人が死んでしまう。しかし Clojuremap は遅延を考慮されているので、そんな心配はいらない。

(time (def mapped-details (map vampire-related-details (range 0 1000000))))
; =>"Elapsed time: 0.053236 msecs"
; => #'user/mapped-details

この例では、 range によって 0 から 999,999 までの遅延シーケンスを生成し、 map が返す遅延シーケンスを mapped-details にバインドしている。しかし、 range が返すシーケンスの全要素に対して vampire-related-details はまだ適用されていないので、処理時間は 12 日もかからず、一瞬で終わる。

遅延シーケンスは、一つはシーケンスの要素をリアライズ方法が記述されたレシピと、今までリアライズされた要素の2つから構成される。 map を使用してもどの要素もまだリアライズされていないが、要素を生成するレシピはある。リアライズされていない要素にアクセスすれば、その度遅延シーケンスはレシピにあるとおり要素を料理して返すことになる。

上の例の場合、 map-detailsリアライズされていないが、map-details の要素にアクセスすれば、要素に対してレシピを適用することになり、検索時間に 1 秒かかるだろう。

(time (first mapped-details))
; => Elapsed time: 32070.326583 msecs"
; => {:makes-blood-puns? false, :has-pulse? true, :name "McFishwich"}

処理に 32 秒かかっている。1 億秒よりかはずっと速くなったが、期待しているよりも 31 秒も多くかかってしまっている。ひとつの要素にアクセスするだけなので 1 秒で済むはずなのになぜだろうか?

それは、Clojure が遅延シーケンスをチャンクに分割するからなのである。1 要素をリアライズするときに先行していくつかの要素もリアライズしている。例の場合だと、1 要素アクセスしても続く 31 要素もリアライズしたので 32 秒かかったのである。 これはパフォーマンス的な理由で行われている。

遅延シーケンスの要素の評価は、初回アクセスのときだけである。よって、もう一回 mapped-details のはじめの要素にアクセスしてもほとんど時間はかからない。

(time (first (mapped-details)))
; => "Elapsed time: 0.022 msecs"
; => {:name "McFishwich", :makes-blood-puns? false, :has-pulse? true}

遅延シーケンスについて整理できたので、はじめに書いたコードが時間かからないことも理解できるだろう

(time (identify-vampire (range 0 1000000)))
; => "Elapsed time: 32019.912 msecs"
; => {:name "Damon Salvatore", :makes-blood-puns? true, :has-pulse? false}

Infinite Sequences

遅延シーケンスの特徴を利用して、無限シーケンスも作成できる。今までシーケンスは有限であったが、Clojure では無限シーケンスを作成できる関数を用意されてる。 repeat はその一つであり、渡された引数の値からなる無限シーケンスを構築する

(concat (take 8 (repeat "na")) ["Batman!"])
; => ("na" "na" "na" "na" "na" "na" "na" "na" "Batman!")

ここでは、文字列 "na" を要素にもつ無限シーケンスを構築している。

repeatedly をつかえば、もっと柔軟な無限シーケンスを構築できる

(take 3 (repeatedly (fn [] (rand-int 10))))
; => (1 4 0)

repeatedly でできた無限シーケンスの要素は、匿名関数 (fn [] (rand-int 10)) の適用結果になる。REPL を実行するたびに、取得要素はランダムに変化する。

遅延シーケンスをリアライズする first や take といった関数は、シーケンスの次の要素がなんであるかを知らないし、取得した要素の次があるならば、ただそれを取得するだけである。

(defn even-numbers
  ([] (even-numbers 0))
  ([n] (cons n (lazy-seq (even-numbers (+ n 2))))))

(take 10 (even-numbers))
; => (0 2 4 6 8 10 12 14 16 18)

ここでは、再帰を利用した無限シーケンスを作成してる。cons は第一引数の要素と第二引数のリストから新たなリストを作成している。

(cons 0 '(2 4 6))
; => (0 2 4 6)

even-numbers では、今の要素と次の要素の計算手順が記された遅延シーケンスをコンス操作の引数に渡すことで 無限シーケンスを構成している。

The Collection Abstraction

シーケンスの抽象化は、構成要素各々に対するオペレーションに関するものに対して、コレクションの抽象化は、データ構造全体に関するものになる。たとえば、 count, empty?, every? とった関数は、個々の要素ではなく、その全体を対する操作になる。

(empty? [])
; => true

(empty? ["no"])
: => false

ここでは代表的で、よくごっちゃになりがちな関数 into, conj をとりあげる。

into

今までみたきたように、シーケンス関数の戻り値はオリジナルのデータ構造ではなく、seq である。それをオリジナルなデータ構造に変換したいとき into がつかえる。

(map identity {:sunlight-reaction "Glitter!"})
; => ([:sunlight-reaction "Glitter!"])

(into {} (map identity {:sunlight-reaction "Glitter!"}))
; => {:sunlight-reaction "Glitter!"}

map 関数は hashmap を seq に変換するが、 into で hashmap に変換し直している。hashmap 以外のデータ構造にも変換できる。

ベクタに変換する

(map identity [:garlic :sesame-oil :fried-eggs])
; => (:garlic :sesame-oil :fried-eggs)

(into [] (map identity [:garlic :sesame-oil :fried-eggs]))
; => [:garlic :sesame-oil :fried-eggs]

セットに変換する

(map identity [:garlic-clove :garlic-clove])
; => (:garlic-clove :garlic-clove)

(into #{} (map identity [:garlic-clove :garlic-clove]))
; => #{:garlic-clove}

into の第一引数が空である必要はない。空ではないと、追記させることができる。

(into {:favorite-emotion "gloomy"} [[:sunlight-reaction "Glitter!"]])
; => {:favorite-emotion "gloomy" :sunlight-reaction "Glitter!"}

(into ["cherry"] '("pine" "spruce"))
; => ["cherry" "pine" "spruce"]

conj

conj 関数もコレクションに要素を追加するが、into と少し異なる。

(conj [0] [1])
; => [0 [1]]

[1] のまま追加されてしまう。

(into [0] [1])
; => [0 1]

一方 into の場合、 1 で追加される conj で同じ結果を得たい場合は、次の通り。

(conj [0] 1)
; => [0 1]

conjスカラー値で受け取るのに対して、 into はコレクションを引数にとるという違いがある。

Function Functions

関数を引数にとったり、戻り値として返す代表的な高階関数として、 apply, partial をとりあげる。

apply

apply は、引数に関数とシーケンスをうけとり、シーケンスを関数の引数として適用するたとえば、 受け取った引数で最大の値を返す max 関数で考えてみる。

(max 0 1 2)

しかし、引数にベクタを受け取った場合、期待どおりにいかない。

(max [0 1 2])
; => [0 1 2]

max に渡すときは、ベクタとしてまとめて渡してはいけず、比較したい値をそれぞれ引数としてわたさないといけない。そして、ベクタとして渡しても動作させるには、 apply が使える。

(apply max [0 1 2])

partial

partial は引数に、一個の関数(f)と 0 個以上の引数(arg)をとり、新しい関数を返す。partial が返す関数を引数(new arg)つきで呼び出すと、partial に渡した f に arg と new arg を渡して適用される。

(def add10 (partial + 10))
(add10 3)
; => 13
(add10 5)
; => 15

(def add-missing-elements
  (partial conj ["water" "earth" "air"]))

(add-missing-elements "unobtainium" "adamantium")
; => ["water" "earth" "air" "unobtainium" "adamantium"]

add10 を呼び出すと、 (+ 10) に与えられた引数を追加して (+ 10 3) が実行される。(+ 10) のように+関数の第一引数を 10 で固定させた関数を作るのが、 partial である。

コンテキストは異なるが関数とその引数の組み合わせが同じものを繰り返し使用するとき、partial は効果的である。

(defn lousy-logger
  [log-level message]
  (condp = log-level
    :warn (clojure.string/lower-case message)
    :emergency (clojure.string/upper-case message)))

(def warn (partial lousy-logger :warn))

(warn "Red light ahead")
; => "red light ahead"

(warn "Red light ahead") を呼び出すことは、 (lousy-logger :warn "Red light ahead") を呼び出すことと同じである。

complement

前節で、1 億人からヴァンパイアを特定する identify-vampire 関数を作成したが、逆に人間を特定するにはどうすればよいだろうか?

(defn identify-humans
  [social-security-numbers]
  (filter #((not (vampire? %)))
          (map vampire-related-details social-security-numbers)))

filter 関数の第一引数は、 #(not (vampire? %)) となっているが、 この述語関数の戻り値を反転させるのが、 complement 関数である。

(defn not-vampire? (complement vampire?))
(defn identify-humans
  [social-security-numbers]
  (filter not-vampire?
          (map vampire-related-details social-security-numbers)))

CLOJURE for the BRAVE and TRUEでClojure学習 part2

はじめに

Clojure の学習備忘録。 今回は、Chapter 4: Core Functions in Depth の途中まで。

Sequence とはなにかを説明した章であり、map, reduce といったいくつか重要な Sequence 関数が紹介される。

js と違って、
Clojure が hashMap も sequence として扱われる点は面白い。

Programing to Abstractions

Clojure の重要な概念に、抽象化プログラミングがある。抽象化プログラミングではない Emacs Lisp(elisp)の場合、 mapcar 関数を使えば、Clojuremap と同じように新しいリストを生成できる。しかし、elisp でハッシュマップに対して map 関数を適用したい場合、 maphash 関数が必要になる。一方 Clojure の場合、リストであれハッシュマップであれ、 同じ map 関数ですませられる。

elisp では同じ map 操作でもデータの構造に応じて異なる関数を使用しないといけないが、Clojure の場合ひとつだけでよいのである。 reduce 関数の場合でも同じである。

というのも Clojure は、データ構造ではなく、 抽象化シーケンス という観点で mapreduce 関数を定義しているからなのである。

あとで紹介する first, rest, cons といったシーケンスのコアとなる操作が適用できるデータ構造であれば、mapreduce その他シーケンス関数をいつでも Clojure は適用可能である。これこそ Clojure が意味する抽象化プログラミングであり、Clojure 哲学の中心的な考え方である。

それでは map 関数を例に抽象化シーケンスについてもう少し詳しくみてみよう。

Treating Lists, Vectors, Sete, And Maps as Sequences

特定のプログラミング言語関係なく map 操作の中心的な振る舞いを考えると、それは y1 = f(x1), y2 = f(x2), yn = f(xn)のように x というシーケンスから y の新しいシーケンスを生成することである。

ここでいうシーケンスというのは、順番に要素が並べられている集合や、あるいは要素に順番がない集合のことであったり、あるいは要素同士が前後関係をもった集合も考えられる。

map 操作やシーケンスの説明では、リスト、ベクタ、その他具体的なデータ構造がどうなのかをはっきり示さない。このようにできるだけ抽象的に考えプログラミングするよう Clojure は設計され、データ構造の抽象化という観点で関数が実装されている。

シーケンスのコアとなる関数 first, rest, cons が適用できるならば、 そのデータ構造は抽象化シーケンスを実現しているといえる。
リストや、ベクタ、セット、ハッシュマップは、みな抽象化シーケンスであるので、 map 関数を適用できるのである。

(defn titleize
  [topic]
  (str topic " fro the Brave and True"))
(map titleize ["Hamsters" "Ragnarok"])
(map titleize '("Empathy" "Decoratting"))
(map titleize #{"Elbows" "Soap Carving"})
(map #(titleize (second %)) {:uncomfortable-thing "Winking"})

はじめの2つは、ベクタやリストに map を適用している。
3つめは、順序のないセットに、4つめはハッシュマップに map が適用してる。

first ,rest, and cons

このセクションでは、Javascript で、リンクリストと3つの関数(first, rest, cons)を実装する。これら3つの関数を使って、 map 関数の組み立て方を説明する。

重要なのは、リンクリストを使った具体的な実装が、Clojure のおけるシーケンス抽象化とどのように異なるのか理解することであり、 特定のデータ構造における実装がどうなのかに着目しなくてよい。

あるデータ構造に対してシーケンス関数が適用できるかは、 first, rest, cons 関数が適用できるかどうかであり、 もし適用できるならば、そのデータ構造にはシーケンスライブラリが適用できる。

リンクリストとは、各ノードがリンクして一列のシーケンスになってるものである。

ここでは、javascript でリンクリストを実装する。
node3 はリンクリストの最後の要素で、next は null になる。

var node3 = {
  value: "last",
  next: null,
};

node2 の next は、node3 を node1 の next は、node2 を参照しており、 以上でリンクリストとなってる。

var node2 = {
  value: "second",
  next: node3,
};

var node1 = {
  value: "first",
  next: node2,
};

このリンクリストに対して 3 つの関数(first, rest, cons)を実装する。

各関数の内容は以下の通り first は、受け取った node の value を返し、
rest は、受け取った node のあとに続く value を返し、
cons は、新しい node をリストの先頭に追加する。

そしてこれら3つが実装できると、
それらを組み合わせて、 map, reduce, filter やその他シーケンス関数が実装できるようになる。

注目してほしいのは、 first, rest の引数名が node になっていることだ。

var first = function (node) {
  return node.value;
};

var rest = function (node) {
  return node.next;
};

var cons = function (newValue, node) {
  return {
    value: newValue,
    next: node,
  };
};

first(node1);
// => "first"

first(rest(node1));
// => "middle"

first(rest(rest(node1)));
// => "last"

var node0 = cons("new first", node1);
first(node0);
// => "new first"

first(rest(node0));
// => "first"

では、準備が整ったので 3つの関数を使って、 map を実装してみる。

var map = function (list, transform) {
  if (list === null) {
    return null;
  } else {
    return cons(transform(first(list)), map(rest(list), transform));
  }
};

map 関数は、リストの1つ目の要素を変換して、残りのリストに対して null が返すまで再帰的に適用していく。
全要素に "mapped!" を文字列追加させて、1つ目の要素取得するならば次のとおりになる。

first(
  map(node1, function (val) {
    return val + " mapped!";
  })
);

// => "first mapped!"

いけてるのは、 map 関数が first, rest, cons だけで実装されているということである。
どんなデータ構造であっても、 first, rest, cons が適用できるならば、 map は機能するのだ。

実際リンクリストだけではなく、配列でも map が機能することを確認しよう。

var first = function (array) {
  return array[0];
};

var rest = function (array) {
  var sliced = array.slice(1, array.length);
  if (sliced.length == 0) {
    return null;
  } else {
    return sliced;
  }
};

var cons = function (newValue, array) {
  return [newValue].concat(array);
};

var list = ["Transylvania", "Forks, WA"];
map(list, function (val) {
  return val + " mapped!";
});
// => ["Transylvania mapped!", "Forks, WA mapped!"]

ここでも、Javascript の Array 関数をつかって、 first, rest, cons を実装してる。
map 関数は、新しく定義された first, rest, cons を参照するようになるので、 array でも機能する。

これで first, rest, cons が実装できれば、 map やその他のシーケンス関数が自由に使用できることがわかるだろう。

Abstraction Through Indirection

しかし、 first 関数がどんなデータ構造でも適用できる説明がないので、上記説明では不十分である。Clojure では、これを2つの間接的な方法で実現している。

プログラミングの世界で間接というのは、1 つの名前が複数の関連した意味を持つという意味でよく使用される。この場合も、 first は複数の、データ構造に依存した意味を持っている。間接は、抽象化を可能にする。

ポリモーフィズムClojure が提供する間接の1つの手段である。詳細は割愛するが、ポリモーフィズム関数は、引数のタイプに応じて異なる機能を提供できる(引数の個数で別の機能を提供するマルチ Arity 関数とは違う) この点については、Chapater13 章で紹介する。

シーケンスに関しては、Clojure はもっと簡単な変換を通じて抽象的な関数が適用できるデータ構造を生成し、間接を実現している。seq 関数を使うのだ。

(seq '(1 2 3))
; => (1 2 3)

(seq [1 2 3])
; => (1 2 3)

(seq #{1 2 3})
; => (1 2 3)

(seq {:name "Bill Compton" :occupation "Dead mopey guy"})
; => ([:name "Bill Compton"] [:occupation "Dead mopey guy"])

ここでは、2 点注目することがある。
1つ目は、 seq 関数は、リストのように振る舞う値を必ず返す(これをシーケンスあるいは seq と呼ぶ)。
2つ目は、ハッシュマップに対して seq を適用すると、key, value を2要素からなるシーケンスを返す。 このため、ハッシュマップでもベクタのように map 関数を適用できるのである。4つ目の例がそれである。

into 関数を使えば、seq から map に戻すことができる。

(into {} (seq {:a 1 :b 2 :c 3}))
; => {:a 1, :c 3, :b 2}

以上から Clojure のシーケンス関数は、引数に seq 関数を適用している。抽象化シーケンスが実装されたデータ構造であれば、 reduce, filter, distict, group-by といった有名な関数が適用できるわけだ。

Seq Function Examples

map

今まで map 関数を見てきたので、今回は map の新しい機能2つを紹介する。 1つは複数のコレクションを引数に取る場合であり、もう一つは関数コレクションを引数に取る場合である。 またよく map で使う keyword マッピングも紹介する。

今まで見てきた map 関数は、次のようにひとつのコレクションを引数にとるもの。

(map inc [1 2 3])

しかし、複数のコレクションを map にわたすことができ、こんな感じである。

(map str ["a" "b" "c"] ["A" "B" "C"])

この処理は、次の処理とやってることおなじである。

(list (str "a" "A") (str "b" "B") (str "c" "C"))

コレクションの同じインデックスのものが、 str に渡されてマッピングされていく。コレクションの要素数が同じでないといけないことには注意しよう。

別の例でもみてみよう。1つは、過去4日間の人間から摂取した血のリットル数で、もうひとつは動物から摂取した血のリットル数のベクタである。 unify-diet-data 関数は、人間・動物の両方から一日の摂取量を引数にとり、ひとつのハッシュマップにまとめる関数である。

(def human-consumption   [8.1 7.3 6.6 5.0])
(def critter-consumption [0.0 0.2 0.3 1.1])
(defn unify-diet-data
  [human critter]
  {:human human
    :critter critter})
(map unify-diet-data human-consumption critter-consumption)

今度は、 map 関数コレクションを引数にとったときの処理についてみてみる。この機能を使えば、ひとまとめの計算処理が可能になる。

(def sum #(reduce + %))
(def avg #(/ (sum %) (count %)))
(defn stats
  [numbers]
  (map #(% numbers) [sum count avg]))

(stats [3 4 10])
; => (17 3 17/3)
(stats [80 1 44 13 6])
; => (144 5 144/5)

ここでは, stats 関数がベクタに可能のされた関数を numbers に対して一つずつ適用している。

Clojure 使いがよく map で使う、ハッシュマップコレクションから特定の key の value を取得する処理についても紹介する。

(def identities
  [{:alias "Batman" :real "Bruce Wayne"}
    {:alias "Spider-Man" :real "Peter Parker"}
    {:alias "Santa" :real "Your mom"}
    {:alias "Easter Bunny" :real "Your dad"}])

(map :real identities)
; => ("Bruce Wayne" "Peter Parker" "Your mom" "Your dad")

reduce

Chapter3 で reduce について紹介したが、ここではまた別の使い方を紹介する。

ひとつめは、ハッシュマップを受けおり、key はそのまま value を更新する処理である。

(reduce (fn [new-map [key val]]
          (assoc new-map key (inc val)))
        {}
        {:max 30 :min 10})
; => {:max 31, :min 11}

引数に受け取った {:max 30 :min 10} は、 ([:max 30] [:min 10]) として reduce 内部で使用されるのでうまくいくわけだ。

もうひとつの reduce の便利な使い方は、value に応じて key をフィルターするものである。次の例では、匿名関数を利用して value が 4 より大きいかどうかを判別し、小さい場合はカットしてる。だから 3.9 の key はなくなる。

(reduce (fn [new-map [key val]]
          (if (> val 4)
            (assoc new-map key val)
            new-map))
        {}
        {:human 4.1
          :critter 3.9})
; => {:human 4.1}

お持ち帰りしてほしいポイントは、 reduce 関数が初見よりもずっと柔軟な関数であるということである。reduce をつかって、map, filter, some と同じ処理ができるように実装してみてもよいだろう。

take, drop, take-while, and drop-while

take, drop はともに number と collection の2つの引数を受け取る。 take は 1 から n 番目までの要素を取得するのに対して、 drop では 1 から n 番目を除いた残りの要素を返す。

(take 3 [1 2 3 4 5 6 7 8 9 10])
; => (1 2 3)

(drop 3 [1 2 3 4 5 6 7 8 9 10])
; => (4 5 6 7 8 9 10)

姉妹関数の take-while, drop-while はよりおもしろい挙動をする。ともに返り値が真偽値である述語関数を受け取り、それを使って take あるいは drop を続けるかどうかを判断する

たとえば、ベクタ表記された食事日誌でしてみる。日誌は、月・日ごとに食したものが記録されている。

(def food-journal
  [{:month 1 :day 1 :human 5.3 :critter 2.3}
    {:month 1 :day 2 :human 5.1 :critter 2.0}
    {:month 2 :day 1 :human 4.9 :critter 2.1}
    {:month 2 :day 2 :human 5.0 :critter 2.5}
    {:month 3 :day 1 :human 4.2 :critter 3.3}
    {:month 3 :day 2 :human 4.0 :critter 3.8}
    {:month 4 :day 1 :human 3.7 :critter 3.9}
    {:month 4 :day 2 :human 3.7 :critter 3.6}])

匿名述語関数である #(< (:month %) 3) をつかって、1, 2 月の日誌データの取得できる。

(take-while #(< (:month %) 3) food-journal)
; => ({:month 1 :day 1 :human 5.3 :critter 2.3}
;     {:month 1 :day 2 :human 5.1 :critter 2.0}
;     {:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 2 :day 2 :human 5.0 :critter 2.5})

take-while は、3 月の日誌データを読み取ると、匿名関数が false を返し、それ以降の処理をやめるのである。

同じような考え方で、 drop-whiletrue である限り要素を drop する。

(drop-while #(< (:month %) 3) food-journal)
; => ({:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8}
;     {:month 4 :day 1 :human 3.7 :critter 3.9}
;     {:month 4 :day 2 :human 3.7 :critter 3.6})

take-while, drop-while の両方を使用すれば、2, 3 月のデータを取得できる

(take-while #(< (:month %) 4)
            (drop-while #(< (:month %) 2) food-journal))
; => ({:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 2 :day 2 :human 5.0 :critter 2.5}
;     {:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8})

filter and some

filter 関数を使えば述語関数が true を返すシーケンス内の全要素がえられる。たとえば、先程の食事日誌を使って、人間から摂取したリットル数が、5 未満のものをえよう。

(filter #(< (:human %) 5) food-journal)

; => ({:month 2 :day 1 :human 4.9 :critter 2.1}
;     {:month 3 :day 1 :human 4.2 :critter 3.3}
;     {:month 3 :day 2 :human 4.0 :critter 3.8}
;     {:month 4 :day 1 :human 3.7 :critter 3.9}
;     {:month 4 :day 2 :human 3.7 :critter 3.6})

これと同じ結果を、 take-while でも実現できるが、 take-while のほうが効率はよい。なぜなら、filter の場合全要素をなめるに対して、~take-while~ では述語関数が false を返した途端処理を止めるからである。

述語関数が true を返す要素を、コレクションが含むかどうかを知りたい場合は、 some をつかえばよい。some は、述語関数が true を初めて返した値を返す。

(some #(> (:critter %) 5) food-journal)
; => nil

(some #(> (:critter %) 3) food-journal)
; => true
(some #(and (> (:critter %) 3) %) food-journal)
; => {:month 3 :day 1 :human 4.2 :critter 3.3}

食事日誌には、動物から 5 リットル以上摂取したことが記録がないので、はじめの例では nil を、 一方 3 リットル以上摂取した記録はあるので true を返している。

注目するべきは、返り値が、日誌記録レコードではなく true であること。もしレコードがほしいならば次の通り。

(some #(and (> (:critter %) 3) %) food-journal)
; => {:month 3 :day 1 :human 4.2 :critter 3.3}

sort and sort-by

sort 関数を使えば昇順で並び替えできる。

(sort [3 1 2])
; => (1 2 3)

もっと凝った並び替えするならば、 sort-by が使える。関数をコレクションの要素に適用して、その戻り値でソートできる。

(sort-by count ["aaa" "c" "bb"])
; => ("c" "bb" "aaa")

sort を使えば, アルファベット順で "aaa" "c" "bb" となるに対して、 sort-by を使えば、count を適用した結果(3, 1 ,2)で並び替える。

concat

concat はシンプルで、あるシーケンスを別のシーケンスの末尾に追加する

(concat [1 2] [3 4])
; => (1 2 3 4)

CLOJURE for the BRAVE and TRUEでClojure学習 part1

はじめに

Clojure の勉強をはじめた。
最近出版された Clojure 書籍がないため、Clojure/ClojureScript 関連リンク集で入門者向けとして紹介されてる
Clojure for the Brave and Trueを翻訳?(読み)ながら自分が重要だとおもったところメモしていくスタイルで Clojure への理解を深めようと思う。

今回は、Chapter3 Do Things: A Clojure Crash Course の学習記録。
Chapter3 ではあるが、1, 2 が環境構築の説明なので、実質この章が文法学習のスタートになる。

Syntax

Form

Clojure で書かれたコードはみな統一された構造をもっており、構造には2つの種類がある。

  • データ構造をそのまま記述(たとえば、数値や、文字列、ハッシュマップ、ベクタ)
  • オペレーション

データ構造をそのまま記述する例は次の通り

1
"a string"
["a" "vector" "of" "strings"]

オペレーションは、開括弧、オペレータ、オペランド、閉括弧で構成される。

(operator oprand1 oprand2 ... operandn)

カンマではなく、空白でオペランドを区別する。

たとえば以下のとおり

(+ 1 2 3)
; => 6
(str "It was the panda " "in the library " "with a dust buster")
; => It was the panda in the library with a dust buster

このような Clojure の構造は、今まで使用してきた言語と異なるので奇妙に思うかもしれない。
他の言語では、オペレーションが異なれば、オペレータとオペランドに応じて構造も変わる。
たとえば、javascript ではオペレータや括弧など様々なものを取り入れている。

1 + 2 + 3;
"It was the panda ".concat("in the library ", "with a dust buster");

一方 Clojure の構造は一貫してシンプル。 どんなオペレータをつかおうとも、またどんな種類のデータを処理しようとも構造は同じである。

Control Form

if

書き方

(if boolean-form
  then-form
  optional-else-form)

判別式が true の場合は、はじめのフォーム(then-form)が評価され、
判別式が false の場合は、つぎのフォーム(optional-else-form)フォームが評価される。

判別式が true の場合

(if true
  "By Zeus' hammer!"
  "By Aquamans's trident!")

判別式が fale の場合

(if false
  "By Zeus' hammer!"
  "By Aquamans's trident!")

else は書かなくてもよい。

(if false
  "By Odin's Elbow")
do

しかし以下の Ruby のように、条件みたしたときに複数の評価をしたい場合どうすればよいのだろうか?

if true
    doer.do_thing(1)
    doer.do_thing(2)
else
    other_doer.do_thing(1)
    other_doer.do_thing(2)
end

do を使用すれば実現できる

(if true
  (do (println "Success")
      "By Zeus' hammer!")
  (do (println "Failure")
      "By Aquamans's trident"))
; => Success!
; => "By Zeus's hammer!"
when

when は、 ifdo をあわせたオペレータであり、 else がない。

(when true
  (println "Success!")
  "abra cadabra")
; => Success!
; => "abra cadabra"

nil, true, false, Truthiness, Equality, and Boolean Expression

Clojure は、true と false を真偽値としてもつ。 nil は、値をもたないものとして Clojure で扱われる。nil かどうかを判別するには、 nil? 関数を使用する。

(nil? 1)
; => false
(nil? nil)
; => true

nil と false は、論理的な偽として扱われ、それ以外はすべて論理的に真として扱われる。

(if "bears eat bears"
  "bears beets Battlestar Galatica")
; => bears beets Battlestar Galatica
(if nil
  "This won't be the result because nil is falsey"
  "nil is falsey")
; => nil is falsey

Namging values with def

def をつかうと名前に値をバインド(固定)することができる。

(def failed-protagonist-names
  [["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"]])
failed-protagonist-names
; => ["Larry Potter" "Doreen the Explorer" "The Incredible Bulk"]

bind する理由は再代入できないようにするためである。

Data Structures

Clojure には便利なデータ構造があり、それに大半な時間を使用するだろう。
オブジェクト指向バックグラウンドの人は、ここで紹介する基本的なデータ構造で多くのことがができることに驚くだろう。

Clojure のデータ構造はイミュータブルであり、変更できない。

1. Numbers

Clojure では、整数、小数に加えて 1/5 のように比率のまま扱える。

93
1.2
1/5

2. Strings

文字列はダブルクォートでくくる。シングルクォートは使用できない。

Clojure では変数に文字列を追加することを許可していないので、 str 関数を使用する。

(def name "Chewbacca")
(str  "\"Uggllglglglglglglglll\" - " name)

3. Maps

他の言語でいう、ハッシュマップやディクショナリと似たものである。ある値を他の何かと関連づける。

空の map はこの通り

{}

:first-name, :last-name はキーワードと呼ばれる(後述する)

{:first-name "Charlie"
:last-name "McFishwich"}

関数を value に指定できる

{"string-key" +}

ネストできる

{:name {:first "John" :middle "Jacob" :last "Jingleheimerschmidt"}}

hash-map 関数を使って map を作成できる

(hash-map :a 1 :b 2)
; => {:a 1 :b 2}

get 関数を使って値を取得できる

(get {:a 1 :b 1} :b)
; => 1

key がなければ、get 関数は nil を返す

(get {:a 1 :b 1} :c)
; => nil

get-in 関数でネストした map から値を取得できる

(get-in {:a 0 :b {:c "ho hum"}} [:b :c])
; => ho hum

map の引数に key を指定すると値を取得できる

({:a 0 :b {:c "ho hum"}} :b)
; => {:c "ho hum"}

4. Keywords

主に map の key として使用される。

関数として使用すると、key に一致する value を取得できる

(:a {:a 1 :b 2 :c 3})
; => 1

get 関数を使えば、default value を設定できる

(:d {:a 1 :b 2 :c 3} "No gnome knows homes like Noah knows")
; => "No gnome knows homes like Noah knows"

5. Vectors

配列のように、インデックスアクセスできる

(get [3 2 1] 0)
; => 3

vector 関数を利用して、ベクタを生成できる

(vector 1 2 3)
; => [1 2 3]

conj 関数で、要素を後ろに追加できる

(conj [1 2 3] 4)
; => [1 2 3 4]

6. Lists

Lists は Vector と同様リニアなコレクションであるが、get で要素を取得できない。 また、List であることを表現するには、シングルクォートを前につける必要がある。

'(1 2 3 4)
; => (1 2 3 4)

リストから要素を取得するには、nth を使う。ただしリストよりベクタの要素アクセスのほうが、パフォーマンスはよい。

(nth '(1 2 3) 0)
; => 1

conj 関数でリストの先頭に要素を追加する

(conj '(1 2 3 ) 4)
; => (4 1 2 3)

リストとベクタの使い分けは、 先頭に要素を追加するとき、マクロを作成するときにリストを使い、 それ以外は、ベクタを利用するのがよい。

7. Sets

Sets は、一意のコレクションである。 同じ要素が複数存在することがない。

#{"kurt vonnegut" 20 :icicle}
; => #{20 :icicle "kurt vonnegut"}

hash-set をつかって Set を作成できる

(hash-set 1 1 2 2)
; => #{1 2}

同じ要素を追加しても、変化しない。

(conj #{1 2} 2)
; => #{1 2}

set 関数でベクタから Sets を作成できる

(set [3 3 3 4 4])
; => #{4 3}

contains?で要素が含まれてるかどうか確認できる

(contains? #{:a :b} :a)
; => true

(contains? #{:a :b} 3)
; => false

(contains? #{nil} nil)
; => true

8. Simplicity

Clojure は、オブジェクト指向であつかう独自のクラスを考えない。組み込みのデータ構造を扱うことが、Clojure のシンプルさである。

10 個のデータ構造が 10 の関数をもつより、1 個データ構造で 100 の関数持つほうがよい。 Alan Perlis

基本的なデータ構造を利用してコードの再利用性を得られる方法を意識するがよい

Functions

1. Caling Functions

今まで見てきたように関数呼び出しはこんな感じ。

(+ 1 2 3 4)
(* 1 2 3 4)
(first [1 2 3 4])

関数呼び出しは、演算子が関数あるいは関数を返す関数式として処理される操作のことである。

関数式としての例

((and (= 1 1) +) 1 2 3)

関数の柔軟性は、関数をファーストクラスとして Number や Vecotor と同じように引数に使えることである。

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

map の返り値が、ベクタではあくリストであることに注意しよう。引数として渡したときはベクタであったのにである。

(+ (inc 199) (/ 100 (- 7 2)))
(+ 200 (/ 100 (- 7 2))) ; evaluated "(inc 199)"
(+ 200 (/ 100 5)) ; evaluated (- 7 2)
(+ 200 20) ; evaluated (/ 100 5)
220 ; final evaluation

すべてのフォームが評価されてから、はじめの + が評価される。

2. Function Calls, Macro Calls, Special Forms

関数呼び出しは、関数式が演算子として処理される式である。式には他に二種類あって、マクロ呼び出しと、スペシャルフォームである。

スペシャルフォームの special の意味というのは、関数呼び出しと異なり、必ずしもすべてのオペランドを評価するわけではないという点にある。

スペシャルフォームの例として if がある。

(if good-mood
    (tweet walking-on-sunshine-lyrics) ;; 1
    (tweet mopey-country-song-lyrics)) ;; 2

この場合、全てのオペランドが評価されず、条件文に応じて 1,2 のいずれかが評価される。

また他の特徴として、関数の引数に指定できないことである。一般的に、 special form は、中心となる Clojure の関数性を実現しており、関数として実装できない。

3. Defineing Functions

関数定義は、5 つのパーツで構成される。

  • defn
  • 名前
  • 関数の説明(オプション)
  • パラメータ
  • 関数内容
(defn too-enthusiastic                                                ;; 関数名
  "Return a cheer that might be a bit too enthusiastic"               ;; 関数の説明
  [name]                                                              ;; パラメータ
  (str "OH. MY. GOD! " name " YOU ARE MOST DEFINITELY LIKE THE BEST " ;; 関数内容
    "MAN SLASH WOMAN EVER I LOVE YOU AND WE SHOULD RUN AWAY SOMEWHERE"))
1. The Docstring

REPL で (doc fn-name) を実行すると、関数の説明を見ることができる。

2. Pramater and Arity

関数に渡す値を引数とよび、引数の数をアリティ(arity)と呼ぶ。
0 個以上の引数をわたすことができ、どんな種類でもよい。

(defn no-parms
  []
  "I take no parameters!")

(defn one-param
  [x]
  (str "I take one parmeter:" x))

(defn two-params
  [x y]
  (str "Two parameters! That's nothing! Pah! I will smoosh them "
  "together to spite you! " x y))

Arity を使えばオーバーロードを実現でき、
ひとつの関数定義に、Arity に応じて異なる処理を実行できる。

Arity ごとに()でくくられて、それぞれ引数のリストをもつ。

(defn multi-arity
  ;; 3 arity
  ([first second third]
    (do-things first second third))
  ;; 2 arity
  ([first second]
    (do-things first second))
  ;; 1 arity
  ([first]
    (do-things first)))

Arity のオーバーロードを使えば、デフォルト引数が使用できる

(defn x-chop
  "Describe the kind of chop you're inflicting on someone"
  ([name chop-type]
    (str "I " chop-type " chop " name "! Take that!"))
  ([name]
    (x-chop name "karate")))
(x-chop "Kanye West" "slap")
; => "I slap chop Kanye West! Take that!"
(x-chop "Kanye East")
; => "I karate chop Kanye East! Take that!"

& を使えば rest パラメータを使用できる。

(defn codger-communication
  [whippersnapper]
  (str "Get off my lawn, " whippersnapper "!!!"))

(defn codger
  [& whippersnappers]
  (map codger-communication whippersnappers))

(codger "Billy" "Anne-Marie" "The Incredible Bulk")
; => ("Get off my lawn, Billy!!!"
;      "Get off my lawn, Anne-Marie!!!"
;      "Get off my lawn, The Incredible Bulk!!!")
(defn favorite-things
  [name & things]
  (str "Hi, " name ", here are my favorite things: "
  (clojure.string/join ", " things)))

(favorite-things "Doreen" "gum" "shoes" "kara-te")
3. Destructing

collection で使う

受け取ったベクタの引数のはじめの要素を first-thing にバインドする

(defn my-first
  [[first-thing]]
  first-thing)
  (my-first ["oven" "bike" "war-exe"])

rest パラメータも使用できる

(defn chooser
  [[first-choice second-choice & unimportant-choices]]
  (println (str "Your first choice is: " first-choice))
  (println (str "Your second choice is: " second-choice))
  (println (str "We're ignoring the rest of your choices. "
    "Here they are in case you need to cry over them:"
  (clojure.string/join "," unimportant-choices))))

(chooser ["Mamalade" "Handome Jack" "Pigpen" "Aquaman"])

Map で使う

{変数 :key}という表現で、変数名に key に対応する value をバインドする

(defn announce-treasure-location
  [{lat :lat lng :lng}]
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})

他のやりかたもある。

(defn announce-treasure-location
  [{:keys [lat lng]}] ;; ここが違う
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng)))

(announce-treasure-location {:lat 28.22 :lng 81.33})

:as を使えば、もともとの map にもアクセスできる

(defn receive-treasure-location
  [{:keys [lat lng] :as treasure-location}]
  (println (str "Treasure lat:" lat))
  (println (str "Treasure lng:" lng))
  (steer-ship! treasure-location))
4. Function body

Function body にはどんな種類のフォームでも含めることができる。
返り値は、最後のフォームの評価結果になる。

(defn illustrative-function
  []
  (+ 1 304)
  30
  "joe")
(illustrative-function)
5. All Functions Are Created Equal

自分で定義した関数であれ、inc、map などの組み込み関数であれ、Clojure は区別せず括弧のはじめを関数として愚直に処理する。これが Clojure の Simple さである。

6. Anonymous Functions

fn を使用すれば匿名関数を作れる。匿名関数でも、通常通り、destructing や rest パラメータを使用できる。

(map (fn [name] (str "Hi, " name))
  ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

またもう一つ簡潔な匿名関数の作成方法がある。

(map #(str "Hi, " %) ["Darth Vader" "Mr. Magoo"])
; => ("Hi, Darth Vader" "Hi, Mr. Magoo")

リーダマクロという機能によって、この書き方でも匿名関数として利用される。
% は、引数を意味しており、複数の引数を受け取る場合、%1 %2 %3…となる。
%、%1 と同じ意味である。

%& で rest パラメータを利用できる。

(#(identity %&) 1 "blarg" :yip)

identity 関数は、自身を返す関数である。
rest パラメータは、リストに格納され、それぞれに対して identity 関数が適用された結果が返ってきてる。

7. Returning Functions

関数を返す関数を、Closure と呼び、関数定義されたスコープ内のどの変数にもアクセスできる。

(defn inc-maker
  "Create a custom incrmeter"
  [inc-by]
  #(+ % inc-by))

(def inc3 (inc-maker 3))

(inc3 7)

Pulling it All Together

問題: ホビットの非対称な体の情報から完全な情報をつくる以下定義されるように、体の部位(name)とサイズ(size)が記載されたホビットの体の情報がある。しかし、左目(left-eye)のように左のパーツしか記述できていない不完全な状態なので、これを完全な状態にするのがゴール。

(def asym-hobbit-body-parts [{:name "head" :size 3}
                              {:name "left-eye" :size 1}
                              {:name "left-ear" :size 1}
                              {:name "mouth" :size 1}
                              {:name "nose" :size 1}
                              {:name "neck" :size 2}
                              {:name "left-shoulder" :size 3}
                              {:name "left-upper-arm" :size 3}
                              {:name "chest" :size 10}
                              {:name "back" :size 10}
                              {:name "left-forearm" :size 3}
                              {:name "abdomen" :size 6}
                              {:name "left-kidney" :size 1}
                              {:name "left-hand" :size 2}
                              {:name "left-knee" :size 2}
                              {:name "left-thigh" :size 4}
                              {:name "left-lower-leg" :size 3}
                              {:name "left-achilles" :size 1}
                              {:name "left-foot" :size 2}])

最終的に生成するコードは以下の通り

(defn matching-part
  [part]
  {:name (clojure.string/replace (:name part) #"^left-" "right-")
    :size (:size part)})

(defn symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (loop [remaining-asym-parts asym-body-parts
          final-body-parts []]
    (if (empty? remaining-asym-parts)
      final-body-parts
      (let [[part & remaining] remaining-asym-parts]
        (recur remaining
                (into final-body-parts
                      (set [part (matching-part part)])))))))

実行すると次の結果を得る。

(symmetrize-body-parts asym-hobbit-body-parts)
; => [{:name "head", :size 3}
;     {:name "left-eye", :size 1}
;     {:name "right-eye", :size 1}
;     {:name "left-ear", :size 1}
;     {:name "right-ear", :size 1}
;     {:name "mouth", :size 1}
;     {:name "nose", :size 1}
;     {:name "neck", :size 2}
;     {:name "left-shoulder", :size 3}
;     {:name "right-shoulder", :size 3}
;     {:name "left-upper-arm", :size 3}
;     {:name "right-upper-arm", :size 3}
;     {:name "chest", :size 10}
;     {:name "back", :size 10}
;     {:name "left-forearm", :size 3}
;     {:name "right-forearm", :size 3}
;     {:name "abdomen", :size 6}
;     {:name "left-kidney", :size 1}
;     {:name "right-kidney", :size 1}
;     {:name "left-hand", :size 2}
;     {:name "right-hand", :size 2}
;     {:name "left-knee", :size 2}
;     {:name "right-knee", :size 2}
;     {:name "left-thigh", :size 4}
;     {:name "right-thigh", :size 4}
;     {:name "left-lower-leg", :size 3}
;     {:name "right-lower-leg", :size 3}
;     {:name "left-achilles", :size 1}
;     {:name "right-achilles", :size 1}
;     {:name "left-foot", :size 2}
;     {:name "right-foot", :size 2}]

以下、コードの説明をしていく

1. let

let は名前に値を bind(固定)する機能がある。

次は、x という変数名に 3 という値を固定している。

(let [x 3]
  x)

次の例は、dalmatian-list の先頭から2要素を取得して dalmatians という変数にバインドしてる

(def dalmatian-list
  ["Pongo" "Perdita" "Puppy 1" "Puppy 2"])
(let [dalmatians (take 2 dalmatian-list)]
  dalmatians)

また let には新しいスコープを作成する機能がある。

(def x 0)
(let [x 1] x)

はじめに変数 x に 0 をバインドしているが、let 内では x を 1 でバインドしてる。

(def x 0)
(let [x (inc x)] x)

let ないで rest パラメータを利用できる。

(def dalmatian-list
  ["Pongo" "Perdita" "Puppy 1" "Puppy 2"])

(let [[pongo & dalmatians] dalmatian-list]
  [pongo dalmatians])

では、問題の該当箇所にもどって説明する。

(let [[part & remaining] remaining-asym-parts]
  (recur remaining
          (into final-body-parts
                (set [part (matching-part part)]))))

let によって、remaining-asym-parts の最初の要素を part に、残りの要素を remaing にバインドしてる。

その後、body 内で recur フォームを評価している。

2. loop

loop フォームのサンプルをみてみる。

(loop [iteration 0]
  (println (str "Iteration " iteration))
  (if (> iteration 3)
    (println "Goodbye!")
    (recur (inc iteration))))

はじめの行では、変数名 iteration を初期値 0 でバインドしている。loop 開始時 iteration を 0 にしている。

次の println を呼び出し、iteration が 3 を超えていたら、Goodbye を出力する。超えていないならば、recur を行う。

recur のオペランドが、次のループの iteration の値になる。

loop 関数を再帰関数をつかって表現できる

(defn recursive-printer
  ;; デフォルトvalueを指定してる
  ([]
    (recursive-printer 0))
  ([iteration]
    (println (str "Iteration " iteration))
    (if (> iteration 3)
      (println "GoodBye!")
      (recursive-printer (inc iteration)))))

(recursive-printer)
; => Iteration 0
; => Iteration 1
; => Iteration 2
; => Iteration 3
; => Iteration 4
; => Goodbye!

再帰関数でかけると言われて、自分で書いてみたら次のようなコードになった。
出力は同じだが、2つことなる。

  • 関数呼び出し時に、引数を渡たしてる
  • do 関数を利用してる
(defn iteration
  "自分で書いてみる"
  [param]
  (if (> param 4)
    (println "Goodbye!")
    (do (println (str "Iteration " param))
        (iteration (inc param)))))

(iteration 0)

0 という引数を渡してることから、
デフォルト引数をまだ身に着けれていないことがわかる。

また do 内で複数を呼ぶ必要はない。

3. Regular Expression

正規表現の書き方

#"regular-expression"

clojure.string/replaceを使用すれば、正規表現に合致する文字列を変換できる。
^は、先頭を意味する

正規表現のテストするには、re-find関数が役に立つ。
マッチすればマッチした文字列を返し、そうでなければ nil を返す

(re-find #"^left-" "left-eye")
; => "left-"

(re-find #"^left-" "cleft-chin")
; => nil

(re-find #"^left-" "wongleblart")
; => nil

では、問題のコードでも、left が先頭につく文字列は、right に変換し そうでなければそのまま変換してることがわかる。

(defn matching-part
  [part]
  {:name (clojure.string/replace (:name part) #"^left-" "right-")
   :size (:size part)})
(matching-part {:name "left-eye" :size 1})
; => {:name "right-eye" :size 1}]

(matching-part {:name "head" :size 3})
; => {:name "head" :size 3}]

4. Better Symmetrizer with reduce

loop よりも reduce 関数を使えば、完結に書くことができる。

reduce の書き方

(reduce function initial-value sequence)
(reduce + 15 [1 2 3 4])
(defn better-symmetrize-body-parts
  "Expects a seq of maps that a :name and :size"
  [asym-body-parts]
  (reduce (fn [final-body-parts part]
            (into final-body-parts (set [part (matching-part part)])))
            []
            asym-body-parts))
(def asym-hobbit-body-parts [{:name "head" :size 3}
                        {:name "left-eye" :size 1}
                        {:name "left-ear" :size 1}
                        {:name "mouth" :size 1}
                        {:name "nose" :size 1}
                        {:name "neck" :size 2}
                        {:name "left-shoulder" :size 3}
                        {:name "left-upper-arm" :size 3}
                        {:name "chest" :size 10}
                        {:name "back" :size 10}
                        {:name "left-forearm" :size 3}
                        {:name "abdomen" :size 6}
                        {:name "left-kidney" :size 1}
                        {:name "left-hand" :size 2}
                        {:name "left-knee" :size 2}
                        {:name "left-thigh" :size 4}
                        {:name "left-lower-leg" :size 3}
                        {:name "left-achilles" :size 1}
                        {:name "left-foot" :size 2}])

(defn better-symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (reduce (fn [final-body-parts part]
            (into final-body-parts (set [part (matching-part part)])))
          []
          asym-body-parts))

学んだ destructing と匿名関数を使えって書き直せば、次のとおりだろう。

(defn match-part
  [{name :name size :size}]
  {:name (clojure.string/replace name #"^left-" "right-")
  :size size})

(defn better-symmetrize-body-parts
  "Expects a seq of maps that have a :name and :size"
  [asym-body-parts]
  (reduce #(into %1 (set [%2 (match-part %2)])) [] asym-hobbit-body-parts))

さいごに

hashMap へのアクセス方法や、List と Vector という2つのデータ構造があったりするが、 今のところ他の言語と比べて大きく異るという感じはしない。

次章から Clojure らしさを感じ取れるだろう。 にしても長い。。。

最後までありがとうございました。

awkでvlookupみたいな表結合させる

はじめに

最近、bash, awk, jqにお世話になっております。

特にawkは使い始めたばかりなので、awkを使ってワンラインでexcelのvlookup的なことをやりたいときにどうすればよいのかわからなくて、excelで頑張ってたりしてました。

でもエンジニアならば、excelなんて使わなくてもコマンドラインでぱぱっと操作できたほうが格好いいじゃない?と思ったので挑戦してみます。

目標

以下の2つの表を想定する。

ユーザ一覧を格納した表(users.csv)

id name
-- ----
1  一郎
2  二郎
3  三郎
4  四郎

何かしらの条件をみたしたIDの表(condition.csv)

id
--
2
4

この2つの表から以下を得たい。

ゴール

id name condition
-- ---- ----
1  一郎 F
2  二郎 T
3  三郎 F
4  四郎 T

conditionにデータがあれば、usersの3列名にTを立て、なければFを立てたいわけです。

sqlでいえば、

SELECT
  id
  , name
  , CASE WHEN condition.id IS NULL THEN 'F' ELSE 'T' END
FROM users
LEFT JOIN CONDITION
ON users.id = CONDITION.id

みたいな操作を実行したいのだ。

回答

awk 'FNR==NR{a[$1]++; next} {print $0, (a[$1]) ? "T" : "F"}' condition.csv users.csv

ね、簡単だね!

説明

なぜ上記コマンドになるのかを説明しようと思う。

FNR==NR{a[$1]; next}

FNR==NRは条件を表現しており、{}内はその条件がマッチしたときの処理を意味する。

よって、FNR==NR, {}内に分けて説明する。

1. FNR==NRとについて

2つのファイルを読み込んで、それぞれNR,FNRと両者の行を出力すると、以下となる。

awk '{print NR, FNR, $0}' condition.csv users.csv
NR FNR $0
-- --- --
1   1   2
2   2   4
3   1   1 一郎
4   2   2 二郎
5   3   3 三郎
6   4   4 四郎

NRは2つのファイルをまとめて何行目を処理しているかを FNRは各ファイルの何行目を処理しているかを表している。

よって、はじめのファイル(condition.csv)が処理中のときは、FNR==NRが真となる。

2. {a[$1]++; next;}

突然登場した a は変数である。var, constなど使わずともいきなり変数を定義できるのである。さらに[$1]とつづくので、この変数は$1をkey名にもつ連想配列になる。
++は、1値を追加せよ(初期値0)という意味になる。

したがって、aという連想配列を宣言して、$1をkeyとして1増加せよという意味である。

next は、以降つづく処理をストップして次の行に進みなさいという意味である。{a[$1]++; next;} {…1} {…2} {…n}と続いたときに{…1}から{…n}の操作は実行されない。

3. {print $0, (a[$1]) ? "T" : "F"}' condition.csv users.csv

今までのを整理すると、 FNR==NR{a[$1]++; next} {...} は、

  1. はじめに読み込んだファイルに対してのみ真となり、
  2. 読み込んだ1列目をkeyとして1追加した連想配列を生成し、
  3. 次に続く処理{…}を実行しない

という意味になる。

残った {print $0, ....} は、FN==FNRが未達成ときに実行される。つまり、NR >= 3のとき、常に処理される。

$0で、users.csvの処理中の行を表示し、 a[$1]は、users.csvの1行目の値を、連想配列aのkeyに指定したときに真となるかどうかを確認して、TまたはFを出力している。

注意

users.csvとcondition.csvの順番を間違えると意味がなくなるので、注意が必要である。

awk 'FNR==NR{a[$1]++; next} {print $0, (a[$1]) ? "T" : "F"}' users.csv condition.csv

結果は、こちらの通り。理由はもうわかりますよね。

id Flag
-- --
2  T
4  T

このやり方を使うときは、必ずSQLでいうRihgt joinになってしまうわけである。users.csvが4行あるのに対して、conditionは2行しかないのでおかしな結果になってしまう。

もっとつぎへ

先程の使用したconditionで該当するユーザが、2,4だけであることが記載されていた。 しかし、そうでないときもあると思います。

たとえば、bmi.csvが、全ユーザのBMI情報を保存しており、25以上ユーザの名前とBMI情報が知りたいということもあります。

全ユーザとBMI情報が記載された表(bmi.csv)id = 2, 4は

id BMI
-- --
1  22
2  40
3  21
4  30
awk 'FNR==NR{a[$1]=$2; next} {if (a[$1] > 25) print $0, a[$1]}' bmi.csv users.csv

今回のように、bmi.csv, users.csvの行数が同じならば、LEFT JOIN的に記述することも可能となります。ちょっと冗長かな?

awk 'FNR==NR{a[$1]=$2; next} {if ($2 > 25) print $1, a[$1],  $2}' users.csv bmi.csv

まとめ

awkをつかって、特定の列をキーに2つの表を結合する方法を整理してみました。

ファイル内容によっては、ファイルの指定順序に気をつけて使ってみてください。

とはいうものの、csvが用意されていたらつかえるわけで、 結局excelスプレッドシートで渡されるようであれば使えないわけですね。

そのあたりはなんとかできないんかな汗

typescript導入したprivateなnpmパッケージの作り方

はじめに

開発の規模を大きくなってくると、共通化したコンポーネントを利用したいこともあると思います。
git submoduleをつかって共通部分を切り出すことも可能ですが、branchの変更忘れてしまうと反映されないので、個人的には好みではないです。 一方privateなnpmパッケージで実現することも可能だと思います。

npm private registoryを利用することも可能ですが、こちらの場合 US $7/月という月額料金がかかってしまいます。 一方、github privateは無料になったので、githubをつかってprivate npmパッケージを作ってみたいと思います。

また共通利用するなら型情報があったほうがありがたいので、typescriptを導入してみたいと思います。

目標

  1. github privateリポジトリの作成
  2. 他のリポジトリからimport確認
  3. npm version を利用してバージョン管理する

1. my private npm moduleの作成

npm moduleの作成

共通ライブラリのnpmパッケージを作っていきます。

  1. npm moduleの作成
mkdir myNpmModule
cd myNpmModule
git init
npm init -y

.gitignoreの設定は割愛します。

  1. typescript設定
npm i -D typescript
npx tsc --init

npx tsc --init で自動生成された.tsconfig.jsonに以下追加してコンパイル設定を修正する。

.tsconfig.json

 {
 ...
 "declaration": true,
 "sourceMap": true,
 "outDir": "./build",
 "rootDir": "./src",
 ...
}

rootDir でbuild対象のファイルを指定し、outDir でbuild後のファイル保存先を指定する。

declaration でビルド時にxxx.d.tsとして型定義ファイルを出力させる。
sourceMap でtsとjsの対応関係をつくっておく。

  1. package.json修正

共通ライブラリの設定を追加します。

package.json

{
  ...
  "main": "build/index.js",
  "files": [
    "build"
  ],
  "scripts": {
    "build": "tsc",
    "prepare": "npm run build"
  },
  ...
}

files ではパッケージ利用側にどのディレクトリ・ファイルをダウンロードさせるかを指定できる。
ただし、package.jsonとREADME.mdは指定しなくてもダウンロードされる。

main でパッケージ利用側がimportコマンドを使用したときにどのファイルをロードするのかを指定する。

scripts では、typescriptのbuildコマンドに加えて、 prepare を設定する。

prepare はパッケージ利用側が、パッケージをインストールする直前に実行させる処理を定義できる。
srcからビルドされたファイルを作成してくれるため、パッケージ管理側はbuildファイルをgit管理しなくてよい。

つまり、利用側で勝手にsrcからbuildディレクトリを生成してくれるため、.gitignoreにbuildディレクトリを指定しなくてよいわけとなります。

  1. 公開モジュール作成
mkdir src
touch src/Person.ts

src/index.ts

class Person {
  private name: string;
  private age: number;

  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
      return this.name;
  }
}

export { Person };
git add .
git commit -m 'my first commit';

github private repoとして登録

githubページより、新しいリポジトリ作成から、privateにチェックをつけて作成する.

f:id:poppon555:20200405122203p:plain

git remote add origin git@github.com:karuta0825/myNpmModule.git
git push -u origin master

f:id:poppon555:20200405122217p:plain

buildディレクトリ管理しなくてよしです。

2. 他のリポジトリからimport確認

package.json

{
  "dependencies": {
   ...
   "myNpmModule": "git+ssh://git@github.com:karuta0825/myNpmModule.git",
   ...
  },
}

利用側のpackage.jsonのdepenenciesで、使用したいprivate moduleを指定する。git+ssh//に続けて、githubのclone or dowloaddに表示されているパスをコピーすればよき。

f:id:poppon555:20200405122242p:plain

npm i を実行すると、node_modulesに作成したprivate packageが作成されてます。

.
├── node_modules
│   ├── @types
│   ├── myNpmModule <-- 追加されてる
│   └── typescript
├── package-lock.json
├── package.json
├── sample.ts
└── tsconfig.json

node_modulesのmyNpmModuleのディレクトリ構成も設定したとおり。

myNpmModule
├── build
│   ├── index.d.ts
│   ├── index.js
│   └── index.js.map
└── package.json

package.jsonのfilesに指定したbuildディレクトリのみがあり、そこには、型定義ファイルのindex.d.tsとコンパイルされたjsとsourceMapの.mapファイルがある。

では、実際にインストールした共通パッケージを利用できるか確認してみます。

f:id:poppon555:20200405123146p:plain importでき、候補も表示されています。

3. my npm moduleをバージョン管理する

共通パッケージを使用していくならば、バージョン管理をしておくも考えられると思います。
ということで、npmのバージョン管理設定も説明しておきます。
とっても簡単。commit後に npm version を使用するだけです。

import dayjs from "dayjs";

class Person {
  private name: string;
  private age: number;
  public constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  public call(): string {
    return this.name;
  }

  // 追加
  public tellBirthYear(): string {
    return dayjs().subtract(this.age, "year").format("YYYY");
  }
}

git commitする

git add .
git commit -m 'add a method to Person class.'

npm versionのあとに、major, minor, patchのいずれかを指定できます。
majorだと、1.0.0 => 2.0.0
minorだと、1.0.0 => 1.1.0
patchだと、1.0.0 => 1.0.1
にバージョンが変わる。

package.jsonのversionや、git logが変更されていること確認できます。

npm version minor

git log --oneline
// 6a0bb79 1.1.0
// 3d505ff add a method to Person class.
// 351675f my first commit

package.json

{
  ...
  "version": "1.1.0",
  ...
}

git pushして、利用側でnpm updateをして確認すると、

npm update

+ myNpmModule@1.1.0
added 1 package from 1 contributor, updated 3 packages and audited 5 packages in 19.281s
found 0 vulnerabilities

1.1.0のmyNpmModuleが利用できるようになりました。

実際下図の通り、追加したメソッドが候補に上がるようになります。

f:id:poppon555:20200405122235p:plain

まとめ

github privateを利用して、typescript導入npmパッケージを作成しました。
また npm version を利用してバージョン管理する方法をかんたんに説明しました。

ライブラリとして使用されるならば、以下のことも今後対応方法見つけられたらと思ってます。

  • パッケージのmin化 現状だとsrcのディレクトリ構成のままbuildディレクトができるので、一本にまとめたほうがよい。
  • material-uiやlodashのようにmyNpmModue/Personみたいな感じでロードファイルを指定できる

参考にしたサイト

https://dev.classmethod.jp/articles/private-npm-modules-to-package-json/https://medium.com/cameron-nokes/the-30-second-guide-to-publishing-a-typescript-package-to-npm-89d93ff7bccd

goで継承をつかう

はじめに

goを勉強始めるとjavaなどで学習してきたオブジェクト指向の考え方を違う場面に出くわす。
たとえば、goでは継承できないことである。
しかし委譲を使えば、同じことを実現できるのでその整理をしようと思う。

仕様整理

下図のような関係を考える。 Personクラスはスーパークラスで、Manクラスをそのサブクラスという関係である。
Personクラスは、名前と年齢をメンバ変数にもち、getName()で名前を取得でき、oldAge()でひとつ年を増やせるメソッドをもつ。
一方Manクラスは、性別("M", "S")をもち、男ならばcall()メソッドで"Mr. 名前"と返す。

f:id:poppon555:20191222141722p:plain

goで実装

goではクラス/メソッドではなく、struct/receiverと呼ぶが、クラスを使う。

package main

// スーパークラス
type Person struct {
  name string
  age  int
}

// コンストラクタ
func newPerson(name string, age int) *Person {
  return &Person{
    name,
    age,
  }
}

func (p *Person) getName() string {
  return p.name
}

func (p *Person) oldAge() {
  p.age = p.age + 1
}

// サブクラス
type Man struct {
  // (1) Personポインタ 
  *Person
  sex string
}

// コンストラクタ
func NewMan(name string, age int) *Man {
  return &Man{
    // (2)  ポインタ変数にインスタンスを格納
    Person: newPerson(name, age),
    sex:    "M",
  }
}

func (m *Man) call() string {
  return "Mr." + m.name
}

(1) Personポインタを渡すことでManインスタンスから直にPersonインスタンスにアクセスできる 。
man.Person.getName() ではなく、man.getName() が可能となる。
Personの部分をショートカットしてgetName()が呼べるので、あたかも継承してるかのように使える。
もし、person Person とメンバ変数名を明記すると、man.Person.getName()でアクセスしないといけないので継承ぽさは消える。

(2) サブクラスでスーパクラスのインスタンスを作成するとき、*を外したPersonにインスタンスを格納する。

testコードでうまくいくことを確認できる

package main

import "testing"

func TestNewMan(t *testing.T) {

  t.Run("スーパクラスのメソッドgetName()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.getName() != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("スーパクラスのメソッドoldAge()が呼べる", func(t *testing.T) {
    man := NewMan("adam", 10)
    man.oldAge()
    if man.age != 11 {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスからスーパークラスのメンバ変数nameにアクセスできる", func(t *testing.T) {
    man := NewMan("adam", 10)
    if man.name != "adam" {
      t.Fatal("err")
    }
  })

  t.Run("サブクラスの定義メソッドcall()が呼べてMr.adamと返す", func(t *testign.T) {
    man := NewMan("adam", 10)
    if man.call() != "Mr.adam" {
      t.Fatal("err")
    }
  })
}
PASS
ok      github.com/karuta0825/study 0.262s

最後に

オブジェクト指向脳の人が、goで継承ぽいものを使う方法を説明しました。
goはコンパイル言語なのに、スクリプト言語のように開発すすめるのはいい言語だなと思います。
javaのライブラリ管理とか面倒だもの。
もっともgenericsが使えないのは、悔しいのですが。。。
そんな感じでgoらしさというのがまだまだわからずなのですが、来年はgoへの理解を深めていきたい!

述語論理をつかったSQL

はじめに

僕は、SQLが苦手である。 普段使い慣れている言語とは別の考えがSQLには必要なのであろうと痛感している。

ということで、達人に学ぶ SQL徹底指南書を使って最近SQLの考え方を学習中。
今回は、SQLで数列を扱うの備忘録を残す。

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

達人に学ぶ SQL徹底指南書 (CodeZine BOOKS)

問題

以下の座席表から空席が 3 つ連続している部分を求めろ たとえば、(3,4,5), (7,8,9)となる。

id status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

データの準備

CREATE TABLE seats
(
    seat   INT NOT NULL,
    status VARCHAR(10),
    PRIMARY KEY (seat)
);

INSERT INTO seats
VALUES (1, ""),
       (2, ""),
       (3, ""),
       (4, ""),
       (5, ""),
       (6, ""),
       (7, ""),
       (8, ""),
       (9, ""),
       (10, ""),
       (11, ""),
       (12, ""),
       (13, ""),
       (14, ""),
       (15, "");

回答

SELECT
  s1.seat as '始点',
  s2.seat as '終点'
FROM seats s1,
     seats s2
WHERE s1.seat + 2 = s2.seat
  AND NOT exists(
        SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '')
    );
始点 終点
3 5
7 9
8 10
9 11

考え方

始点、終点を s1, s2 の自己結合によって得、その間に s3 のすべての status が空を満たすというクエリをつくる方針をとる。

すべての〇〇が XX という条件を満たすというクエリを記述するのにAll演算子を使おうと考えるが、MySQLにはAll演算子は用意されていない。

なので、EXSITS演算子を利用して All と同じ意味をもつ式を記述することになる。
いきなりだが、以下は述語論理から成立する式である。

1. ∀xF(x) => ¬∃x¬F(x)
2. ∃xF(x) => ¬∀x¬F(x)

∀ は All を意味するので、今回は 1 のパターンである。
この考え方を日本語で整理すると、
始点・終点間のすべての座席が空である

始点・終点間に空ではない(a)座席は存在しない(b)
と表現可能となる。

(a), (b)をクエリで記述すると

  1. SELECT * FROM seats s3 WHERE (s3.seat BETWEEN s1.seat AND s2.seat) AND (s3.status <> '空')
  2. NOT exists(1.)

となる。

よって、上記回答の通りになる。

おわりに

にしてもSQLはクエリに悩むよりも、データの準備が大変だ。。。 もっと楽にデータ準備できないものかと感じる