誰しもお世話になっているが目立たない、あの「関」
「今年もアドベントカレンダーの季節だなぁ。こういうの苦手だから先に何書くか考えておこう。よし、最近ChromebookでElectron開発したときの開発環境構築についてでも書こう!手順をちゃんとメモっておいて、OK完璧!」
そして月日は流れ....
今年のリブセンスの Advent Calendar はカレンダーごとに漢字1字のテーマがあります。
なん....だと.....?
閑話休題。
株式会社リブセンスで転職ナビというサービスを担当している佐藤です。
このカレンダーは「関」にまつわる何かを書いていきます。
ということで関なんですが、真っ先に思い浮かぶのは関数だと思うんですよ。
しかしどうせなら他の人と被らなくて、かつみんなにとって身近な関をテーマとして取り上げたい。
ということで、”関係モデル”について書いていこうかなと思います。
関係モデルとは
何はともあれWikipedia先生(毎年寄付している)に聞いてみると、
関係モデル(かんけいモデル、リレーショナルモデル、英語: relational model)はエドガー・F・コッドが集合論と述語論理に基づいて考案したデータベースモデルであり、関係データベース(リレーショナルデータベース)の基礎となっている。
とのこと。
簡単に言ってしまうと、みんな大好きリレーショナルデータベースの数学的な土台となっている理論です。
このモデルをもとにした演算体系が関係代数というもので、みんなが好きだったり嫌いだったりするSQLは、関係代数の一実装という血筋に当たります。
リレーショナルデータベースは、もはや私のような30代のおっさんエンジニアにとっても空気のように当たり前の存在です。
そのせいか関係モデルは話題に上がりづらい気がしますが、ドキュメント指向データベースや列指向データベース、グラフデータベースのような、リレーショナルデータベース以外のデータベースシステムを理解する上でも、リレーショナルデータベースの基礎を理解しておくことにはとても意義があるように思います。
”関係”の定義を実装しながら理解する
さて、ということで関係モデルについて理解したいわけなので、再びWikipedia先生(毎年寄付しているが通知が止まらない)にご登場いただきましょう。
関係モデル(リレーショナルモデル)における基本的な前提は、あらゆるデータは n 項(n-ary)の関係(リレーション)で表現されるということである。 数学における関係は二項関係をいうが、関係モデルでは関係の概念を n 項に拡張している(nは0もしくは正の整数)。 一つの n 項の関係は、n個の定義域(ドメイン、後述する)の直積集合の部分集合である。
困ったことになりました。
センター試験が数1Aだった文系人間にはWikipedia先生(来年も寄付するから通知は止めてほしい)の言っていることが分かりません。
こういうときはまず自分のわかる言葉に翻訳することが大事ですね。
ということでこの部分をClojureで表現してみましょう。
”定義域”の実装
”定義域”というのは、とある属性が取りうる値の範囲のことらしいです。
とりあえずサンプルらしく人間関係(リレーショナルデータベースでいうpeopleテーブル)を作りたいので、属性としては名前、年齢、性別を用意しましょう。
年齢という属性が取りうる値の範囲は整数なので簡単ですね。
(def integers (iterate inc 0))
↓のようにすれば、ただただ第二引数と同じ整数を返します。
boot.user=> (nth integers 100)
100
Clojureには無限シーケンスがあるので、こういうとき非常に便利ですね。
次は性別です。
これも簡単ですね。
(def genders '(:male :female))
さて、問題は名前です。
名前という属性が取りうる値の範囲は、世の中に存在する文字という文字の集合n個の直積集合になってしまいそうです。
さすがにそれは辛いので、アルファベットで妥協しましょう、妥協させてくださいm(_ _)m
(def alphabets
(map char
(concat
(range (int \A) (inc (int \Z)))
(range (int \a) (inc (int \z))))))
これでAから始まってzで終わる52個のアルファベットシーケンスができました。
boot.user=> (first alphabets)
\A
boot.user=> (last alphabets)
\z
boot.user=> (count alphabets)
52
このアルファベットを文字列にしてみましょう。
まずは桁数を引数にとって、アルファベット集合の桁数分の直積集合を作ります。
(def gen-strs
(memoize
(fn [digit]
(cond
(< digit 1) ""
(= digit 1) (map str alphabets)
:else (mapcat
(fn [a]
(map (fn [b] (str a b)) alphabets))
(gen-strs (dec digit)))))))
3桁のアルファベットは以下のように表現されます。
boot.user=> (first (gen-strs 3))
"AAA"
boot.user=> (last (gen-strs 3))
"zzz"
boot.user=> (count (gen-strs 3))
140608
すごいですね。
たった3桁のアルファベットだけで、525252=140,608通りもの文字列を表現することができるようです。
あとはこれを無限シーケンスに仕立てれば、無限に続くアルファベット文字列の集合を表現できます。
(def strings (->> (range) (mapcat gen-strs)))
boot.user=> (first strings)
"A"
boot.user=> (nth strings 100000)
"jyE"
アルファベットを辞書順に並べていって出来上がる100,000番目の文字列は”jyE”であることがわかりました。
どうでもいいですね。
”n個の定義域の直積集合”の実装
名前、年齢、性別の定義域が完成したので、”n個の定義域の直積集合”を作りましょう。
人間関係における組(リレーショナルデータベースでいう行)は人間ですので、人間を定義します。
(defrecord Person [name age gender])
あとはこの人間の各属性に先程の定義域を当てはめて無限シーケンスに仕立てれば、人間関係が構築できるはずです(言いたかった)。
リスト内包表記のおかげで、直積の表現が直感的ですね。
(def people (for [name strings
age integers
gender genders]
(Person. name age gender)))
こい!
boot.user=> (nth people 10)
#boot.user.Person{:name "A", :age 5, :gender :male}
boot.user=> (nth people 1000)
#boot.user.Person{:name "A", :age 500, :gender :male}
ん?
boot.user=> (nth people 100000)
#boot.user.Person{:name "A", :age 50000, :gender :male}
boot.user=> (nth people 100001)
#boot.user.Person{:name "A", :age 50000, :gender :female}
あ”
やってしまいました。
内部的にはgender→age→nameの順番にループが回っており、かつageが無限に続くため、ひたすら高齢化の一途を辿る男女Aさんの集合が出来上がりました。
やはり現実的には年齢に長さの制限を持たせる必要がありますね。
無限の扱いに長けたClojureにも、不老不死社会の実現は難しいようです。
2017年12月現在の世界最高齢者は117歳なようですので、年齢は0〜117の範囲としましょう。
(def people (for [name strings
age (range 0 117)
gender genders]
(Person. name age gender)))
boot.user=> (nth people 10)
#boot.user.Person{:name "A", :age 5, :gender :male}
boot.user=> (nth people 1000)
#boot.user.Person{:name "E", :age 32, :gender :male}
boot.user=> (nth people 100000)
#boot.user.Person{:name "HL", :age 41, :gender :male}
boot.user=> (nth people 100001)
#boot.user.Person{:name "HL", :age 41, :gender :female}
やりました!
記念すべき100,000番目の人間はHLさん41歳男性(住所不定)のようです!
”n個の定義域の直積集合の部分集合”の実装
”n個の定義域の直積集合”が表現できたので、最後に”n個の定義域の直積集合の部分集合”を実装できれば、関係モデルにおける関係(リレーショナルデータベースでいうテーブル)が理解できそうです。
部分集合の作り方にも色々あると思いますが、すでにだいぶ長くなってしまったので雑に順序だけで作りましょう。
(defn gen-relation [start end]
(set
(->> people
(drop start)
(take (- end start)))))
あとは何番目から何番目までの人間をグルーピングするかの指定だけですね。
(def relation (gen-relation 10 20))
boot.user=> (pprint relation)
#{{:name "A", :age 8, :gender :male}
{:name "A", :age 9, :gender :female}
{:name "A", :age 7, :gender :female}
{:name "A", :age 7, :gender :male}
{:name "A", :age 9, :gender :male}
{:name "A", :age 6, :gender :male}
{:name "A", :age 6, :gender :female}
{:name "A", :age 5, :gender :female}
{:name "A", :age 8, :gender :female}
{:name "A", :age 5, :gender :male}}
これが”n個の定義域の直積集合の部分集合”、つまり関係なんですね、感動的です。
順序がグチャグチャになっていますが、これはset関数によって順序の概念のない本当の意味での集合になったためです。
関係代数はSQLのお母さん
さて、こうしてClojureの実装によって関係への理解を深めると、以下のような疑問が湧いてくるはずです。
「え?なにこれ?茶番?なんの役に立つのこれ?ここまで読んだ時間返してくれる?」
大丈夫です。関係の概念だけではごはんを食べることができませんが、関係同士の演算ができればデータを操作して社会の役に立ち、ごはんを食べていくことができます。
Clojureのset APIには先述の関係代数実装がありますので、SQLのように集合を操作することができます。
まずは操作するに値するだけの大きな関係を定義します。
(def huge-relation (gen-relation 100000 1000000))
そしてSQLさながらに関係演算をすれば、
boot.user=> (pp/pprint
#_=> (sort-by
#_=> :age
#_=> (set/project
#_=> (->> huge-relation
#_=> (set/select #(> 10 (:age %)))
#_=> (set/select #(= :male (:gender %)))
#_=> (set/select #(= "AAA" (:name %))))
#_=> [:name :age])))
({:name "AAA", :age 0}
{:name "AAA", :age 1}
{:name "AAA", :age 2}
{:name "AAA", :age 3}
{:name "AAA", :age 4}
{:name "AAA", :age 5}
{:name "AAA", :age 6}
{:name "AAA", :age 7}
{:name "AAA", :age 8}
{:name "AAA", :age 9})
想定通りの組(行)が返ってきます。
上記の実装は、ClojureがわからなくてもSQLがわかればなんとなく読める気がしませんか?
これならなんとなくエンジニアとしてごはんを食べていってもいい感じがしますよね?
まとめ
- 誰かがテーブルって言ったら「それって”n個の定義域の直積集合の部分集合”をRDBMS上において実装したもののことだよね?家具じゃなくて。」とミサワ風にいうと良い
- Clojureは無限を表現するのが得意な素晴らしい言語だが、不老不死社会の実現まではできない
- アルファベットを辞書順に並べていって出来上がる100,000番目の文字列は”jyE”
- 株式会社リブセンスでは主にRuby/PHPのエンジニアを募集していますが、Clojureが好きな方が来ても損はしないと思います(個人の見解です)
ソースコード全文
ビルドにはBootを利用しています。
#!/usr/bin/env boot
(ns boot.user
(:require [clojure.string :as str]
[clojure.set :as set]
[clojure.pprint :as pp]))
(def alphabets
(map char
(concat
(range (int \A) (inc (int \Z)))
(range (int \a) (inc (int \z))))))
(def gen-strs
(memoize
(fn [digit]
(cond
(< digit 1) ""
(= digit 1) (map str alphabets)
:else (mapcat
(fn [a]
(map (fn [b] (str a b)) alphabets))
(gen-strs (dec digit)))))))
(def strings (->> (range) (mapcat gen-strs)))
(def integers (iterate inc 0))
(def genders '(:male :female))
(defrecord Person [name age gender])
(def people (for [name strings
age (range 0 117)
gender genders]
(Person. name age gender)))
(defn gen-relation [start end]
(set
(->> people
(drop start)
(take (- end start)))))
(def huge-relation (gen-relation 100000 1000000))
(defn -main [& args]
(pp/pprint
(sort-by
:age
(set/project
(->> huge-relation
(set/select #(> 10 (:age %)))
(set/select #(= :male (:gender %)))
(set/select #(= "AAA" (:name %))))
[:name :age]))))