Share
Embed
Kotlin Meets Data-Oriented Programming: Kotlinで実践する「データ指向プログラミング」
書籍『データ指向プログラミング』( https://www.shoeisha.co.jp/book/detail/9784798179797 )は、プログラミング言語Clojureにおいて典型的なプログラミングスタイルの根幹にある考え方を他言語でも応用できる形で抽出し紹介する試みであるということができます。
Clojureを実務や趣味で継続的に利用するとともに比較的最近Kotlinに再入門した立場から、この本で提示されている「データ指向プログラミング」というプログラミングスタイルを概説しながらKotlinらしい実践の可能性について考察します。
データ指向プログラミングの原則:
- 原則 #1: コードをデータから切り離す
- 原則 #2: データを汎用的なデータ構造で表す
- 原則 #3: データはイミュータブルである
- 原則 #4: データスキーマをデータ表現から切り離す
Kent OHASHI
October 25, 2024
Transcript
44 slides
-
のシニアエンジニア 主要技術スタック のたまごスポンサー の運営企業 関数型⾔語 関数型プログラミングが⼤好き 仕事や趣味で などに⻑く 触れてきた 現職で初めて
を仕事で読み書きするように なって半年ほど lagénorhynque カマイルカ 株式会社スマートラウンド 2 -
書籍 は、プログラミン グ⾔語 において典型的なプログラミングスタ イルの根幹にある考え⽅を他⾔語でも応⽤できる形で 抽出し紹介する試みであるということができます。 を実務や趣味で継続的に利⽤するとともに⽐ 較的最近 に再⼊⾨した⽴場から、この本で提⽰ されている「データ指向プログラミング」というプロ
グラミングスタイルを概説しながら らしい実践 の可能性について考察します。 Kotlin Meets Data-Oriented Programming: Kotlinで実践する「データ指向プログラミング」 『データ指向プログラミング』 4 -
書籍 まえがき 『データ指向プログラミング』 特別なのは機能ではなく原則だと いうことで意⾒が⼀致した。 の基本原則を抜き出そうとしていた 私たちは、実際には、それらの原則 を他のプログラミング⾔語に応⽤で きることに気づいた。本書の構想が 沸いてきたのはそのときだった。私
が でとても気に⼊っている点 を世界中の開発者コミュニティに伝 えたかった。 7 -
に魅了された著者が ⾔語 コミュニテ ィで⼀般的なプログラミングスタイルのエッセンス を他⾔語でも応⽤できる形で抽出しようとした本 古典的な に対するアンチテーゼといえる 他の⾔語や設計思想と必ずしも馴染まず批判され ることもある 批判も理解できる
である の視点から捉え直し、 での 現実的な応⽤の可能性を考えたい 8 -
(ちなみに) Clojureとは 動的型付き ⾮オブジェクト指向 年に登場した 年〜 年〜 作者 のプレゼン は他
のコミュニティでも多少知られているかも ⾔語の設計にも⾊濃く反映されている、 の重要性について語っている 関数型⾔語 古典的な には当初から批判的 モダンに再設計された 系⾔語 ⾔語 9 -
データ指向プログラミングと親和性の⾼い技術 ロックフリー な楽観的並⾏性制御 の 純粋な関数、不変なデータとの相性が良い 状態変化のタイムトラベル、リプレイ 状態が不変の汎⽤データ構造で表現されていれば 極めて簡単 永続データ構造 不変
かつ永続的 であれば 効率も犠牲になりにくい 『純粋関数型データ構造』 12 -
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 -
「レコード」 ≒ を定義することもできる マップとレコードはインターフェースが共通している ため、関数 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 -
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 -
や の代わりに インターフェースを定義することで特定の具象型 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}" } -
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 -
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 -
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 -
データの仕様 述語 により値レベルの制約まで記述できる (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 ; 第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 -
データに対する検証 ;; 必須の :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 -
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 -
「データ指向プログラミング」は ⾔語 コミ ュニティに由来するプログラミングスタイル その⼒が最も効果的に発揮されるのは を使うときかも もいいぞ のような静的型付きオブジェクト指向⾔語で も こそ
参考になる⽰唆を含んでいる そもそも近年の新興⾔語ではクラスベースの を押し出していない印象がある。クラスと いう枠組みでのモデル化に囚われる必要はない 42 -
Clojure による解説 公式サイト と はどう違う? を解説 ログミー の設計に⾒る という考え⽅ を解説
ログミー の世界観 紙箱 44