- 株式会社スマートラウンドのシニアエンジニア
- スタートアップと投資家のやり取りを効率化するデータ管理プラットフォームを開発している
- 技術スタック: Kotlin/Ktor & TypeScript/Vue.js
- Server-Side Kotlin Meetupの運営にも協力
- スタートアップと投資家のやり取りを効率化するデータ管理プラットフォームを開発している
- Clojure, Haskellなどの関数型言語の愛好者
- 関数型まつりの運営スタッフ(座長のひとり)
- Java, Scala, Clojure, KotlinとJVM言語での開発経験
- Kotlinの実務利用は1年半ほど🐣
-
オブジェクト指向エクササイズ
-
関数型プログラミングというスタイル
-
🐬の「関数型エクササイズ」
-
Kotlinでの実践例
O'Reilly Japanの書籍詳細ページより
-
『ThoughtWorksアンソロジー』第5章のタイトル
- 原題: Object Calisthenics (≒ オブジェクト体操)
-
手続き型プログラミングからオブジェクト指向プログラミングのコード設計の発想に親しむための訓練方法として(少々大胆で今や古めかしい?)ルール集
-
i.e. パラダイムシフトに順応してもらうきっかけ
- → 関数型プログラミングについても同じようなアプローチを考えたい🐬
-
ルール1: 1つのメソッドにつきインデントは1段階までにすること
- 主な狙い: 責務の分離
-
ルール2: else句を使用しないこと
- 主な狙い: 可読性
-
ルール3: すべてのプリミティブ型と文字列型をラップすること
- 主な狙い: カプセル化、型安全性
-
ルール4: 1行につきドットは1つまでにすること
- 主な狙い: 責務の分離、カプセル化
-
ルール5: 名前を省略しないこと
- 主な狙い: 可読性、責務の分離
-
ルール6: すべてのエンティティを小さくすること
- 主な狙い: 責務の分離、カプセル化
-
ルール7: 1つのクラスにつきインスタンス変数は2つまでにすること
- 主な狙い: 責務の分離、カプセル化
-
ルール8: ファーストクラスコレクションを使用すること
- 主な狙い: カプセル化
-
ルール9: Getter、Setter、プロパティを使用しないこと
- 主な狙い: カプセル化
リファクタリング前:
class Board {
fun board(): String =
buildString {
for (row in data) {
for (square in row)
append(square)
appendLine()
}
}
}リファクタリング後:
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()
}
}リファクタリング前:
fun endMe() {
if (status == DONE) {
doSomething()
} else {
doSomethingElse()
}
}リファクタリング後:
fun endMe() {
if (status == DONE) {
doSomething()
return
}
doSomethingElse()
}リファクタリング前:
fun head(): Node {
if (isAdvancing())
return first
else
return last
}リファクタリング後:
fun head(): Node =
if (isAdvancing()) first else lastリファクタリング前:
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))
}
}リファクタリング後:
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)
}
}
// 次ページに続く // 前ページから続く
fun boardRepresentation(): String =
buildString {
for (l in squares())
l.addTo(this)
}
}リファクタリング前:
class Name(
val first: String,
val middle: String,
val last: String,
)リファクタリング後:
class Name(
val family: Surname,
val given: GivenNames,
)
class Surname(val family: String)
class GivenNames(val names: List<String>)[参考] 関数型まつり2025での🐬の発表
関数型言語テイスティング: Haskell, Scala, Clojure, Elixirを比べて味わう関数型プログラミングの旨さ
-
関数型プログラミング := 純粋関数を基本要素として、その組み合わせによってプログラムを構成していくプログラミングスタイル
- → 言語を問わず実践可能(実践しやすさは異なる)
-
関数型言語 := 関数型プログラミングが言語/標準ライブラリレベルで十分に支援される(そして関数型プログラミングスタイルがユビキタスな)言語
- → 例えばJavaScript/TypeScriptやJava、Kotlin、古典的なLisp方言は含めない
※ 🐬が思い浮かぶ概念/用語を連想的に列挙したもの(網羅的でも体系的でもない)
-
純粋性(purity)
-
不変性(immutability)
-
合成可能性(composability)
-
式指向(expression-oriented)
-
宣言型プログラミング(declarative programming)
-
(型)安全性((type) safety)
関数型プログラミングのコード設計に親しむために
-
ルール1: 1つの関数は単一の(文ではなく)式で表すこと
- 主な狙い: 式指向
-
ルール2: 関数は引数と戻り値を持つこと
- 主な狙い: 純粋性
-
ルール3: 関数は引数以外の入力に依存しないこと
- 主な狙い: 純粋性
-
ルール4: I/O処理は関数として分離し注入すること
- 主な狙い: 純粋性
-
ルール5: 再代入可能な変数、可変なデータ構造を使用/定義しないこと
- 主な狙い: 不変性
-
ルール6: 繰り返し処理はループ構文ではなくコレクション操作で行うこと
- 主な狙い: 宣言型プログラミング
-
ルール7: 汎用的な構文や関数よりも目的に特化した関数を選択すること
- 主な狙い: 宣言型プログラミング
-
ルール8: 既存の関数を部分適用/合成して新たな関数を定義すること
- 主な狙い: 合成可能性
-
ルール9: 不正な状態が表せないようにデータ型の選択/定義で制限すること
- 主な狙い: (型)安全性
-
Kotlinの基本的な言語機能を活かす
- Kotlinに無理なく馴染む表現を目指す
-
オブジェクト指向スタイルを排除せず併用する
- Kotlinはオブジェクト指向言語
-
準標準/サードパーティライブラリに依存しない
リファクタリング前:
fun endMe() {
if (status == DONE) {
doSomething()
return
}
doSomethingElse()
}リファクタリング後:
fun endMe() =
if (status == DONE) doSomething()
else doSomethingElse()-
文ではなく式として表すことで命令型のコードが排除されやすくなる
-
Kotlinでは:
-
命令型言語でお馴染み(?)の構文を引き継ぎつつも分岐構文は式になっていて扱いやすい
-
単一式(single-expression)関数の構文を積極的に活用すると良い制約になる
- 1つの式で表しづらくなったら分割することを強いられる
-
リファクタリング前:
fun endMe() =
if (status == DONE) doSomething()
else doSomethingElse()リファクタリング後:
fun endMe(input: SomeInput): SomeOutput =
if (status == DONE) doSomething(input)
else doSomethingElse(input)-
引数をとらない/戻り値を返さない関数は副作用を持ちやすいので必要最小限にする
-
Kotlinでは:
-
オブジェクト指向言語でのクラスに属する関数(メソッド)のレシーバーは暗黙的な引数といえる
-
クラスとしてモデル化するなら、明示的な引数のない関数もありうる
- ただし、クラスで表す理由は自問したい
-
リファクタリング前:
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)-
引数を介さず関数外からの入力(グローバル/モジュール/クラス変数など)にアクセスすると関数の参照透過性が損なわれやすいので避ける
- 不変の値(定数)を参照するのであれば問題はない(関数型言語でもクロージャーはありふれている)
-
Kotlinでは:
-
厳格に従うと、クラスのメソッドが他のメンバー変数にアクセスすることさえできなくなる
-
プライベートメソッドでは引数を介したアクセスのみに制限するような規約も考えられる
-
リファクタリング前:
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)
}-
純粋関数を基本ブロックとするため、I/O処理など副作用の発生箇所は分離/局所化したい
-
高階関数によって注入するアプローチがシンプルかつ汎用的
-
I/Oを型レベルで分離できる言語/ライブラリも
-
-
Kotlinでは:
interfaceやabstract classを利用してもよいが、高階関数で十分な状況も多々ありそう- インターフェースを最小化することにも繋がる
リファクタリング前:
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()-
関数型言語では再代入可能な変数がなく可変データ構造が定義/利用しにくくなっていることも多い
-
不変だが効率的なコレクション実装もある
-
関数/モジュールに閉じて可変な変数/データ構造を扱うのは問題ない(パフォーマンスの都合など)
-
-
Kotlinでは:
-
変数は
valで宣言し、標準コレクションは読み取り専用なものを使う(今や一般的かも?) -
明示的に可変な変数やデータ構造を扱わずに済む関数を選択/設計する
-
リファクタリング前:
for (n in 1..100) {
when {
n % 15 == 0 -> println("FizzBuzz")
n % 3 == 0 -> println("Fizz")
n % 5 == 0 -> println("Buzz")
else -> println(n)
}
}リファクタリング後:
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)-
(イミュータブル)コレクションの操作(変換)は関数型プログラミングのありふれた日常の一部
-
プログラムとはデータ変換の連鎖
-
効率のために命令型のループ構文を局所的に利用することはありうる
-
-
Kotlinでは:
-
標準ライブラリに高レベルな関数が充実しているので活用する
-
for,whileやforEach関数はI/Oなどの副作用発生を意図する状況以外では利用しない
-
リファクタリング前:
val numberOfAdultUsers =
users.fold(0) { acc, user ->
if (user.age >= 18) acc + 1 else acc
}リファクタリング後:
val numberOfAdultUsers =
users.count { it.age >= 18 }-
より宣言的になるように意図が表れる形式を選ぶ
-
汎用構文 < 汎用関数 < 目的特化関数
-
コレクションに対して:
- e.g.
loop,match<fold<map,filter,sum
- e.g.
-
直和型に対して:
- e.g.
match<fold<map,filter
- e.g.
-
-
Kotlinでは:
- 様々な用途の関数を知って使い分ける、自ら定義する
リファクタリング前:
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(n)// 上記の関数がある状況で
fun listFirst5AdultUsers(users: List<User>): List<User> =
takeFirstUsers(
sortUsers(
filterUsersByTargetAge(users, 18)
) { it.joinedAt }, 5
)リファクタリング後(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
.filterByTargetAge(18)
.sortByJoinedAt()
.takeFirst(5)
private fun List<User>.filterByTargetAge(minAge: Int):
List<User> =
filterUsersByTargetAge(this, minAge)
private fun List<User>.sortByJoinedAt(): List<User> =
sortUsers(this) { it.joinedAt }
private fun List<User>.takeFirst(n: Int): List<User> =
takeFirstUsers(this, n)-
関数を簡潔に再利用するために部分適用や関数合成に役立つユーティリティを活用する
- 多くの関数型言語にはラムダ式の略記法、パイプ演算子、自動的なカリー化などがある
-
Kotlinでは:
-
部分適用や関数合成を楽にする仕組みはなさそう
-
メソッドチェーンも関数合成の一種とみなせる
letなどのスコープ関数が便利- 拡張関数やレシーバー付き関数リテラルを活用して滑らかに繋ぐこともできる
-
リファクタリング前:
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)
}
}リファクタリング後:
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
}-
代数的データ型でとりうる値のパターンを定義し、パターンマッチングで網羅的に分岐/分解する
-
booleanやoptional/nullableの乱用を避ける
- 組み合わせで不正な状態が生じやすくなるため
-
Kotlinでは:
-
sealed interface/classやenumで代数的データ型を表せる -
when式で網羅的に場合分けできる- 🐬< パターンマッチしたい(Javaではできる)
-
🐬が考える、関数型プログラミング実践者の発想:
-
⛓️ 適切な制約が解放をもたらす
-
→ 純粋関数と不変データを基本に
-
→ 不正値を表現不能にしてより(型)安全に
-
-
🧱 単純で安定なブロックを基礎に全体を構成する
- → 式指向に、宣言的に、合成可能に
Kotlinらしく関数型プログラミングを実践しよう🐥
設計改善の機会になるはず💪
(もの足りなくなったら(?)、本格的な関数型言語もぜひ😈)

