Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Calisthenics in Kotlin: Kotlinで「関数型エ...

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

関数型プログラミングのコード設計に親しむために、Kotlinで「関数型エクササイズ」しよう💪

Kotlin Fest 2025セッション概要: https://2025.kotlinfest.dev/timetable/1719040800_b/

Avatar for Kent OHASHI

Kent OHASHI

November 01, 2025
Tweet

More Decks by Kent OHASHI

Other Decks in Programming

Transcript

  1. のシニアエンジニア スタートアップと投資家のやり取りを効率化する データ管理プラットフォームを開発している 技術スタック: Kotlin/Ktor & TypeScript/Vue.js の運営にも協力 , などの関数型言語の愛好者

    の運営スタッフ( 座長のひとり) Java, , Clojure, Kotlin とJVM 言語での開発経験 Kotlin の実務利用は1 年半ほど🐣 lagénorhynque 🐬カマイルカ 株式会社スマートラウンド Server-Side Kotlin Meetup Clojure Haskell 関数型まつり Scala 2
  2. オブジェクト指向エクササイズ 『ThoughtWorks アンソロジー』第5 章のタイトル 原題: Object Calisthenics (≒ オブジェクト体操) 手続き型プログラミングからオブジェクト指向プロ

    グラミングのコード設計の発想に親しむための訓練 方法として( 少々大胆で今や古めかしい?) ルール集 i.e. パラダイムシフトに順応してもらうきっかけ → 関数型プログラミングについても同じような アプローチを考えたい🐬 6
  3. オブジェクト指向エクササイズ 9 つのルール ルール1: 1 つのメソッドにつきインデントは1 段階 までにすること 主な狙い: 責務の分離

    ルール2: else 句を使用しないこと 主な狙い: 可読性 ルール3: すべてのプリミティブ型と文字列型をラッ プすること 主な狙い: カプセル化、型安全性 7
  4. ルール1: 1 つのメソッドにつきインデントは1 段階までに すること リファクタリング前: class Board { fun

    board(): String = buildString { for (row in data) { for (square in row) append(square) appendLine() } } } 10
  5. リファクタリング後: 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
  6. ルール2: else 句を使用しないこと リファクタリング前: リファクタリング後: fun endMe() { if (status

    == DONE) { doSomething() } else { doSomethingElse() } } fun endMe() { if (status == DONE) { doSomething() return } doSomethingElse() } 12
  7. リファクタリング前: リファクタリング後: fun head(): Node { if (isAdvancing()) return first

    else return last } fun head(): Node = if (isAdvancing()) first else last 13
  8. ルール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
  9. リファクタリング後: 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
  10. リファクタリング後: class Name( val family: Surname, val given: GivenNames, )

    class Surname(val family: String) class GivenNames(val names: List<String>) 18
  11. ( 現在の) 🐬によるFP とFPL の定義 関数型プログラミング := 純粋関数を基本要素とし て、その組み合わせによってプログラムを構成して いくプログラミングスタイル

    → 言語を問わず実践可能( 実践しやすさは異なる) 関数型言語 := 関数型プログラミングが言語/ 標準ラ イブラリレベルで十分に支援される( そして関数型 プログラミングスタイルがユビキタスな) 言語 → 例えばJavaScript/TypeScript やJava 、Kotlin 、 古典的なLisp 方言は含めない 21
  12. 重視する もの (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
  13. 重視する もの (values) 式指向 (expression-oriented) 不変性 (immutability) 宣⾔型プログラミング (declarative programming)

    ( 型) 安全性 ((type) safety) 合成可能性 (composability) 永続性 (persistence) 純粋性 (purity) 参照透過性 (referential transparency) 命令型プログラミング (imperative programming) ⽂指向 (statement-oriented) 副作⽤ (side effect) 破壊的更新 (mutation) 可変性 (mutability) ⼊出⼒ (I/O) 23
  14. 🐬の「関数型エクササイズ」 9 つのルール ルール1: 1 つの関数は単一の( 文ではなく) 式で表す こと 主な狙い:

    式指向 ルール2: 関数は引数と戻り値を持つこと 主な狙い: 純粋性 ルール3: 関数は引数以外の入力に依存しないこと 主な狙い: 純粋性 26
  15. ルール4: I/O 処理は関数として分離し注入すること 主な狙い: 純粋性 ルール5: 再代入可能な変数、可変なデータ構造を 使用/ 定義しないこと 主な狙い:

    不変性 ルール6: 繰り返し処理はループ構文ではなくコレク ション操作で行うこと 主な狙い: 宣言型プログラミング 27
  16. 今回採用した方針 Kotlin の基本的な言語機能を活かす Kotlin に無理なく馴染む表現を目指す オブジェクト指向スタイルを排除せず併用する Kotlin はオブジェクト指向言語 準標準/ サードパーティライブラリに依存しない

    🐬< 例えば の便利な要素を利用するのも良 さそうだが、全面的に使いたくなったらむしろ が適していそう😈(Kotlin でそこまでする?) Arrow Scala 30
  17. ルール1: 1 つの関数は単一の( 文ではなく) 式で表すこと リファクタリング前: リファクタリング後: fun endMe() {

    if (status == DONE) { doSomething() return } doSomethingElse() } fun endMe() = if (status == DONE) doSomething() else doSomethingElse() 31
  18. ルール2: 関数は引数と戻り値を持つこと リファクタリング前: リファクタリング後: fun endMe() = if (status ==

    DONE) doSomething() else doSomethingElse() fun endMe(input: SomeInput): SomeOutput = if (status == DONE) doSomething(input) else doSomethingElse(input) 33
  19. ルール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
  20. 引数を介さず関数外からの入力( グローバル/ モジュ ール/ クラス変数など) にアクセスすると関数の参照 透過性が損なわれやすいので避ける 不変の値( 定数) を参照するのであれば問題はない

    ( 関数型言語でもクロージャーはありふれている) Kotlin では: 厳格に従うと、クラスのメソッドが他のメンバー 変数にアクセスすることさえできなくなる プライベートメソッドでは引数を介したアクセス のみに制限するような規約も考えられる 36
  21. ルール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
  22. 純粋関数を基本ブロックとするため、I/O 処理など 副作用の発生箇所は分離/ 局所化したい 高階関数によって注入するアプローチがシンプル かつ汎用的 I/O を型レベルで分離できる言語/ ライブラリも Kotlin

    では: interface や abstract class を利用しても よいが、 で十分な状況も多々ありそう インターフェースを最小化することにも繋がる 高階関数 38
  23. ルール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
  24. 関数型言語では再代入可能な変数がなく可変データ 構造が定義/ 利用しにくくなっていることも多い 不変だが効率的なコレクション実装もある 関数/ モジュールに閉じて可変な変数/ データ構造 を扱うのは問題ない( パフォーマンスの都合など) Kotlin

    では: 変数は val で宣言し、 は読み 取り専用なものを使う( 今や一般的かも?) 明示的に可変な変数やデータ構造を扱わずに済む 関数を選択/ 設計する 標準コレクション 40
  25. ルール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
  26. リファクタリング後: 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
  27. ( イミュータブル) コレクションの操作( 変換) は関数 型プログラミングのありふれた日常の一部 プログラムとはデータ変換の連鎖 効率のために命令型のループ構文を局所的に利用 することはありうる Kotlin

    では: 標準ライブラリに高レベルな関数が充実している ので活用する for, while や forEach 関数はI/O などの副作用 発生を意図する状況以外では利用しない 43
  28. より宣言的になるように意図が表れる形式を選ぶ 汎用構文 < 汎用関数 < 目的特化関数 コレクションに対して: e.g. loop, match

    < fold < map, filter, sum 直和型に対して: e.g. match < fold < map, filter Kotlin では: 様々な用途の関数を知って使い分ける、自ら定義 する 45
  29. ルール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
  30. リファクタリング後(1): リファクタリング後(2): fun listFirst5AdultUsers(users: List<User>): List<User> = filterUsersByTargetAge(users, 18) .let

    { sortUsers(it) { it.joinedAt } } .let { takeFirstUsers(it, 5) } 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
  31. ルール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
  32. リファクタリング後: 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
  33. Further Reading 5 章 オブジェクト指向エクササイズ 原書: 原書: 『ThoughtWorks アンソロジー』 関数型言語テイスティング:

    Haskell, Scala, Clojure, Elixir を比べて味わう関数型プログラミングの旨さ 『なっとく!関数型プログラミング』 Grokking Functional Programming 『関数型ドメインモデリング』 Domain Modeling Made Functional 54