Slide 1

Slide 1 text

Kotlin Meets Data-Oriented Programming Kotlinで実践する「データ指向プログラミング」 1

Slide 2

Slide 2 text

のシニアエンジニア 主要技術スタック のたまごスポンサー の運営企業 関数型⾔語 関数型プログラミングが⼤好き 仕事や趣味で などに⻑く 触れてきた 現職で初めて を仕事で読み書きするように なって半年ほど lagénorhynque カマイルカ 株式会社スマートラウンド 2

Slide 3

Slide 3 text

Kotlin Fest 2024への私 のCfP 3

Slide 4

Slide 4 text

書籍 は、プログラミン グ⾔語 において典型的なプログラミングスタ イルの根幹にある考え⽅を他⾔語でも応⽤できる形で 抽出し紹介する試みであるということができます。 を実務や趣味で継続的に利⽤するとともに⽐ 較的最近 に再⼊⾨した⽴場から、この本で提⽰ されている「データ指向プログラミング」というプロ グラミングスタイルを概説しながら らしい実践 の可能性について考察します。 Kotlin Meets Data-Oriented Programming: Kotlinで実践する「データ指向プログラミング」 『データ指向プログラミング』 4

Slide 5

Slide 5 text

データ指向プログラミングとは への適⽤可能性を探る まとめ 5

Slide 6

Slide 6 text

1. データ指向プログラミングとは 6

Slide 7

Slide 7 text

書籍 まえがき 『データ指向プログラミング』 特別なのは機能ではなく原則だと いうことで意⾒が⼀致した。 の基本原則を抜き出そうとしていた 私たちは、実際には、それらの原則 を他のプログラミング⾔語に応⽤で きることに気づいた。本書の構想が 沸いてきたのはそのときだった。私 が でとても気に⼊っている点 を世界中の開発者コミュニティに伝 えたかった。 7

Slide 8

Slide 8 text

に魅了された著者が ⾔語 コミュニテ ィで⼀般的なプログラミングスタイルのエッセンス を他⾔語でも応⽤できる形で抽出しようとした本 古典的な に対するアンチテーゼといえる 他の⾔語や設計思想と必ずしも馴染まず批判され ることもある 批判も理解できる である の視点から捉え直し、 での 現実的な応⽤の可能性を考えたい 8

Slide 9

Slide 9 text

(ちなみに) Clojureとは 動的型付き ⾮オブジェクト指向 年に登場した 年〜 年〜 作者 のプレゼン は他 のコミュニティでも多少知られているかも ⾔語の設計にも⾊濃く反映されている、 の重要性について語っている 関数型⾔語 古典的な には当初から批判的 モダンに再設計された 系⾔語 ⾔語 9

Slide 10

Slide 10 text

データ指向プログラミングの背景 古典的な のアプローチに対する問題意識 必要以上の複雑さを⽣みがち いろいろな要素が絡み合っている 硬直的で柔軟性に⽋けることがある フレームワークに頼らざるを得なかったり → もっとシンプル に情報を扱うアプローチ があるはず そうして⽣まれたのが ⾔語でもある 10

Slide 11

Slide 11 text

データ指向プログラミングの原則 原則 コードをデータから切り離す 原則 データを汎⽤的なデータ構造で表す 原則 データはイミュータブルである 原則 データスキーマをデータ表現から切り離す のプログラミングスタイルそのもの 11

Slide 12

Slide 12 text

データ指向プログラミングと親和性の⾼い技術 ロックフリー な楽観的並⾏性制御 の 純粋な関数、不変なデータとの相性が良い 状態変化のタイムトラベル、リプレイ 状態が不変の汎⽤データ構造で表現されていれば 極めて簡単 永続データ構造 不変 かつ永続的 であれば 効率も犠牲になりにくい 『純粋関数型データ構造』 12

Slide 13

Slide 13 text

2. Kotlinへの適⽤可能性を探る 13

Slide 14

Slide 14 text

データ指向プログラミングの原則(再掲) 原則 コードをデータから切り離す 原則 データを汎⽤的なデータ構造で表す 原則 データはイミュータブルである 原則 データスキーマをデータ表現から切り離す のプログラミングスタイルそのもの 14

Slide 15

Slide 15 text

原則 #1: コードをデータから切り離す 15

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

利⽤例 ;; FYI: プロンプトの `dop-examples` は現在の名前空間(モジュール) ;; そこでdef/requireされているものは⾮修飾名で参照できる dop-examples> (let [data (make-author "Isaac" "Asimov" 500)] (full-name data)) "Isaac Asimov" 17

Slide 18

Slide 18 text

「レコード」 ≒ を定義することもできる マップとレコードはインターフェースが共通している ため、関数 full-name はそのまま使える (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)) dop-examples> (let [data (make-author' "Isaac" "Asimov" 500)] (full-name data)) "Isaac Asimov" 18

Slide 19

Slide 19 text

(書籍より) 利点とコスト 主な利点 コードをさまざまなコンテキストで再利⽤できる コードを単体でテストできる システムがあまり複雑にならない傾向にある 主なコスト どのコードがどのデータにアクセスできるのかを 制御できない パッケージ化がない システムを構成するエンティティの数が増える 19

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

利⽤例 らしい ドット記法が必要であれば > 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

Slide 22

Slide 22 text

や の代わりに インターフェースを定義することで特定の具象型 Author2 に縛られなくすることはできる 構造的型 拡張可能レコード 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}" }

Slide 23

Slide 23 text

原則 #2: データを汎⽤的なデータ構造で表す 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

(書籍より) 利点とコスト 主な利点 特定のユースケースに限定されないジェネリック 関数を利⽤できる 柔軟なデータモデル 主なコスト パフォーマンスが少し低下する データスキーマがない コンパイル時にデータの有効性が確認されない 静的に型付けされる⾔語では、明⽰的な型変換 キャスト が必要になることがある 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

原則 #3: データはイミュータブルである 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

(書籍より) 利点とコスト 主な利点 すべての関数から⾃信を持ってデータにアクセス できる コードの振る舞いが予測可能である 等価のチェックが⾼速である 並⾏処理の安全性が⾃動的に確保される 主なコスト パフォーマンスが低下する 永続的なデータ構造のためのライブラリが必要で ある 29

Slide 30

Slide 30 text

Kotlinの場合 再代⼊不可な プロパティとほぼ不変単に の場合あり な データ構造を利⽤することはできる > val data1 = Author("Isaac", "Asimov", 500) > val data2 = data.copy(numOfBooks = 100) > data1 === data2 res9: kotlin.Boolean = false // 参照が異なる別のデータ ミュータビリティとイミュータビリティの狭間 関 数型⾔語使いから⾒た コレクション 30

Slide 31

Slide 31 text

原則 #4: データスキーマをデータ表現から切り離す 31

Slide 32

Slide 32 text

Clojureの場合 ライブラリ が標準で 含まれており、広く使われている 契約プログラミング (ns dop-examples (:require [clojure.spec.alpha :as s] ; clojure.specの導⼊ [clojure.string :as str])) (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

Slide 33

Slide 33 text

データの仕様 述語 により値レベルの制約まで記述できる (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

Slide 34

Slide 34 text

関数の仕様 データの仕様で関数の⼊出⼒仕様を記述できる (s/fdef make-author :args (s/cat :first-name ::first-name ; 第1引数 :last-name ::last-name ; 第2引数 :num-of-books ::num-of-books) ; 第3引数 :ret ::author) ; 戻り値 (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

Slide 35

Slide 35 text

データに対する検証 ;; 必須の :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

Slide 36

Slide 36 text

;; :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

Slide 37

Slide 37 text

関数 引数 に対する検証 ;; 関数の引数に対するチェックを有効化 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

Slide 38

Slide 38 text

;; キーに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

Slide 39

Slide 39 text

(書籍より) 利点とコスト 主な利点 検証すべきデータを⾃由に選択できる オプションフィールドを利⽤できる ⾼度なデータ検証条件を利⽤できる データモデルを⾃動的に可視化できる 主なコスト データとスキーマの結び付きが弱い パフォーマンスが少し低下する 39

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

3. まとめ 41

Slide 42

Slide 42 text

「データ指向プログラミング」は ⾔語 コミ ュニティに由来するプログラミングスタイル その⼒が最も効果的に発揮されるのは を使うときかも もいいぞ のような静的型付きオブジェクト指向⾔語で も こそ 参考になる⽰唆を含んでいる そもそも近年の新興⾔語ではクラスベースの を押し出していない印象がある。クラスと いう枠組みでのモデル化に囚われる必要はない 42

Slide 43

Slide 43 text

Further Reading データ指向プログラミング 本発表のサンプルコード 『データ指向プログラミング』 新刊『データ指向プログラミング』から「はじめ に」を公開! オブジェクト指向との対⽐も語ら れる (コードジン) データ指向プログラミングの真実をお話しします 43

Slide 44

Slide 44 text

Clojure による解説 公式サイト と はどう違う?  を解説 ログミー の設計に⾒る という考え⽅  を解説 ログミー の世界観 紙箱 44