6K Views
October 25, 24
スライド概要
書籍『データ指向プログラミング』( https://www.shoeisha.co.jp/book/detail/9784798179797 )は、プログラミング言語Clojureにおいて典型的なプログラミングスタイルの根幹にある考え方を他言語でも応用できる形で抽出し紹介する試みであるということができます。
Clojureを実務や趣味で継続的に利用するとともに比較的最近Kotlinに再入門した立場から、この本で提示されている「データ指向プログラミング」というプログラミングスタイルを概説しながらKotlinらしい実践の可能性について考察します。
データ指向プログラミングの原則:
- 原則 #1: コードをデータから切り離す
- 原則 #2: データを汎用的なデータ構造で表す
- 原則 #3: データはイミュータブルである
- 原則 #4: データスキーマをデータ表現から切り離す
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
Kotlin Meets Data-Oriented Programming Kotlinで実践する「データ指向プログラミング」 1
lagénorhynque カマイルカ 株式会社スマートラウンドのシニアエンジニア 主要技術スタック のたまごスポンサー の運営企業 関数型⾔語 関数型プログラミングが⼤好き 仕事や趣味で などに⻑く 触れてきた 現職で初めて を仕事で読み書きするように なって半年ほど 2
Kotlin Fest 2024への私 のCfP 3
Kotlin Meets Data-Oriented Programming: Kotlinで実践する「データ指向プログラミング」 書籍『データ指向プログラミング』は、プログラミン グ⾔語 において典型的なプログラミングスタ イルの根幹にある考え⽅を他⾔語でも応⽤できる形で 抽出し紹介する試みであるということができます。 を実務や趣味で継続的に利⽤するとともに⽐ 較的最近 に再⼊⾨した⽴場から、この本で提⽰ されている「データ指向プログラミング」というプロ グラミングスタイルを概説しながら らしい実践 の可能性について考察します。 4
データ指向プログラミングとは への適⽤可能性を探る まとめ 5
1. データ指向プログラミングとは 6
書籍『データ指向プログラミング』まえがき 特別なのは機能ではなく原則だと いうことで意⾒が⼀致した。 の基本原則を抜き出そうとしていた 私たちは、実際には、それらの原則 を他のプログラミング⾔語に応⽤で きることに気づいた。本書の構想が 沸いてきたのはそのときだった。私 が でとても気に⼊っている点 を世界中の開発者コミュニティに伝 えたかった。 7
に魅了された著者が ⾔語 コミュニテ ィで⼀般的なプログラミングスタイルのエッセンス を他⾔語でも応⽤できる形で抽出しようとした本 古典的な に対するアンチテーゼといえる 他の⾔語や設計思想と必ずしも馴染まず批判され ることもある 批判も理解できる である の視点から捉え直し、 現実的な応⽤の可能性を考えたい での 8
(ちなみに) Clojureとは 動的型付き ⾮オブジェクト指向 関数型⾔語 古典的な には当初から批判的 モダンに再設計された 系⾔語 年に登場した ⾔語 年〜 年〜 作者 のプレゼン は他 のコミュニティでも多少知られているかも ⾔語の設計にも⾊濃く反映されている、 の重要性について語っている 9
データ指向プログラミングの背景 古典的な のアプローチに対する問題意識 必要以上の複雑さを⽣みがち いろいろな要素が絡み合っている 硬直的で柔軟性に⽋けることがある フレームワークに頼らざるを得なかったり → もっとシンプル があるはず に情報を扱うアプローチ そうして⽣まれたのが ⾔語でもある 10
データ指向プログラミングの原則 原則 コードをデータから切り離す 原則 データを汎⽤的なデータ構造で表す 原則 データはイミュータブルである 原則 データスキーマをデータ表現から切り離す のプログラミングスタイルそのもの 11
データ指向プログラミングと親和性の⾼い技術 ロックフリー な楽観的並⾏性制御 の 純粋な関数、不変なデータとの相性が良い 状態変化のタイムトラベル、リプレイ 状態が不変の汎⽤データ構造で表現されていれば 極めて簡単 永続データ構造 不変 かつ永続的 であれば 効率も犠牲になりにくい 『純粋関数型データ構造』 12
2. Kotlinへの適⽤可能性を探る 13
データ指向プログラミングの原則(再掲) 原則 コードをデータから切り離す 原則 データを汎⽤的なデータ構造で表す 原則 データはイミュータブルである 原則 データスキーマをデータ表現から切り離す のプログラミングスタイルそのもの 14
原則 #1: コードをデータから切り離す 15
Clojureの場合
関数の定義 データはマップ { } で表す
(ns dop-examples)
; 名前空間(namespace)の定義
(defn make-author [first-name last-name num-of-books]
{:first-name first-name
:last-name last-name
:num-of-books num-of-books})
(defn full-name [{:keys [first-name last-name]}]
(str first-name " " last-name))
(defn prolific? [{:keys [num-of-books]}]
(or (some-> num-of-books (> 100))
false))
16
利⽤例 ;; FYI: プロンプトの `dop-examples` は現在の名前空間(モジュール) ;; そこでdef/requireされているものは⾮修飾名で参照できる dop-examples> (let [data (make-author "Isaac" "Asimov" 500)] (full-name data)) "Isaac Asimov" 17
「レコード」 ≒ を定義することもできる (defrecord Author [first-name last-name num-of-books]) (defn make-author' [first-name last-name num-of-books] (->Author first-name last-name num-of-books)) マップとレコードはインターフェースが共通している ため、関数 full-name はそのまま使える dop-examples> (let [data (make-author' "Isaac" "Asimov" 500)] (full-name data)) "Isaac Asimov" 18
(書籍より) 利点とコスト 主な利点 コードをさまざまなコンテキストで再利⽤できる コードを単体でテストできる システムがあまり複雑にならない傾向にある 主なコスト どのコードがどのデータにアクセスできるのかを 制御できない パッケージ化がない システムを構成するエンティティの数が増える 19
Kotlinの場合
データと関数の定義
data class Author(
val firstName: String,
val lastName: String,
val numOfBooks: Int?,
)
object NameCalculation {
fun fullName(data: Author): String =
"${data.firstName} ${data.lastName}"
}
object AuthorRating {
fun isProlific(data: Author): Boolean =
data.numOfBooks?.let { it > 100 } ?: false
}
クラス オブジェクトは「モジュール」でもある
20
利⽤例 > val data = Author("Isaac", "Asimov", 500) > NameCalculation.fullName(data) res2: kotlin.String = Isaac Asimov らしい ドット記法が必要であれば > fun Author.fullName(): String = NameCalculation.fullName(this) > data.fullName() res4: kotlin.String = Isaac Asimov 21
構造的型 や拡張可能レコード の代わりに interface Namable { val firstName: String val lastName: String } data class Author2( override val firstName: String, override val lastName: String, val numOfBooks: Int?, ) : Namable object NameCalculation2 { fun fullName(data: Namable): String = "${data.firstName} ${data.lastName}" } インターフェースを定義することで特定の具象型 Author2 に縛られなくすることはできる
原則 #2: データを汎⽤的なデータ構造で表す 23
Clojureの場合
;; マップ(リテラルで作成)
dop-examples> {:first-name "Isaac"
:last-name "Asimov"
:num-of-books 500}
{:first-name "Isaac", :last-name "Asimov", :num-of-books 500}
;; レコード(コンストラクタ関数で作成)
dop-examples> (->Author "Isaac" "Asimov" 500)
{:first-name "Isaac", :last-name "Asimov", :num-of-books 500}
;; どちらも Associative (連想データ)インターフェースを実装している
dop-examples> (associative? {:first-name "Isaac"
:last-name "Asimov"
:num-of-books 500})
true
dop-examples> (associative? (->Author "Isaac" "Asimov" 500))
true
連想データに対するあらゆるオペレータ 関数 マクロ
特殊形式 が利⽤できる
24
(書籍より) 利点とコスト 主な利点 特定のユースケースに限定されないジェネリック 関数を利⽤できる 柔軟なデータモデル 主なコスト パフォーマンスが少し低下する データスキーマがない コンパイル時にデータの有効性が確認されない 静的に型付けされる⾔語では、明⽰的な型変換 キャスト が必要になることがある 25
Kotlinの場合 構造的型や拡張可能レコードのサポートがなく、後 述のデータスキーマを記述するのも⼀般的ではない 主体で具体的な型としてデータを定義 するのが妥当そう 適宜インターフェース化しうる > data class Author( val firstName: String, val lastName: String, val numOfBooks: Int?, ) > Author("Isaac", "Asimov", 500) res6: Line_0.Author = Author(firstName=Isaac, lastName=Asimov , numOfBooks=500) 26
原則 #3: データはイミュータブルである 27
Clojureの場合
マップほかネイティブのデータ構造は不変
;; 関数 assoc は連想データのエントリーをupsertする
dop-examples> (assoc {:first-name "Isaac"
:last-name "Asimov"
:num-of-books 500}
:num-of-books 100)
{:first-name "Isaac", :last-name "Asimov", :num-of-books 100}
dop-examples> (let [data {:first-name "Isaac"
:last-name "Asimov"
:num-of-books 500}
data' (assoc data
:num-of-books 100)]
(identical? data data'))
false ; 参照が異なる別のデータ(永続データなので内部的には共有がある)
安全であり 実⽤上 ⼗分に効率的でもある
28
(書籍より) 利点とコスト 主な利点 すべての関数から⾃信を持ってデータにアクセス できる コードの振る舞いが予測可能である 等価のチェックが⾼速である 並⾏処理の安全性が⾃動的に確保される 主なコスト パフォーマンスが低下する 永続的なデータ構造のためのライブラリが必要で ある 29
Kotlinの場合 再代⼊不可な プロパティとほぼ不変 単に の場合あり な データ構造を利⽤することはできる > val data1 = Author("Isaac", "Asimov", 500) > val data2 = data.copy(numOfBooks = 100) > data1 === data2 res9: kotlin.Boolean = false // 参照が異なる別のデータ ミュータビリティとイミュータビリティの狭間 関 数型⾔語使いから⾒た コレクション 30
原則 #4: データスキーマをデータ表現から切り離す 31
Clojureの場合
契約プログラミングライブラリ
が標準で
含まれており、広く使われている
(ns dop-examples
(:require
[clojure.spec.alpha :as s]
[clojure.string :as str]))
; clojure.specの導⼊
(defn make-author [first-name last-name num-of-books]
{:first-name first-name
:last-name last-name
:num-of-books num-of-books})
(defn full-name [{:keys [first-name last-name]}]
(str first-name " " last-name))
(defn prolific? [{:keys [num-of-books]}]
(or (some-> num-of-books (> 100))
false))
32
データの仕様 (s/def ::name (s/and string? (complement str/blank?) ; 空⽂字列/空⽩のみでない #(<= (count %) 100))) ; ⻑さが100以下 (s/def ::first-name ::name) (s/def ::last-name ::name) (s/def ::num-of-books (s/nilable ; nilになりうる (s/and nat-int? ; (0を含む)⾃然数 #(<= % 10000)))) ; 10000以下 (s/def ::author (s/keys :req-un [::first-name ::last-name] :opt-un [::num-of-books])) 述語 ; 列挙したキーを必ず含む ; 列挙したキーを任意で含む により値レベルの制約まで記述できる 33
関数の仕様 (s/fdef make-author :args (s/cat :first-name ::first-name :last-name ::last-name :num-of-books ::num-of-books) :ret ::author) ; 戻り値 ; 第1引数 ; 第2引数 ; 第3引数 (s/fdef full-name :args (s/cat :data (s/keys :req-un [::first-name ::last-name])) :ret string?) (s/fdef prolific? :args (s/cat :data (s/keys :req-un [::num-of-books])) :ret boolean?) データの仕様で関数の⼊出⼒仕様を記述できる 34
データに対する検証
;; 必須の :last-name キーが⽋けたマップの場合
dop-examples> (s/explain ::author {:first-name "Isaac"
:num-of-books 500})
{:first-name "Isaac", :num-of-books 500} - failed:
(contains? % :last-name) spec: :dop-examples/author
nil
35
;; :num-of-books の値が負の数の場合
dop-examples> (s/explain ::author {:first-name "Isaac"
:last-name "Asimov"
:num-of-books -1})
-1 - failed: nat-int? in: [:num-of-books] at: [:num-of-books
:clojure.spec.alpha/pred] spec: :dop-examples/num-of-books
-1 - failed: nil? in: [:num-of-books] at: [:num-of-books
:clojure.spec.alpha/nil] spec: :dop-examples/num-of-books
nil
36
関数 引数 に対する検証 ;; 関数の引数に対するチェックを有効化 dop-examples> (clojure.spec.test.alpha/instrument) [dop-examples/make-author dop-examples/full-name dop-examples/prolific?] ;; 第1引数(first-name)が空の場合 dop-examples> (make-author "" "Asimov" 500) Execution error - invalid arguments to dop-examples/make-author at (REPL:103). "" - failed: (complement blank?) at: [:first-name] spec: :dop-examples/name 37
;; キーにtypoがある(必須の :first-name キーがない)場合
dop-examples> (full-name {:fist-name "Isaac"
:last-name "Asimov"})
Execution error - invalid arguments to dop-examples/full-name
at (REPL:106).
{:fist-name "Isaac", :last-name "Asimov"} - failed:
(contains? % :first-name) at: [:data]
38
(書籍より) 利点とコスト 主な利点 検証すべきデータを⾃由に選択できる オプションフィールドを利⽤できる ⾼度なデータ検証条件を利⽤できる データモデルを⾃動的に可視化できる 主なコスト データとスキーマの結び付きが弱い パフォーマンスが少し低下する 39
Kotlinの場合 依存型 などのサポートはないので、 型で表現しがたい仕様はアサーションで検証 data class Author3( val firstName: String, val lastName: String, val numOfBooks: Int?, ) { init { require(firstName.isNotBlank() && firstName.length <= 100) require(lastName.isNotBlank() && lastName.length <= 100) require(numOfBooks?.let { it in 0..10000 } ?: true) } } プリミティブなデータをリッチにすることもできる 40
3. まとめ 41
「データ指向プログラミング」は ⾔語 コミ ュニティに由来するプログラミングスタイル その⼒が最も効果的に発揮されるのは を使うときかも もいいぞ のような静的型付きオブジェクト指向⾔語で も こそ 参考になる⽰唆を含んでいる そもそも近年の新興⾔語ではクラスベースの を押し出していない印象がある。クラスと いう枠組みでのモデル化に囚われる必要はない 42
Further Reading データ指向プログラミング 本発表のサンプルコード 『データ指向プログラミング』 新刊『データ指向プログラミング』から「はじめ に」を公開! オブジェクト指向との対⽐も語ら れる (コードジン) データ指向プログラミングの真実をお話しします 43
Clojure 公式サイト による解説 と はどう違う? を解説 ログミー の設計に⾒る という考え⽅ を解説 ログミー の世界観 紙箱 44