Slide 1

Slide 1 text

Functional Calisthenics in Kotlin Kotlin で「関数型エクササイズ」を実践しよう #KotlinFest 1

Slide 2

Slide 2 text

のシニアエンジニア スタートアップと投資家のやり取りを効率化する データ管理プラットフォームを開発している 技術スタック: Kotlin/Ktor & TypeScript/Vue.js の運営にも協力 , などの関数型言語の愛好者 の運営スタッフ( 座長のひとり) Java, , Clojure, Kotlin とJVM 言語での開発経験 Kotlin の実務利用は1 年半ほど🐣 lagénorhynque 🐬カマイルカ 株式会社スマートラウンド Server-Side Kotlin Meetup Clojure Haskell 関数型まつり Scala 2

Slide 3

Slide 3 text

1. オブジェクト指向エクササイズ 2. 関数型プログラミングというスタイル 3. 🐬の「関数型エクササイズ」 4. Kotlin での実践例 3

Slide 4

Slide 4 text

1. オブジェクト指向エクササイズ 4

Slide 5

Slide 5 text

O'Reilly Japan の より 『ThoughtWorks アンソロジー』 書籍詳細ページ 5

Slide 6

Slide 6 text

オブジェクト指向エクササイズ 『ThoughtWorks アンソロジー』第5 章のタイトル 原題: Object Calisthenics (≒ オブジェクト体操) 手続き型プログラミングからオブジェクト指向プロ グラミングのコード設計の発想に親しむための訓練 方法として( 少々大胆で今や古めかしい?) ルール集 i.e. パラダイムシフトに順応してもらうきっかけ → 関数型プログラミングについても同じような アプローチを考えたい🐬 6

Slide 7

Slide 7 text

オブジェクト指向エクササイズ 9 つのルール ルール1: 1 つのメソッドにつきインデントは1 段階 までにすること 主な狙い: 責務の分離 ルール2: else 句を使用しないこと 主な狙い: 可読性 ルール3: すべてのプリミティブ型と文字列型をラッ プすること 主な狙い: カプセル化、型安全性 7

Slide 8

Slide 8 text

ルール4: 1 行につきドットは1 つまでにすること 主な狙い: 責務の分離、カプセル化 ルール5: 名前を省略しないこと 主な狙い: 可読性、責務の分離 ルール6: すべてのエンティティを小さくすること 主な狙い: 責務の分離、カプセル化 8

Slide 9

Slide 9 text

ルール7: 1 つのクラスにつきインスタンス変数は2 つまでにすること 主な狙い: 責務の分離、カプセル化 ルール8: ファーストクラスコレクションを使用する こと 主な狙い: カプセル化 ルール9: Getter 、Setter 、プロパティを使用しない こと 主な狙い: カプセル化 9

Slide 10

Slide 10 text

ルール1: 1 つのメソッドにつきインデントは1 段階までに すること リファクタリング前: class Board { fun board(): String = buildString { for (row in data) { for (square in row) append(square) appendLine() } } } 10

Slide 11

Slide 11 text

リファクタリング後: class Board { fun board(): String = buildString { collectRows(this) } fun collectRows(sb: StringBuilder) { // 拡張関数にする案も for (row in data) collectRow(sb, row) } fun collectRow(sb: StringBuilder, row: List) { for (square in row) sb.append(square) sb.appendLine() } } 11

Slide 12

Slide 12 text

ルール2: else 句を使用しないこと リファクタリング前: リファクタリング後: fun endMe() { if (status == DONE) { doSomething() } else { doSomethingElse() } } fun endMe() { if (status == DONE) { doSomething() return } doSomethingElse() } 12

Slide 13

Slide 13 text

リファクタリング前: リファクタリング後: fun head(): Node { if (isAdvancing()) return first else return last } fun head(): Node = if (isAdvancing()) first else last 13

Slide 14

Slide 14 text

ルール4: 1 行につきドットは1 つまでにすること リファクタリング前: class Board { class Piece(..., val representation: String) class Location(..., val current: Piece) fun boardRepresentation(): String = buildString { for (l in squares()) append(l.current.representation.substring(0, 1)) } } 14

Slide 15

Slide 15 text

リファクタリング後: class Board { class Piece(..., private val representation: String) { fun character(): String = representation.substring(0, 1) fun addTo(sb: StringBuilder) { sb.append(character()) } } class Location(..., private val current: Piece) { fun addTo(sb: StringBuilder) { current.addTo(sb) } } // 次ページに続く 15

Slide 16

Slide 16 text

// 前ページから続く fun boardRepresentation(): String = buildString { for (l in squares()) l.addTo(this) } } 16

Slide 17

Slide 17 text

ルール7: 1 つのクラスにつきインスタンス変数は2 つま でにすること リファクタリング前: class Name( val first: String, val middle: String, val last: String, ) 17

Slide 18

Slide 18 text

リファクタリング後: class Name( val family: Surname, val given: GivenNames, ) class Surname(val family: String) class GivenNames(val names: List) 18

Slide 19

Slide 19 text

2. 関数型プログラミングという スタイル 19

Slide 20

Slide 20 text

[ 参考] での🐬の発表 関数型まつり2025 関数型言語テイスティング: Haskell, Scala, Clojure, Elixir を比べて味わう関数型プログラミングの旨さ 20

Slide 21

Slide 21 text

( 現在の) 🐬によるFP とFPL の定義 関数型プログラミング := 純粋関数を基本要素とし て、その組み合わせによってプログラムを構成して いくプログラミングスタイル → 言語を問わず実践可能( 実践しやすさは異なる) 関数型言語 := 関数型プログラミングが言語/ 標準ラ イブラリレベルで十分に支援される( そして関数型 プログラミングスタイルがユビキタスな) 言語 → 例えばJavaScript/TypeScript やJava 、Kotlin 、 古典的なLisp 方言は含めない 21

Slide 22

Slide 22 text

重視する もの (values) ⾔語 (languages) 処理系/ 実装 (implementations) パターン (patterns) Lisp 式指向 (expression-oriented) 不変性 (immutability) 宣⾔型プログラミング (declarative programming) ( 型) 安全性 ((type) safety) 合成可能性 (composability) 永続性 (persistence) 純粋性 (purity) Clojure Erlang Elixir Haskell OCaml Standard ML ML 適⽤ (apply) 評価 (eval) 抽象構⽂⽊ (abstract syntax tree; AST) マクロ (macro) 参照透過性 (referential transparency) 再帰 (recursion) 遅延評価 (lazy evaluation) メモ化 (memoization) 並⾏プログラミング (concurrent programming) アクターモデル (actor model) STM (software transactional memory) CSP (communicating sequential processes) 継続 (continuation) ⾼階関数 (higher-order function) 関数合成 (function composition) カリー化 (currying) 部分適⽤ (partial application) 関数型 プログラミング (functional programming) 理論 (theories) 数学 (mathematics) 圏論 (category theory) 型システム (type system) ラムダ計算 (lambda calculus) メタプログラミング (metaprogramming) 形式⼿法 (formal methods) 定理証明⽀援系 (theorem prover) 評価 (evaluation) 制御 (control) 関数 (function) パイプ演算⼦ (pipe operator) メソッドチェーン (method chaining) 代数的データ型 (algebraic data type; ADT) パターンマッチ (pattern matching) クロージャー/ 関数閉包 (closure) オブジェクト (object) Idris Elm パーサーコンビネーター (parser combinator) 離散数学 (discrete mathematics) 契約プログラミング (contract programming) 抽象データ型 (abstract data type) データ (data) 直和型 (sum type) 直積型 (product type) 篩型 (refinement type) 依存型 (dependent type) カプセル化 (encapsulation) ポリモーフィズム/ 多相 (polymorphism) サブタイプ多相 (subtype polymorphism) パラメータ多相 (parametric polymorphism) アドホック多相 (ad hoc polymorphism) 型クラス (type class) マルチメソッド (multimethod) プロトコル (protocol) 変性 (variance) 継承 (inheritance) カリー= ハワード同型対応 (Curry–Howard correspondence) 分配束縛 (destructuring) 純粋関数型データ構造 (purely functional data structure) 永続データ構造 (persistent data structure) Coq (Rocq) Scala 構⽂解析 (parse) オーバーロード/ 多重定義 (overloading) 命令型プログラミング (imperative programming) ⽂指向 (statement-oriented) 副作⽤ (side effect) 破壊的更新 (mutation) 可変性 (mutability) pipes & filters goroutines & channels プロパティベーステスト (property-based testing) ⼊出⼒ (I/O) データ指向プログラミング (data-oriented programming) ファンクター (functor) モナド (monad) リスト (list) 遅延リスト/ ストリーム (lazy list/stream) 同図像性 (homoiconicity) 超循環評価器 (meta-circular evaluator) セルフホスティング (self-hosting) ベクター (vector) 全域関数 (total function) 部分関数 (partial function) オブジェクト指向プログラミング (object-oriented programming) アプリカティブ (applicative) トレイト (trait) Agda 必要呼び (call by need) F# ジェネリクス (generics) Lean Gleam 末尾再帰 (tail recursion) 意味論 (semantics) 型推論 (type inference) 再帰型 (recursive type) GoF デザインパターン (GoF Design Patterns) ※ 🐬が思い浮かぶ概念/ 用語を連想的に列挙したもの( 網羅的でも体系的でもない) 🐬の「関数型プログラミング」コンセプトマップ 22

Slide 23

Slide 23 text

重視する もの (values) 式指向 (expression-oriented) 不変性 (immutability) 宣⾔型プログラミング (declarative programming) ( 型) 安全性 ((type) safety) 合成可能性 (composability) 永続性 (persistence) 純粋性 (purity) 参照透過性 (referential transparency) 命令型プログラミング (imperative programming) ⽂指向 (statement-oriented) 副作⽤ (side effect) 破壊的更新 (mutation) 可変性 (mutability) ⼊出⼒ (I/O) 23

Slide 24

Slide 24 text

関数型プログラミングで重要な性質 純粋性(purity) 不変性(immutability) 合成可能性(composability) 式指向(expression-oriented) 宣言型プログラミング(declarative programming) ( 型) 安全性((type) safety) 24

Slide 25

Slide 25 text

3. 🐬の「関数型エクササイズ」 関数型プログラミングのコード設計に親しむために 25

Slide 26

Slide 26 text

🐬の「関数型エクササイズ」 9 つのルール ルール1: 1 つの関数は単一の( 文ではなく) 式で表す こと 主な狙い: 式指向 ルール2: 関数は引数と戻り値を持つこと 主な狙い: 純粋性 ルール3: 関数は引数以外の入力に依存しないこと 主な狙い: 純粋性 26

Slide 27

Slide 27 text

ルール4: I/O 処理は関数として分離し注入すること 主な狙い: 純粋性 ルール5: 再代入可能な変数、可変なデータ構造を 使用/ 定義しないこと 主な狙い: 不変性 ルール6: 繰り返し処理はループ構文ではなくコレク ション操作で行うこと 主な狙い: 宣言型プログラミング 27

Slide 28

Slide 28 text

ルール7: 汎用的な構文や関数よりも目的に特化し た関数を選択すること 主な狙い: 宣言型プログラミング ルール8: 既存の関数を部分適用/ 合成して新たな関 数を定義すること 主な狙い: 合成可能性 ルール9: 不正な状態が表せないようにデータ型の 選択/ 定義で制限すること 主な狙い: ( 型) 安全性 28

Slide 29

Slide 29 text

4. Kotlin での実践例 29

Slide 30

Slide 30 text

今回採用した方針 Kotlin の基本的な言語機能を活かす Kotlin に無理なく馴染む表現を目指す オブジェクト指向スタイルを排除せず併用する Kotlin はオブジェクト指向言語 準標準/ サードパーティライブラリに依存しない 🐬< 例えば の便利な要素を利用するのも良 さそうだが、全面的に使いたくなったらむしろ が適していそう😈(Kotlin でそこまでする?) Arrow Scala 30

Slide 31

Slide 31 text

ルール1: 1 つの関数は単一の( 文ではなく) 式で表すこと リファクタリング前: リファクタリング後: fun endMe() { if (status == DONE) { doSomething() return } doSomethingElse() } fun endMe() = if (status == DONE) doSomething() else doSomethingElse() 31

Slide 32

Slide 32 text

文ではなく式として表すことで命令型のコードが排 除されやすくなる Kotlin では: 命令型言語でお馴染み(?) の構文を引き継ぎつつも は式になっていて扱いやすい の構文を積極的に 活用すると良い制約になる 1 つの式で表しづらくなったら分割することを 強いられる 分岐構文 単一式(single-expression) 関数 32

Slide 33

Slide 33 text

ルール2: 関数は引数と戻り値を持つこと リファクタリング前: リファクタリング後: fun endMe() = if (status == DONE) doSomething() else doSomethingElse() fun endMe(input: SomeInput): SomeOutput = if (status == DONE) doSomething(input) else doSomethingElse(input) 33

Slide 34

Slide 34 text

引数をとらない/ 戻り値を返さない関数は副作用を 持ちやすいので必要最小限にする Kotlin では: オブジェクト指向言語でのクラスに属する関数 ( メソッド) のレシーバーは暗黙的な引数といえる クラスとしてモデル化するなら、明示的な引数の ない関数もありうる ただし、クラスで表す理由は自問したい 34

Slide 35

Slide 35 text

ルール3: 関数は引数以外の入力に依存しないこと リファクタリング前: リファクタリング後: var n: Int = 42 // 関数外の不安定な変数/ 値 fun f(x: Int): Int = x + n fun f(x: Int, y: Int): Int = x + y // 適宜、インターフェースを整える fun g(x: Int): Int = f(x, 42) fun h(x: Int, y: Int = 42): Int = f(x, y) 35

Slide 36

Slide 36 text

引数を介さず関数外からの入力( グローバル/ モジュ ール/ クラス変数など) にアクセスすると関数の参照 透過性が損なわれやすいので避ける 不変の値( 定数) を参照するのであれば問題はない ( 関数型言語でもクロージャーはありふれている) Kotlin では: 厳格に従うと、クラスのメソッドが他のメンバー 変数にアクセスすることさえできなくなる プライベートメソッドでは引数を介したアクセス のみに制限するような規約も考えられる 36

Slide 37

Slide 37 text

ルール4: I/O 処理は関数として分離し注入すること リファクタリング前: リファクタリング後: fun listUsers(ids: List): List = UserRepository() .findByIds(ids) .map { UserView(it) } fun listUsers( ids: List, resolveUsers: (List) -> List, ): List = resolveUsers(ids).map { UserView(it) } // 利用例 listUsers(userIds) { ids -> UserRepository().findByIds(ids) } 37

Slide 38

Slide 38 text

純粋関数を基本ブロックとするため、I/O 処理など 副作用の発生箇所は分離/ 局所化したい 高階関数によって注入するアプローチがシンプル かつ汎用的 I/O を型レベルで分離できる言語/ ライブラリも Kotlin では: interface や abstract class を利用しても よいが、 で十分な状況も多々ありそう インターフェースを最小化することにも繋がる 高階関数 38

Slide 39

Slide 39 text

ルール5: 再代入可能な変数、可変なデータ構造を使用/ 定義しないこと リファクタリング前: リファクタリング後: val wordCount = mutableMapOf() words.forEach { word -> val count = wordCount.getOrDefault(word, 0) wordCount[word] = count + 1 } // wordCount.toMap() で読み取り専用マップは得られる val wordCount: Map = words.groupingBy { it }.eachCount() 39

Slide 40

Slide 40 text

関数型言語では再代入可能な変数がなく可変データ 構造が定義/ 利用しにくくなっていることも多い 不変だが効率的なコレクション実装もある 関数/ モジュールに閉じて可変な変数/ データ構造 を扱うのは問題ない( パフォーマンスの都合など) Kotlin では: 変数は val で宣言し、 は読み 取り専用なものを使う( 今や一般的かも?) 明示的に可変な変数やデータ構造を扱わずに済む 関数を選択/ 設計する 標準コレクション 40

Slide 41

Slide 41 text

ルール6: 繰り返し処理はループ構文ではなくコレクシ ョン操作で行うこと リファクタリング前: for (n in 1..100) { when { n % 15 == 0 -> println("FizzBuzz") n % 3 == 0 -> println("Fizz") n % 5 == 0 -> println("Buzz") else -> println(n) } } 41

Slide 42

Slide 42 text

リファクタリング後: fun fizzBuzz(n: Int): String = when { n % 15 == 0 -> "FizzBuzz" n % 3 == 0 -> "Fizz" n % 5 == 0 -> "Buzz" else -> n.toString() } (1..100) .map(::fizzBuzz) .forEach(::println) 42

Slide 43

Slide 43 text

( イミュータブル) コレクションの操作( 変換) は関数 型プログラミングのありふれた日常の一部 プログラムとはデータ変換の連鎖 効率のために命令型のループ構文を局所的に利用 することはありうる Kotlin では: 標準ライブラリに高レベルな関数が充実している ので活用する for, while や forEach 関数はI/O などの副作用 発生を意図する状況以外では利用しない 43

Slide 44

Slide 44 text

ルール7: 汎用的な構文や関数よりも目的に特化した関 数を選択すること リファクタリング前: リファクタリング後: val numberOfAdultUsers = users.fold(0) { acc, user -> if (user.age >= 18) acc + 1 else acc } val numberOfAdultUsers = users.count { it.age >= 18 } 44

Slide 45

Slide 45 text

より宣言的になるように意図が表れる形式を選ぶ 汎用構文 < 汎用関数 < 目的特化関数 コレクションに対して: e.g. loop, match < fold < map, filter, sum 直和型に対して: e.g. match < fold < map, filter Kotlin では: 様々な用途の関数を知って使い分ける、自ら定義 する 45

Slide 46

Slide 46 text

ルール8: 既存の関数を部分適用/ 合成して新たな関数を 定義すること リファクタリング前: fun filterUsersByTargetAge(users: List, minAge: Int): List = users.filter { it.age >= minAge } fun > sortUsers(users: List, keyFn: (User) -> K): List = users.sortedBy(keyFn) fun takeFirstUsers(users: List, n: Int): List = users.take(n) // 上記の関数がある状況で fun listFirst5AdultUsers(users: List): List = takeFirstUsers( sortUsers( filterUsersByTargetAge(users, 18) ) { it.joinedAt }, 5 ) 46

Slide 47

Slide 47 text

リファクタリング後(1): リファクタリング後(2): fun listFirst5AdultUsers(users: List): List = filterUsersByTargetAge(users, 18) .let { sortUsers(it) { it.joinedAt } } .let { takeFirstUsers(it, 5) } fun listFirst5AdultUsers(users: List): List = users .filterByTargetAge(18) .sortByJoinedAt() .takeFirst(5) private fun List.filterByTargetAge(minAge: Int): List = filterUsersByTargetAge(this, minAge) private fun List.sortByJoinedAt(): List = sortUsers(this) { it.joinedAt } private fun List.takeFirst(n: Int): List = takeFirstUsers(this, n) 47

Slide 48

Slide 48 text

関数を簡潔に再利用するために部分適用や関数合成 に役立つユーティリティを活用する 多くの関数型言語にはラムダ式の略記法、パイプ 演算子、自動的なカリー化などがある Kotlin では: 部分適用や関数合成を楽にする仕組みはなさそう メソッドチェーンも関数合成の一種とみなせる let などの が便利 や を活用 して滑らかに繋ぐこともできる スコープ関数 拡張関数 レシーバー付き関数リテラル 48

Slide 49

Slide 49 text

ルール9: 不正な状態が表せないようにデータ型の選択/ 定義で制限すること リファクタリング前: data class User( val id: UserId, val isRegistered: Boolean, val isActive: Boolean, val joinedAt: LocalDateTime?, val leftAt: LocalDateTime?, ) { companion object { fun registeringUser(id: UserId): User = User(id, false, false, null, null) fun activeUser(id: UserId, /* 略 */): User = User(id, true, true, joinedAt, null) fun inactiveUser(id: UserId, /* 略 */): User = User(id, true, false, joinedAt, leftAt) } } 49

Slide 50

Slide 50 text

リファクタリング後: sealed interface User { val id: UserId data class RegisteringUser( override val id: UserId, ) : User data class ActiveUser( override val id: UserId, val joinedAt: LocalDateTime, ) : User data class InactiveUser( override val id: UserId, val joinedAt: LocalDateTime, val leftAt: LocalDateTime, ) : User } 50

Slide 51

Slide 51 text

代数的データ型でとりうる値のパターンを定義し、 パターンマッチングで網羅的に分岐/ 分解する boolean やoptional/nullable の乱用を避ける 組み合わせで不正な状態が生じやすくなるため Kotlin では: sealed interface/class や enum で代数的 データ型を表せる when 式で網羅的に場合分けできる 🐬< パターンマッチしたい(Java ではできる) 51

Slide 52

Slide 52 text

おわりに 🐬が考える、関数型プログラミング実践者の発想: ⛓️ 適切な制約が解放をもたらす → 純粋関数と不変データを基本に → 不正値を表現不能にしてより( 型) 安全に 🧱 単純で安定なブロックを基礎に全体を構成する → 式指向に、宣言的に、合成可能に 52

Slide 53

Slide 53 text

Kotlin らしく関数型プログラミングを実践しよう🐥 設計改善の機会になるはず💪 ( もの足りなくなったら(?) 、本格的な関数型言語もぜひ😈) 53

Slide 54

Slide 54 text

Further Reading 5 章 オブジェクト指向エクササイズ 原書: 原書: 『ThoughtWorks アンソロジー』 関数型言語テイスティング: Haskell, Scala, Clojure, Elixir を比べて味わう関数型プログラミングの旨さ 『なっとく!関数型プログラミング』 Grokking Functional Programming 『関数型ドメインモデリング』 Domain Modeling Made Functional 54