>100 Views
November 01, 25
スライド概要
関数型プログラミングのコード設計に親しむために、Kotlinで「関数型エクササイズ」しよう💪
Kotlin Fest 2025セッション概要: https://2025.kotlinfest.dev/timetable/1719040800_b/
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Elixir, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
Functional Calisthenics in Kotlin で「関数型エクササイズ」を実践しよう Kotlin #KotlinFest 1
🐬カマイルカ 株式会社スマートラウンドのシニアエンジニア スタートアップと投資家のやり取りを効率化する データ管理プラットフォームを開発している 技術スタック: Kotlin/Ktor & TypeScript/Vue.js Server-Side Kotlin Meetupの運営にも協力 Clojure, Haskellなどの関数型言語の愛好者 関数型まつりの運営スタッフ(座長のひとり) Java, Scala, Clojure, KotlinとJVM言語での開発経験 Kotlinの実務利用は1年半ほど🐣 lagénorhynque 2
オブジェクト指向エクササイズ 関数型プログラミングというスタイル 🐬の「関数型エクササイズ」 での実践例 1. 2. 3. 4. Kotlin 3
1. オブジェクト指向エクササイズ 4
『ThoughtWorksアンソロジー』 O'Reilly Japan の書籍詳細ページより 5
オブジェクト指向エクササイズ 『ThoughtWorksアンソロジー』第5章のタイトル 原題: Object Calisthenics (≒ オブジェクト体操) 手続き型プログラミングからオブジェクト指向プロ グラミングのコード設計の発想に親しむための訓練 方法として(少々大胆で今や古めかしい?)ルール集 i.e. パラダイムシフトに順応してもらうきっかけ → 関数型プログラミングについても同じような アプローチを考えたい🐬 6
オブジェクト指向エクササイズ 9つのルール ルール1: 1つのメソッドにつきインデントは1段階 までにすること 主な狙い: 責務の分離 ルール2: else句を使用しないこと 主な狙い: 可読性 ルール3: すべてのプリミティブ型と文字列型をラッ プすること 主な狙い: カプセル化、型安全性 7
ルール4: 1行につきドットは1つまでにすること 主な狙い: 責務の分離、カプセル化 ルール5: 名前を省略しないこと 主な狙い: 可読性、責務の分離 ルール6: すべてのエンティティを小さくすること 主な狙い: 責務の分離、カプセル化 8
ルール7: 1つのクラスにつきインスタンス変数は2 つまでにすること 主な狙い: 責務の分離、カプセル化 ルール8: ファーストクラスコレクションを使用する こと 主な狙い: カプセル化 ルール9: Getter、Setter、プロパティを使用しない こと 主な狙い: カプセル化 9
ルール1: 1つのメソッドにつきインデントは1段階までに すること リファクタリング前: class Board { fun board(): String = buildString { for (row in data) { for (square in row) append(square) appendLine() } } } 10
リファクタリング後:
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<Square>) {
for (square in row)
sb.append(square)
sb.appendLine()
}
}
拡張関数にする案も
11
ルール2: else句を使用しないこと リファクタリング前: fun endMe() { if (status == DONE) { doSomething() } else { doSomethingElse() } } リファクタリング後: fun endMe() { if (status == DONE) { doSomething() return } doSomethingElse() } 12
リファクタリング前: fun head(): Node { if (isAdvancing()) return first else return last } リファクタリング後: fun head(): Node = if (isAdvancing()) first else last 13
ルール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
リファクタリング後: 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
// 前ページから続く fun boardRepresentation(): String = buildString { for (l in squares()) l.addTo(this) } } 16
ルール7: 1つのクラスにつきインスタンス変数は2つま でにすること リファクタリング前: class Name( val first: String, val middle: String, val last: String, ) 17
リファクタリング後: class Name( val family: Surname, val given: GivenNames, ) class Surname(val family: String) class GivenNames(val names: List<String>) 18
2. 関数型プログラミングという スタイル 19
参考] 関数型まつり2025での🐬の発表 [ 関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ 20
現在の)🐬によるFPとFPLの定義 関数型プログラミング := 純粋関数を基本要素とし て、その組み合わせによってプログラムを構成して いくプログラミングスタイル → 言語を問わず実践可能(実践しやすさは異なる) 関数型言語 := 関数型プログラミングが言語/標準ラ イブラリレベルで十分に支援される(そして関数型 プログラミングスタイルがユビキタスな)言語 → 例えばJavaScript/TypeScriptやJava、Kotlin、 古典的なLisp方言は含めない ( 21
🐬の「関数型プログラミング」コンセプトマップ Idris 永続性 Elm Gleam (persistence) 可変性 Agda (mutability) 合成可能性 Elixir (composability) 不変性 Lean 破壊的更新 (immutability) Erlang Haskell (mutation) 型 安全性 Coq (Rocq) ( ) ((type) safety) 参照透過性 重視する もの ⾔語 Clojure (referential transparency) (languages) 副作⽤ 純粋性 (values) 同図像性 ML 宣⾔型プログラミング Lisp (declarative programming) 命令型プログラミング OCaml (side effect) (purity) (homoiconicity) F# ⼊出⼒ (imperative programming) 式指向 マクロ (I/O) (expression-oriented) ⽂指向 (macro) パーサーコンビネーター Standard ML (statement-oriented) (parser combinator) Scala 抽象構⽂⽊ (abstract syntax tree; AST) プロパティベーステスト 圏論 (property-based testing) 構⽂解析 離散数学 (category theory) メタプログラミング (parse) (discrete mathematics) (metaprogramming) 契約プログラミング 数学 形式⼿法 (contract programming) 関数型 プログラミング (mathematics) (formal methods) 理論 篩型 (refinement type) (theories) 型システム 意味論 (type system) 型推論 依存型 カリー=ハワード同型対応 (Curry–Howard correspondence) (multimethod) 継続 メモ化 アクターモデル (actor model) セルフホスティング (self-hosting) (protocol) 再帰 遅延評価 超循環評価器 (meta-circular evaluator) プロトコル アドホック多相 (ad hoc polymorphism) (recursion) (lazy evaluation) (memoization) ポリモーフィズム/多相 ジェネリクス パラメータ多相 (polymorphism) 制御 評価 (trait) 変性 純粋関数型データ構造 (evaluation) トレイト (generics) (parametric polymorphism) (control) (variance) (purely functional data structure) 遅延リスト/ストリーム サブタイプ多相 (subtype polymorphism) (lazy list/stream) goroutines & channels CSP (communicating sequential processes) 並⾏プログラミング パターン (concurrent programming) (patterns) リスト (vector) 永続データ構造 全域関数 (total function) 関数 (function) ⾼階関数 関数合成 (function composition) ファンクター クロージャー/関数閉包 データ (data) データ指向プログラミング (data-oriented programming) カリー化 メソッドチェーン (method chaining) pipes & filters オブジェクト (destructuring) 再帰型 代数的データ型 (recursive type) (algebraic data type; ADT) (abstract data type) (currying) 分配束縛 パターンマッチ (pattern matching) 抽象データ型 部分適⽤ (partial application) パイプ演算⼦ (pipe operator) (monad) (applicative) (closure) (higher-order function) (inheritance) モナド アプリカティブ (functor) 部分関数 継承 ベクター (list) (persistent data structure) STM (software transactional memory) (partial function) (type class) (continuation) (call by need) (apply) (eval) (overloading) マルチメソッド 必要呼び (type inference) 適⽤ 評価 オーバーロード/多重定義 型クラス 末尾再帰 (semantics) (tail recursion) (dependent type) (implementations) (functional programming) ラムダ計算 (lambda calculus) 定理証明⽀援系 (theorem prover) 処理系/実装 直和型 直積型 (product type) (sum type) カプセル化 (encapsulation) (object) オブジェクト指向プログラミング (object-oriented programming) デザインパターン GoF (GoF Design Patterns) ※ 🐬 が思い浮かぶ概念/用語を連想的に列挙したもの(網羅的でも体系的でもない) 22
永続性 (persistence) 可変性 (mutability) 合成可能性 (composability) 不変性 破壊的更新 (immutability) (mutation) 型 安全性 ( ) ((type) safety) 重視する もの (values) 命令型プログラミング 参照透過性 (referential transparency) 純粋性 (purity) 副作⽤ (side effect) 宣⾔型プログラミング (declarative programming) (imperative programming) 式指向 ⽂指向 ⼊出⼒ (I/O) (expression-oriented) (statement-oriented) 23
関数型プログラミングで重要な性質 純粋性(purity) 不変性(immutability) 合成可能性(composability) 式指向(expression-oriented) 宣言型プログラミング(declarative programming) (型)安全性((type) safety) 24
3. 🐬の「関数型エクササイズ」 関数型プログラミングのコード設計に親しむために 25
🐬の「関数型エクササイズ」 9つのルール ルール1: 1つの関数は単一の(文ではなく)式で表す こと 主な狙い: 式指向 ルール2: 関数は引数と戻り値を持つこと 主な狙い: 純粋性 ルール3: 関数は引数以外の入力に依存しないこと 主な狙い: 純粋性 26
ルール4: I/O処理は関数として分離し注入すること 主な狙い: 純粋性 ルール5: 再代入可能な変数、可変なデータ構造を 使用/定義しないこと 主な狙い: 不変性 ルール6: 繰り返し処理はループ構文ではなくコレク ション操作で行うこと 主な狙い: 宣言型プログラミング 27
ルール7: 汎用的な構文や関数よりも目的に特化し た関数を選択すること 主な狙い: 宣言型プログラミング ルール8: 既存の関数を部分適用/合成して新たな関 数を定義すること 主な狙い: 合成可能性 ルール9: 不正な状態が表せないようにデータ型の 選択/定義で制限すること 主な狙い: (型)安全性 28
での実践例 4. Kotlin 29
今回採用した方針 Kotlinの基本的な言語機能を活かす Kotlinに無理なく馴染む表現を目指す オブジェクト指向スタイルを排除せず併用する Kotlinはオブジェクト指向言語 準標準/サードパーティライブラリに依存しない 🐬< 例えばArrowの便利な要素を利用するのも良 さそうだが、全面的に使いたくなったらむしろ Scalaが適していそう😈(Kotlinでそこまでする?) 30
ルール1: 1つの関数は単一の(文ではなく)式で表すこと リファクタリング前: fun endMe() { if (status == DONE) { doSomething() return } doSomethingElse() } リファクタリング後: fun endMe() = if (status == DONE) doSomething() else doSomethingElse() 31
文ではなく式として表すことで命令型のコードが排 除されやすくなる Kotlinでは: 命令型言語でお馴染み(?)の構文を引き継ぎつつも 分岐構文は式になっていて扱いやすい 単一式(single-expression)関数の構文を積極的に 活用すると良い制約になる 1つの式で表しづらくなったら分割することを 強いられる 32
ルール2: 関数は引数と戻り値を持つこと リファクタリング前: fun endMe() = if (status == DONE) doSomething() else doSomethingElse() リファクタリング後: fun endMe(input: SomeInput): SomeOutput = if (status == DONE) doSomething(input) else doSomethingElse(input) 33
引数をとらない/戻り値を返さない関数は副作用を 持ちやすいので必要最小限にする Kotlinでは: オブジェクト指向言語でのクラスに属する関数 (メソッド)のレシーバーは暗黙的な引数といえる クラスとしてモデル化するなら、明示的な引数の ない関数もありうる ただし、クラスで表す理由は自問したい 34
ルール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
引数を介さず関数外からの入力(グローバル/モジュ ール/クラス変数など)にアクセスすると関数の参照 透過性が損なわれやすいので避ける 不変の値(定数)を参照するのであれば問題はない (関数型言語でもクロージャーはありふれている) Kotlinでは: 厳格に従うと、クラスのメソッドが他のメンバー 変数にアクセスすることさえできなくなる プライベートメソッドでは引数を介したアクセス のみに制限するような規約も考えられる 36
ルール4: I/O処理は関数として分離し注入すること
リファクタリング前:
fun listUsers(ids: List<UserId>): List<UserView> =
UserRepository()
.findByIds(ids)
.map { UserView(it) }
リファクタリング後:
fun listUsers(
ids: List<UserId>,
resolveUsers: (List<UserId>) -> List<User>,
): List<UserView> =
resolveUsers(ids).map { UserView(it) }
利用例
//
listUsers(userIds) { ids ->
UserRepository().findByIds(ids)
}
37
純粋関数を基本ブロックとするため、I/O処理など 副作用の発生箇所は分離/局所化したい 高階関数によって注入するアプローチがシンプル かつ汎用的 I/Oを型レベルで分離できる言語/ライブラリも Kotlinでは: interface や abstract class を利用しても よいが、高階関数で十分な状況も多々ありそう インターフェースを最小化することにも繋がる 38
ルール5: 再代入可能な変数、可変なデータ構造を使用/
定義しないこと
リファクタリング前:
val wordCount = mutableMapOf<String, Int>()
words.forEach { word ->
val count = wordCount.getOrDefault(word, 0)
wordCount[word] = count + 1
}
// wordCount.toMap()
で読み取り専用マップは得られる
リファクタリング後:
val wordCount: Map<String, Int> =
words.groupingBy { it }.eachCount()
39
関数型言語では再代入可能な変数がなく可変データ 構造が定義/利用しにくくなっていることも多い 不変だが効率的なコレクション実装もある 関数/モジュールに閉じて可変な変数/データ構造 を扱うのは問題ない(パフォーマンスの都合など) Kotlinでは: 変数は val で宣言し、標準コレクションは読み 取り専用なものを使う(今や一般的かも?) 明示的に可変な変数やデータ構造を扱わずに済む 関数を選択/設計する 40
ルール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
リファクタリング後:
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
イミュータブル)コレクションの操作(変換)は関数 型プログラミングのありふれた日常の一部 プログラムとはデータ変換の連鎖 効率のために命令型のループ構文を局所的に利用 することはありうる Kotlinでは: 標準ライブラリに高レベルな関数が充実している ので活用する for, while や forEach 関数はI/Oなどの副作用 発生を意図する状況以外では利用しない ( 43
ルール7: 汎用的な構文や関数よりも目的に特化した関
数を選択すること
リファクタリング前:
val numberOfAdultUsers =
users.fold(0) { acc, user ->
if (user.age >= 18) acc + 1 else acc
}
リファクタリング後:
val numberOfAdultUsers =
users.count { it.age >= 18 }
44
より宣言的になるように意図が表れる形式を選ぶ 汎用構文 < 汎用関数 < 目的特化関数 コレクションに対して: e.g. loop, match < fold < map, filter, sum 直和型に対して: e.g. match < fold < map, filter では: 様々な用途の関数を知って使い分ける、自ら定義 する Kotlin 45
ルール8: 既存の関数を部分適用/合成して新たな関数を
定義すること
リファクタリング前:
fun filterUsersByTargetAge(users: List<User>, minAge: Int):
List<User> =
users.filter { it.age >= minAge }
fun <K : Comparable<K>> sortUsers(users: List<User>, keyFn:
(User) -> K): List<User> =
users.sortedBy(keyFn)
fun takeFirstUsers(users: List<User>, n: Int): List<User> =
users.take(5)
上記の関数がある状況で
//
fun listFirst5AdultUsers(users: List<User>): List<User> =
takeFirstUsers(
sortUsers(
filterUsersByTargetAge(users, 18)
) { it.joinedAt }, 5
)
46
リファクタリング後(1):
fun listFirst5AdultUsers(users: List<User>): List<User> =
filterUsersByTargetAge(users, 18)
.let { sortUsers(it) { it.joinedAt } }
.let { takeFirstUsers(it, 5) }
リファクタリング後(2):
fun listFirst5AdultUsers(users: List<User>): List<User> =
users
.filterAdult(18)
.sortByJoinedAt()
.takeFirst5()
private fun List<User>.filterAdult(minAge: Int): List<User> =
filterUsersByTargetAge(this, minAge)
private fun List<User>.sortByJoinedAt(): List<User> =
sortUsers(this) { it.joinedAt }
private fun List<User>.takeFirst5(): List<User> =
takeFirstUsers(this, 5)
47
関数を簡潔に再利用するために部分適用や関数合成 に役立つユーティリティを活用する 多くの関数型言語にはラムダ式の略記法、パイプ 演算子、自動的なカリー化などがある Kotlinでは: 部分適用や関数合成を楽にする仕組みはなさそう メソッドチェーンも関数合成の一種とみなせる let などのスコープ関数が便利 拡張関数やレシーバー付き関数リテラルを活用 して滑らかに繋ぐこともできる 48
ルール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
リファクタリング後: 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
代数的データ型でとりうる値のパターンを定義し、 パターンマッチングで網羅的に分岐/分解する booleanやoptional/nullableの乱用を避ける 組み合わせで不正な状態が生じやすくなるため Kotlinでは: sealed interface/class や enum で代数的 データ型を表せる when 式で網羅的に場合分けできる 🐬< パターンマッチしたい(Javaではできる) 51
おわりに 🐬が考える、関数型プログラミング実践者の発想: ⛓️ 適切な制約が解放をもたらす → 純粋関数と不変データを基本に → 不正値を表現不能にしてより(型)安全に 🧱 単純で安定なブロックを基礎に全体を構成する → 式指向に、宣言的に、合成可能に 52
らしく関数型プログラミングを実践しよう🐥 設計改善の機会になるはず💪 Kotlin もの足りなくなったら(?)、本格的な関数型言語もぜひ😈) ( 53
Further Reading 『ThoughtWorksアンソロジー』 5章 オブジェクト指向エクササイズ 関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ 『なっとく!関数型プログラミング』 原書: Grokking Functional Programming 『関数型ドメインモデリング』 原書: Domain Modeling Made Functional 54