$30 off During Our Annual Pro Sale. View Details »

Kotlin Meets Data-Oriented Programming

Kotlin Meets Data-Oriented Programming

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
Tweet

More Decks by Kent OHASHI

Other Decks in Programming

Transcript

  1. (ちなみに) Clojureとは 動的型付き ⾮オブジェクト指向 年に登場した 年〜 年〜 作者 のプレゼン は他

    のコミュニティでも多少知られているかも ⾔語の設計にも⾊濃く反映されている、 の重要性について語っている 関数型⾔語 古典的な には当初から批判的 モダンに再設計された 系⾔語 ⾔語 9
  2. 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
  3. 「レコード」 ≒ を定義することもできる マップとレコードはインターフェースが共通している ため、関数 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
  4. 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
  5. 利⽤例 らしい ドット記法が必要であれば > 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
  6. や の代わりに インターフェースを定義することで特定の具象型 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}" }
  7. 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
  8. 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
  9. Kotlinの場合 再代⼊不可な プロパティとほぼ不変単に の場合あり な データ構造を利⽤することはできる > val data1 =

    Author("Isaac", "Asimov", 500) > val data2 = data.copy(numOfBooks = 100) > data1 === data2 res9: kotlin.Boolean = false // 参照が異なる別のデータ ミュータビリティとイミュータビリティの狭間 関 数型⾔語使いから⾒た コレクション 30
  10. 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
  11. データの仕様 述語 により値レベルの制約まで記述できる (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
  12. 関数の仕様 データの仕様で関数の⼊出⼒仕様を記述できる (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
  13. データに対する検証 ;; 必須の :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
  14. ;; :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
  15. 関数 引数 に対する検証 ;; 関数の引数に対するチェックを有効化 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
  16. ;; キーに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
  17. 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
  18. 「データ指向プログラミング」は ⾔語 コミ ュニティに由来するプログラミングスタイル その⼒が最も効果的に発揮されるのは を使うときかも もいいぞ のような静的型付きオブジェクト指向⾔語で も こそ

    参考になる⽰唆を含んでいる そもそも近年の新興⾔語ではクラスベースの を押し出していない印象がある。クラスと いう枠組みでのモデル化に囚われる必要はない 42