functional kotlin
ADT
- 型->状態
- 操作->イベント
モノイド?
リファクタリングの概要
-
DoorEvent
の定義:- 状態遷移を表現するための
DoorEvent
型を定義します。これは、Door
を受け取り、新しいDoor
を返す関数です。
- 状態遷移を表現するための
-
状態ごとのイベント処理:
- 各状態のイベント(
close
,swing
,open
,lock
,unlock
,turnKey
)をDoorEvent
型として定義します。
- 各状態のイベント(
-
新しい状態を返すイベント関数の実装:
-
Door
クラスの各状態に対応するイベント関数を定義し、DoorEvent
型で処理します。
-
リファクタリング後のコード
sealed class Door {
// Define the Open state with its data and behavior
data class Open(val angle: Double) : Door()
// Define the Closed state with its behavior
object Closed : Door() // シンボル見たいのはobject
// Define the Locked state with its data and behavior
data class Locked(val turns: Int) : Door()
}
// Type alias for Door event functions
typealias DoorEvent = (Door) -> Door
// Event functions for each Door state transition
val closeDoor: DoorEvent = { door ->
when (door) {
is Door.Open -> Door.Closed
else -> door // No change if the state is not Open
}
}
val swingDoor: (Double) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Open -> Door.Open(door.angle + delta)
else -> door // No change if the state is not Open
}
}
}
val openDoor: (Double) -> DoorEvent = { degrees ->
{ door ->
when (door) {
is Door.Closed -> Door.Open(degrees)
else -> door // No change if the state is not Closed
}
}
}
val lockDoor: DoorEvent = { door ->
when (door) {
is Door.Closed -> Door.Locked(1)
else -> door // No change if the state is not Closed
}
}
val unlockDoor: DoorEvent = { door ->
when (door) {
is Door.Locked -> Door.Closed
else -> door // No change if the state is not Locked
}
}
val turnKey: (Int) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Locked -> Door.Locked(door.turns + delta)
else -> door // No change if the state is not Locked
}
}
}
// Example usage
fun main() {
var door: Door = Door.Open(45.0)
println("Initial state: $door") // Initial state: Open(angle=45.0)
// Applying events
door = closeDoor(door)
println("After closing: $door") // After closing: Closed
door = openDoor(90.0)(door)
println("After opening: $door") // After opening: Open(angle=90.0)
door = swingDoor(15.0)(door)
println("After swinging: $door") // After swinging: Open(angle=105.0)
door = closeDoor(door)
door = lockDoor(door)
println("After locking: $door") // After locking: Locked(turns=1)
door = turnKey(2)(door)
println("After turning key: $door") // After turning key: Locked(turns=3)
door = unlockDoor(door)
println("After unlocking: $door") // After unlocking: Closed
}
詳細な説明
-
Door
クラスの各状態:-
Door
はsealed class
で、3つの状態(Open
,Closed
,Locked
)を持っています。 -
Open
とLocked
はデータクラスで、それぞれangle
とturns
のプロパティを持っています。 -
Closed
はオブジェクトで、単一のインスタンスを表現します。
-
-
DoorEvent
型:-
DoorEvent
は、Door
を受け取り、新しいDoor
を返す関数の型エイリアスです。
-
-
イベント関数の定義:
- 各イベント(
closeDoor
,swingDoor
,openDoor
,lockDoor
,unlockDoor
,turnKey
)は、現在の状態を受け取り、新しい状態に遷移するための関数です。 -
when
式を使って、状態に応じた処理を行います。
- 各イベント(
-
使用例:
-
main
関数内で、Door
の初期状態を設定し、各イベントを適用して状態を遷移させています。
-
このリファクタリングにより、Door
の各状態ごとの動作が DoorEvent
関数として明確に定義され、コードの可読性とメンテナンス性が向上します。
はい、Kotlin の let
関数を使用して、Door
の状態に対するイベント関数をチェーンすることができます。let
関数は、受け取ったオブジェクトをブロックのレシーバとして使用し、その結果を返すため、状態遷移のチェーン操作に適しています。
以下に、let
を使ってイベント関数をチェーンする方法を示します。
fun main() {
var door: Door = Door.Open(45.0)
println("Initial state: $door") // Initial state: Open(angle=45.0)
// Applying events using `let` for chaining
door = door
.let(closeDoor) // Closing the door
.also { println("After closing: $it") } // After closing: Closed
.let(openDoor(90.0)) // Opening the door with 90 degrees
.also { println("After opening: $it") } // After opening: Open(angle=90.0)
.let(swingDoor(15.0)) // Swinging the door by 15 degrees
.also { println("After swinging: $it") } // After swinging: Open(angle=105.0)
.let(closeDoor) // Closing the door again
.let(lockDoor) // Locking the door
.also { println("After locking: $it") } // After locking: Locked(turns=1)
.let(turnKey(2)) // Turning the key twice
.also { println("After turning key: $it") } // After turning key: Locked(turns=3)
.let(unlockDoor) // Unlocking the door
.also { println("After unlocking: $it") } // After unlocking: Closed
// Final state
println("Final state: $door")
}
解説
-
初期状態:
-
var door: Door = Door.Open(45.0)
でドアを 45 度開いた状態で初期化します。 - 初期状態を出力します。
-
-
let
を使用してイベントをチェーンする:-
door = door.let(closeDoor)
でcloseDoor
イベントを適用します。let
は受け取ったオブジェクト(ここではdoor
)をレシーバとして使用し、イベント関数を適用した結果を返します。 - 各
let
の後にalso
を使用して、中間状態を出力します。 - この操作を続けて、
openDoor
,swingDoor
,closeDoor
,lockDoor
,turnKey
,unlockDoor
イベントをチェーンします。
-
-
最終状態:
- 最終的な状態を
println("Final state: $door")
で出力します。
- 最終的な状態を
メリット
-
可読性の向上:
let
を使用することで、状態遷移のチェーンが明確になり、コードの読みやすさが向上します。 -
中間状態の確認:
also
を使って中間状態を簡単に出力でき、デバッグが容易になります。
この方法で、Door
の状態遷移をチェーンし、各ステップで状態を確認できます。
Result
型を使用することで、状態遷移中にエラーが発生した場合の処理をより安全に扱うことができます。Result
型は、操作が成功した場合に Success
を、失敗した場合に Failure
を返すため、各操作が成功したかどうかを簡単にチェーンして処理することができます。
以下に、Door
の状態遷移を Result
型と let
を使ってチェーンする方法を示します。
Result
型を使用したリファクタリング
まず、Door
の各イベント関数を Result
型を返すように変更します。
sealed class Door {
// Define the Open state with its data and behavior
data class Open(val angle: Double) : Door()
// Define the Closed state with its behavior
object Closed : Door()
// Define the Locked state with its data and behavior
data class Locked(val turns: Int) : Door()
}
// Type alias for Door event functions
typealias DoorEvent = (Door) -> Result<Door>
// Event functions for each Door state transition using Result type
val closeDoor: DoorEvent = { door ->
when (door) {
is Door.Open -> Result.success(Door.Closed)
else -> Result.failure(Exception("Cannot close a door that is not open"))
}
}
val swingDoor: (Double) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Open -> Result.success(Door.Open(door.angle + delta))
else -> Result.failure(Exception("Cannot swing a door that is not open"))
}
}
}
val openDoor: (Double) -> DoorEvent = { degrees ->
{ door ->
when (door) {
is Door.Closed -> Result.success(Door.Open(degrees))
else -> Result.failure(Exception("Cannot open a door that is not closed"))
}
}
}
val lockDoor: DoorEvent = { door ->
when (door) {
is Door.Closed -> Result.success(Door.Locked(1))
else -> Result.failure(Exception("Cannot lock a door that is not closed"))
}
}
val unlockDoor: DoorEvent = { door ->
when (door) {
is Door.Locked -> Result.success(Door.Closed)
else -> Result.failure(Exception("Cannot unlock a door that is not locked"))
}
}
val turnKey: (Int) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Locked -> Result.success(Door.Locked(door.turns + delta))
else -> Result.failure(Exception("Cannot turn the key on a door that is not locked"))
}
}
}
// Example usage
fun main() {
var door: Door = Door.Open(45.0)
println("Initial state: $door") // Initial state: Open(angle=45.0)
// Applying events using `let` for chaining with Result type
val result = runCatching { door }
.mapCatching(closeDoor)
.also { println("After closing: $it") } // After closing: Closed
.mapCatching(openDoor(90.0))
.also { println("After opening: $it") } // After opening: Open(angle=90.0)
.mapCatching(swingDoor(15.0))
.also { println("After swinging: $it") } // After swinging: Open(angle=105.0)
.mapCatching(closeDoor)
.mapCatching(lockDoor)
.also { println("After locking: $it") } // After locking: Locked(turns=1)
.mapCatching(turnKey(2))
.also { println("After turning key: $it") } // After turning key: Locked(turns=3)
.mapCatching(unlockDoor)
.also { println("After unlocking: $it") } // After unlocking: Closed
// Final state
println("Final state: ${result.getOrElse { "Operation failed with error: ${it.message}" }}")
}
解説
-
Result
型を返すイベント関数:- 各イベント関数(
closeDoor
,swingDoor
,openDoor
,lockDoor
,unlockDoor
,turnKey
)がResult<Door>
型を返すように変更されました。 -
Result.success
を使って操作が成功した場合の新しい状態を返し、Result.failure
を使って失敗した場合に例外を返します。
- 各イベント関数(
-
let
によるチェーン操作:-
runCatching
を使って初期状態のdoor
をResult
型に変換し、その後の操作を安全にチェーンします。 - 各操作は
mapCatching
を使ってResult
型の中で行われ、エラーが発生した場合はチェーンが中断されます。
-
-
中間状態の出力:
-
also
を使って、中間状態またはエラーを出力します。 -
also
はチェーン内でのデバッグに便利です。
-
-
最終状態の出力:
- 最終的な状態を
result.getOrElse
を使って出力します。操作が成功した場合は状態を出力し、失敗した場合はエラーメッセージを出力します。
- 最終的な状態を
メリット
-
エラー処理の強化:
Result
型を使うことで、操作が失敗した場合のエラーを安全に処理でき、エラーが発生してもチェーン全体の処理を中断することができます。 -
可読性の向上:
let
とmapCatching
を使ったチェーンによって、操作の流れが明確になり、コードの可読性が向上します。 -
安全な操作: 各操作が
Result
型でラップされているため、エラーが発生しても安全に次の操作に進むことができます。
この方法を使うことで、Door
の状態遷移をより安全に管理し、エラーが発生した場合の処理も明確にすることができます。
はい、kotlin-result
を使用すると、チェーン操作をよりエレガントに記述するための方法があります。特に flatMap
を使用すると、ネストを避けながら複数の操作を直列化できます。flatMap
は、結果が Result
型である場合にそれを展開し、次の操作に渡すために使用します。
以下に、kotlin-result
を使ってチェーン操作をより適切に書き直したコードを示します。
kotlin-result
を使ったエレガントなチェーン操作
import com.github.michaelbull.result.*
import com.github.michaelbull.result.coroutines.*
import com.github.michaelbull.result.map
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.flatMap
import com.github.michaelbull.result.combine
sealed class Door {
data class Open(val angle: Double) : Door()
object Closed : Door()
data class Locked(val turns: Int) : Door()
}
// Type alias for Door event functions using Result
typealias DoorEvent = (Door) -> Result<Door, DoorError>
// Define a DoorError to handle different error cases
sealed class DoorError(val message: String) {
object InvalidClose : DoorError("Cannot close a door that is not open.")
object InvalidSwing : DoorError("Cannot swing a door that is not open.")
object InvalidOpen : DoorError("Cannot open a door that is not closed.")
object InvalidLock : DoorError("Cannot lock a door that is not closed.")
object InvalidUnlock : DoorError("Cannot unlock a door that is not locked.")
object InvalidTurnKey : DoorError("Cannot turn the key on a door that is not locked.")
}
// Event functions using Result type
val closeDoor: DoorEvent = { door ->
when (door) {
is Door.Open -> Ok(Door.Closed)
else -> Err(DoorError.InvalidClose)
}
}
val swingDoor: (Double) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Open -> Ok(Door.Open(door.angle + delta))
else -> Err(DoorError.InvalidSwing)
}
}
}
val openDoor: (Double) -> DoorEvent = { degrees ->
{ door ->
when (door) {
is Door.Closed -> Ok(Door.Open(degrees))
else -> Err(DoorError.InvalidOpen)
}
}
}
val lockDoor: DoorEvent = { door ->
when (door) {
is Door.Closed -> Ok(Door.Locked(1))
else -> Err(DoorError.InvalidLock)
}
}
val unlockDoor: DoorEvent = { door ->
when (door) {
is Door.Locked -> Ok(Door.Closed)
else -> Err(DoorError.InvalidUnlock)
}
}
val turnKey: (Int) -> DoorEvent = { delta ->
{ door ->
when (door) {
is Door.Locked -> Ok(Door.Locked(door.turns + delta))
else -> Err(DoorError.InvalidTurnKey)
}
}
}
// Example usage with more elegant chaining
fun main() {
var door: Door = Door.Open(45.0)
println("Initial state: $door") // Initial state: Open(angle=45.0)
// Applying events using flatMap for better chaining with kotlin-result
val result = Ok(door)
.flatMap(closeDoor)
.also { println("After closing: $it") }
.flatMap(openDoor(90.0))
.also { println("After opening: $it") }
.flatMap(swingDoor(15.0))
.also { println("After swinging: $it") }
.flatMap(closeDoor)
.flatMap(lockDoor)
.also { println("After locking: $it") }
.flatMap(turnKey(2))
.also { println("After turning key: $it") }
.flatMap(unlockDoor)
.also { println("After unlocking: $it") }
// Final state
result
.onSuccess { println("Final state: $it") }
.onFailure { println("Operation failed with error: ${it.message}") }
}
解説
-
flatMap
の使用:-
flatMap
は、前の操作が成功した場合に次の操作を続けて行い、失敗した場合はエラーをそのまま返します。これにより、ネストを避けながらチェーン操作を行うことができます。 -
flatMap
は、Result
型を返す関数を連続して呼び出す場合に非常に便利です。
-
-
初期状態の設定:
-
Ok(door)
で初期状態のDoor
をResult
型にラップして、チェーン操作を開始します。
-
-
チェーン操作:
- 各状態遷移の操作 (
closeDoor
,openDoor
,swingDoor
,lockDoor
,turnKey
,unlockDoor
) をflatMap
を使ってチェーンします。 -
also
を使って中間状態を出力し、デバッグを簡単にします。
- 各状態遷移の操作 (
-
最終状態の出力:
- 最終的な結果を
onSuccess
とonFailure
で処理します。操作が成功した場合には状態を出力し、失敗した場合にはエラーメッセージを出力します。
- 最終的な結果を
メリット
-
簡潔なエラーハンドリング:
flatMap
を使用することで、各操作の結果を自然にチェーンし、エラーが発生した場合は自動的にチェーンが中断されるため、エラー処理が非常にシンプルになります。 - 高い可読性: チェーン操作が直線的に記述されるため、コードの流れが明確で、理解しやすくなります。
-
デバッグの容易さ:
also
を使って中間結果を出力することで、チェーンの各ステップの状態を簡単に確認できます。
この方法を使用することで、Door
の状態遷移をよりエレガントに管理し、エラー処理を簡潔に行うことができます。
データ型の方に各操作を持ちたい時(trait風)
はい、Kotlin でも型クラス(インターフェース)の概念を使って、1つの型が複数の型クラス(インターフェース)を持つことができます。これにより、ある型が複数の異なる能力(動作)を実装することができます。
以下に、User
クラスが Printable
インターフェースと他のインターフェース(例えば Comparable
)を実装する例を示します。
例:複数のインターフェースの実装
interface Printable {
fun prettyPrint(): String
}
interface Comparable<T> {
fun compareTo(other: T): Int
}
class User(val name: String, val age: Int) : Printable, Comparable<User> {
override fun prettyPrint(): String {
return "User(name=$name, age=$age)"
}
override fun compareTo(other: User): Int {
return this.age - other.age
}
}
fun <T : Printable> printPretty(value: T) {
println(value.prettyPrint())
}
fun main() {
val user1 = User("Alice", 30)
val user2 = User("Bob", 25)
printPretty(user1) // User(name=Alice, age=30)
// 比較
if (user1.compareTo(user2) > 0) {
println("${user1.name} is older than ${user2.name}")
} else {
println("${user1.name} is younger than or the same age as ${user2.name}")
}
}
解説
-
インターフェースの定義:
-
Printable
は、prettyPrint
メソッドを持つインターフェースです。 -
Comparable
は、compareTo
メソッドを持ち、2つのオブジェクトを比較するインターフェースです。
-
-
複数のインターフェースの実装:
-
User
クラスはPrintable
とComparable<User>
の2つのインターフェースを実装しています。 -
prettyPrint
メソッドは、User
オブジェクトの文字列表現を返します。 -
compareTo
メソッドは、User
オブジェクトを年齢で比較します。
-
-
ジェネリック関数の使用:
-
printPretty
関数は、Printable
を実装する任意の型に対してprettyPrint
メソッドを呼び出します。
-
複数のインターフェースを使用する理由
-
多様な機能の提供:
- 1つのクラスが複数のインターフェースを実装することで、異なる側面の機能を提供できます。例えば、
User
クラスは表示のためのPrintable
と、比較のためのComparable
を同時に提供します。
- 1つのクラスが複数のインターフェースを実装することで、異なる側面の機能を提供できます。例えば、
-
疎結合な設計:
- 異なるインターフェースを実装することで、クラスが特定の機能や役割に対して疎結合になります。これにより、コードの再利用性と保守性が向上します。
-
型安全性の向上:
- ジェネリック型とインターフェースを組み合わせることで、型安全なコードを実現し、型キャストの必要性を減らすことができます。
さらなる拡張
他にも、Kotlin ではデフォルト実装(default implementation
)を使って、インターフェースに基本的な実装を提供することもできます。また、Kotlin のデリゲーション機能を使うことで、あるクラスが他のクラスの機能を委譲することも可能です。
これにより、Kotlin でも非常に柔軟に複数の能力を持つクラスを設計することができます。
もし他に具体的なシナリオや質問があれば教えてください。
ポリモフィズム風
ご指摘の通り、sealed
クラスを使用して型に基づいたディスパッチを行う方法も非常に有効です。Kotlinのsealed
クラスは、型安全なディスパッチをサポートし、特定の型に応じた処理をwhen
式でシンプルに実装できます。この方法では、inline
やreified
といった特別な構文を使わずに、型に基づいた操作を実現できます。
sealed
クラスを使ったディスパッチ
sealed
クラスを使うと、クラスのサブタイプが限定されるため、コンパイラはすべてのサブクラスを知っており、when
式を使って型ごとに異なる処理を記述できます。
sealed
クラスの定義
まず、共通の操作を持つsealed
クラスを定義し、そのサブクラスとして扱う型ごとに派生クラスを作成します。
sealed class Showable {
data class IntValue(val value: Int) : Showable()
data class BoolValue(val value: Boolean) : Showable()
data class StringValue(val value: String) : Showable()
}
ここでは、IntValue
、BoolValue
、StringValue
のように、Int
、Boolean
、String
に対応するクラスを定義しています。
show
関数の定義
次に、Showable
型に対するshow
関数を定義し、when
式を使って各サブクラスに対する処理を記述します。
fun Showable.show(): String = when (this) {
is Showable.IntValue -> value.toString()
is Showable.BoolValue -> if (value) "true" else "false"
is Showable.StringValue -> value
}
この関数では、Showable
型の値に基づいて適切な処理を行い、それぞれの型に応じて文字列を返します。
使用例
このshow
関数を使って、型に応じた処理を行うコードは以下のようになります。
fun main() {
val intVal = Showable.IntValue(42)
val boolVal = Showable.BoolValue(true)
val strVal = Showable.StringValue("Hello, Kotlin!")
println(intVal.show()) // "42"
println(boolVal.show()) // "true"
println(strVal.show()) // "Hello, Kotlin!"
}
sealed
クラスによる型安全なディスパッチの利点
-
型安全:
sealed
クラスとwhen
式を使うことで、コンパイラはすべてのサブクラスを知っているため、未処理のケースがある場合には警告を出します。これにより、型安全なコードが保証されます。 -
明確で簡潔なコード:
when
式を使って、各サブクラスごとに処理を記述するため、コードが明確で簡潔になります。特定の型に対して何をするかが一目でわかるため、可読性が向上します。 -
拡張性:
新しい型を追加する場合は、sealed
クラスの新しいサブクラスを定義し、when
式にそのサブクラスの処理を追加するだけです。これにより、システム全体を変更することなく、簡単に拡張できます。
まとめ
sealed
クラスとwhen
式を使うことで、KotlinでもHaskellの型クラスのような自動ディスパッチに近い仕組みを実現できます。この方法は、特に特定の型に対して異なる処理を行いたい場合に有効です。また、inline
やreified
のような特別な構文を使う必要がないため、よりシンプルに実装できます。
以下に、sealed
クラスを使った型に応じた処理のディスパッチの完全なコード例を示します。
sealed class Showable {
data class IntValue(val value: Int) : Showable()
data class BoolValue(val value: Boolean) : Showable()
data class StringValue(val value: String) : Showable()
}
fun Showable.show(): String = when (this) {
is Showable.IntValue -> value.toString()
is Showable.BoolValue -> if (value) "true" else "false"
is Showable.StringValue -> value
}
fun main() {
val intVal = Showable.IntValue(42)
val boolVal = Showable.BoolValue(true)
val strVal = Showable.StringValue("Hello, Kotlin!")
println(intVal.show()) // "42"
println(boolVal.show()) // "true"
println(strVal.show()) // "Hello, Kotlin!"
}
このアプローチにより、Kotlinでの型安全なディスパッチが実現でき、型に応じた処理を効果的に行うことが可能です。
王道のADTの多態
show(intVal)
の形式で型に応じた処理をディスパッチするためには、Kotlinでsealed
クラスとwhen
式を活用しつつ、関数として使用できるようにする方法があります。
ここでは、Kotlinの特性を活かしながら、show(intVal)
の形式で関数を呼び出せるようにする具体的な実装方法を紹介します。
sealed
クラスと通常の関数の組み合わせ
方法1: まず、sealed
クラスを使って型ごとの処理をディスパッチする方法を考えます。その後、通常の関数を定義して、関数呼び出し形式で利用できるようにします。
sealed
クラスの定義
各型に対応するsealed
クラスを定義します。
sealed class Showable {
data class IntValue(val value: Int) : Showable()
data class BoolValue(val value: Boolean) : Showable()
data class StringValue(val value: String) : Showable()
}
show
関数の定義
次に、Showable
型の引数を取るshow
関数を定義し、when
式で型に基づいた処理を記述します。
fun show(showable: Showable): String = when (showable) {
is Showable.IntValue -> showable.value.toString()
is Showable.BoolValue -> if (showable.value) "true" else "false"
is Showable.StringValue -> showable.value
}
使用例
Showable
型を作成し、それをshow
関数に渡します。
fun main() {
val intVal = Showable.IntValue(42)
val boolVal = Showable.BoolValue(true)
val strVal = Showable.StringValue("Hello, Kotlin!")
println(show(intVal)) // "42"
println(show(boolVal)) // "true"
println(show(strVal)) // "Hello, Kotlin!"
}
この方法では、show(intVal)
のように、関数形式でディスパッチを実現しています。
// 関数定義
fun addItemFunction(item: ToDoItem): ToDoListProjectionRow {
return ToDoListProjectionRow(User("SampleUser"), true, ToDoList(listOf(item)))
}
// 関数型の変数
val addItemLambda: (ToDoItem) -> ToDoListProjectionRow = { item ->
ToDoListProjectionRow(User("SampleUser"), true, ToDoList(listOf(item)))
}
val addItemLambda: (ToDoItem) -> ToDoListProjectionRow = {(
item
) -> ToDoListProjectionRow(
User("SampleUser"), true, ToDoList(listOf(item))
)
}
val addItemLambda: (ToDoItem) -> ToDoListProjectionRow = {
(item) ->
ToDoListProjectionRow(User("SampleUser"), true, ToDoList(listOf(item)))
}
個人的には item
の前で改行したいけど、いい感じの見え方がない
val addItemLambda: (ToDoItem) -> ToDoListProjectionRow
までは仕様で、
item以降が実装だから
-> funで定義した方が見やすいか ":"の後に戻り値がきているのが継承とかとややこしい
val newListName = requestForm("newlistname")
.flatMap(ListName.Companion::fromUntrusted) // 成功時に ListName に変換し、Result をネストしない
.getOrElse { error ->
// 失敗時はエラーメッセージを使って BAD_REQUEST レスポンスを返す
return Response(Response.BAD_REQUEST).body("Form error: $error") //ここでreturnできるのいい
}
はい、以下の2つの呼び出しはKotlinにおいて等価です:
transform2(outcome1, outcome2, { a, b -> a + b }) // 通常の関数呼び出し
transform2(outcome1, outcome2) { a, b -> a + b } // ラムダ式の外部配置
この2つの形式は、**Kotlinの「ラムダ式の外部配置」**という機能により同じ意味を持ちます。
アプリカティブは複数のファンクタ +関数をとる?
操作の方も文脈につつまれる?
アプリカティブファンクター(Applicative Functor)は、単一の種類の複数のファンクターを扱うことが基本です。しかし、異なる種類のファンクターを組み合わせて操作する方法もありますが、それは直接的にはアプリカティブファンクターの操作の範疇ではなく、他の技法や設計パターンを利用することになります。
単一の種類の複数のファンクター
アプリカティブファンクターの基本的な操作では、同じ種類のファンクターを組み合わせて操作します。例えば、リストのアプリカティブ操作やオプションのアプリカティブ操作は、同じ種類のファンクター(リストまたはオプション)に対して行われます。
例: リストのアプリカティブ操作
// 3つのリストをアプリカティブに組み合わせる例
fun main() {
val list1 = listOf(1, 2)
val list2 = listOf("A", "B")
val list3 = listOf(true, false)
// リストの要素を組み合わせる関数
val combine: (Int) -> (String) -> (Boolean) -> String = { a -> { b -> { c -> "$a-$b-$c" } } }
// アプリカティブの `ap` 操作を使ってリストを組み合わせる
val result = listOf(combine)
.ap(list1)
.ap(list2)
.ap(list3)
println(result) // 出力: [1-A-true, 1-A-false, 1-B-true, 1-B-false, 2-A-true, 2-A-false, 2-B-true, 2-B-false]
}
この例では、3つのリストをアプリカティブ操作で組み合わせています。同じ種類のファンクター(リスト)を組み合わせることで、すべての要素の組み合わせが得られます。
異なる種類の複数のファンクター
異なる種類のファンクターを組み合わせる操作は、通常のアプリカティブファンクターの範囲外になりますが、これを扱うための方法はいくつか存在します。
1. モナドトランスフォーマー
モナドトランスフォーマーは、異なる種類のモナド(特定のファンクターの一種)を組み合わせて操作する技法です。モナドトランスフォーマーを使うと、例えば Option
と List
などの異なるファンクターを組み合わせて操作できます。
// OptionとListの組み合わせを扱うモナドトランスフォーマーの例(Kotlinには直接のサポートはないが、イメージとして)
fun <A> listOfOptionsTraverse(options: List<Option<A>>): Option<List<A>> {
return options.foldRight(Option.Some(emptyList())) { opt, acc ->
opt.flatMap { value -> acc.map { list -> list + value } }
}
}
この例では、Option
と List
の組み合わせを扱っています。各 Option
が持つ値を取り出し、それらをリストに集約します。
2. プロダクト型や組み合わせ
異なるファンクターを同時に扱うために、プロダクト型(例えば、タプル)を使って、複数のファンクターをラップすることができます。
// ListとOptionを同時に扱うためにタプルを使う例
fun main() {
val options = listOf(
Pair(Option.Some(1), Option.Some("A")),
Pair(Option.Some(2), Option.None),
Pair(Option.None, Option.Some("B"))
)
val result = options.mapNotNull { (optInt, optStr) ->
optInt.flatMap { int ->
optStr.map { str ->
"$int-$str"
}
}.orElse(null)
}
println(result) // 出力: [1-A]
}
この例では、Option
と Option
をペアとして扱い、それらを List
内で操作しています。このように、異なるファンクターを組み合わせて操作する方法を取ることができます。
アプリカティブファンクターの本質
アプリカティブファンクターの本質は、「同じ種類のファンクターに対して、コンテキストを保ちながら関数を適用する」ことです。これにより、複数のファンクターを同時に扱い、その結果を集約することが可能です。
まとめ
- アプリカティブファンクターは、同じ種類の複数のファンクターに対して操作を行い、その結果を集約します。
- 異なる種類のファンクターを組み合わせて操作する場合、モナドトランスフォーマーやプロダクト型を使うことで、異なるファンクターを一緒に扱うことが可能です。
- アプリカティブの
ap
操作を使うことで、複数のファンクターのコンテキストを統合し、操作を行うことができます。
このように、アプリカティブファンクターは複数のファンクターを扱う際に非常に強力ですが、異なる種類のファンクターを同時に操作するには、追加の技法やデザインパターンが必要となります。
fun <U> ap(f: Option<(T) -> U>): Option<U> = when (this) {
is Some -> f.map { func -> func(this.value) }
None -> None
}
この書き換えでは、f.map { it(value) }
を f.map { func -> func(this.value) }
に変更しています。func
は、f
に含まれる関数を参照するための明示的な名前です。
完全な例
以下は、Option
クラスとその ap
関数を書き換えた完全な例です。
// Optionの定義
sealed class Option<out T> {
object None : Option<Nothing>()
data class Some<out T>(val value: T) : Option<T>()
// map 関数
fun <U> map(f: (T) -> U): Option<U> = when (this) {
is Some -> Some(f(value))
None -> None
}
// ap 関数
fun <U> ap(f: Option<(T) -> U>): Option<U> = when (this) {
is Some -> f.map { func -> func(this.value) }
None -> None
}
// flatMap 関数
fun <U> flatMap(f: (T) -> Option<U>): Option<U> = when (this) {
is Some -> f(value)
None -> None
}
}
// 使用例
fun main() {
val someValue: Option<Int> = Option.Some(10)
val functionOption: Option<(Int) -> String> = Option.Some { it.toString() + " apples" }
val noneFunction: Option<(Int) -> String> = Option.None
// 関数を適用する
val result1 = someValue.ap(functionOption)
val result2 = someValue.ap(noneFunction)
val noneValue: Option<Int> = Option.None
val result3 = noneValue.ap(functionOption)
println(result1) // 出力: Some(10 apples)
println(result2) // 出力: None
println(result3) // 出力: None
}
解説
-
f.map { func -> func(this.value) }
:-
map
関数は、Option
型のf
に対して適用されます。 -
f
がSome
の場合、f
に含まれる関数がfunc
としてラムダ式内で使用されます。 -
func(this.value)
は、this
の値 (Option.Some
の値) をfunc
に適用します。
-
import com.github.michaelbull.result.*
// combineResults関数はそのままです
fun <T, E> combineResults(results: List<Result<T, E>>): Result<List<T>, List<E>> {
return results.fold(
Ok(emptyList()) as Result<List<T>, List<E>>
) { acc, result ->
when (acc) {
is Ok -> when (result) {
is Ok -> Ok(acc.value + result.value)
is Err -> Err(listOf(result.error))
}
is Err -> when (result) {
is Ok -> acc
is Err -> Err(acc.error + result.error)
}
}
}
}
// validateNonEmpty と validateMinLength もそのままです
fun validateNonEmpty(input: String): Result<String, String> {
return if (input.isNotEmpty()) {
Ok(input)
} else {
Err("Input must not be empty")
}
}
fun validateMinLength(input: String, minLength: Int): Result<String, String> {
return if (input.length >= minLength) {
Ok(input)
} else {
Err("Input must be at least $minLength characters long")
}
}
fun main() {
// 検証対象のデータ
val inputData = "Hello"
// バリデータ関数のリスト
val validators: List<(String) -> Result<String, String>> = listOf(
{ validateNonEmpty(it) },
{ validateMinLength(it, 5) },
{ validateMinLength(it, 10) } // 失敗する例
)
// フルエントに繋げたバリデーション結果の集約
val combinedResult = validators.map { it(inputData) }
.let { combineResults(it) }
// 結果の表示
when (combinedResult) {
is Ok -> println("Validation passed with values: ${combinedResult.value}")
is Err -> println("Validation failed with errors: ${combinedResult.error}")
}
}
エンティティ
- エンティティ->data class
- 値 -> value classの方がいい?
- inline化されて性能がミリよくなるくらい?
- 値である、という知識は明示的になると思う
- smartコンストラクタ-> companiono object
data class ListName internal constructor(val name: String) {
companion object {
fun fromTrusted(name: String): ListName = ListName(name)
fun fromUntrustedOrThrow(name: String): ListName =
fromUntrusted(name).onFailure { error(it.msg) }
fun fromUntrusted(name: String): ListNameOutcome =
name.validateListName(::nameTooShort, ::nameTooLong, ::nameWithInvalidChars)
}
override fun toString(): String = name
}
-
internal constructor
でこいつのコンパニオンオブジェクトであるファクトリメソッドからしか作れなくする
結局
ListName.name
みたいにアクセスしなければいけないやつ。
ただの値に型がついてるというより、クラスに包まれたもの、という呪縛に引きづられる。
そこはtypescriptがいい。
"中身"、生の値としてにアクセスしたい=stringとして扱い、としたらtoStringを使うことで違和感はないか
value class UserId private constructor(val id: Int) {
companion object {
// ファクトリーメソッド
fun from(value: Int): UserId {
// ここで追加の検証やロジックを実行可能
require(value > 0) { "ID must be positive" }
return UserId(value)
}
}
// メソッドやプロパティを追加可能
fun display(): String {
return "UserId: $id"
}
}
fun main() {
// 正しいインスタンス生成方法(コンパニオンオブジェクト経由)
val userId = UserId.from(123)
println(userId.display()) // UserId: 123
// コンストラクタを直接使用することはできない(コンパイルエラー)
// val invalidUserId = UserId(123) // エラー: Cannot access '<init>': it is private in 'UserId'
}
privateの方が良さそう。internalは同モジュールからなら使える
コマンド->イベントのマッピング
Command -> CurrentState -> Event
現在の状態によって、発動するイベントが異なる。
ここを最初にやる?
ポリシーにも近い?
workflowのIOはevent
usecaseのIOはcommand
データ型の一般化
sealed class Tentative<Reason, A> {
data class Valid<A>(val value: A) : Tentative<Nothing, A>() // 有効な値を表す
data class Invalid<Reason, A>(val value: A, val reason: Reason) : Tentative<Reason, A>() // 無効な値とその理由を表す
}
// 使用例
typealias MyReason = String // 理由の型
typealias MyValue = Int // 値の型
val validValue: Tentative<MyReason, MyValue> = Tentative.Valid(42)
val invalidValue: Tentative<MyReason, MyValue> = Tentative.Invalid(42, "Out of range")
// 状態を判定する関数
fun handleTentative(tentative: Tentative<MyReason, MyValue>) {
when (tentative) {
is Tentative.Valid -> println("Valid value: ${tentative.value}")
is Tentative.Invalid -> println("Invalid value: ${tentative.value}, Reason: ${tentative.reason}")
}
}
handleTentative(validValue) // 出力: Valid value: 42
handleTentative(invalidValue) // 出力: Invalid value: 42, Reason: Out of range
ファントム型とやら
// Kotlin のファントム型の実装例
// 通貨を表すタグクラスを定義
sealed class Currency
object USD : Currency()
object EUR : Currency()
// ファントム型を使った Money 型の定義
data class Money<T : Currency>(val amount: Double)
// ファントム型を使った Money を生成する関数
fun createMoneyUSD(amount: Double): Money<USD> = Money(amount)
fun createMoneyEUR(amount: Double): Money<EUR> = Money(amount)
// 同じ通貨の Money を加算する関数
fun <T : Currency> addMoney(m1: Money<T>, m2: Money<T>): Money<T> {
return Money(m1.amount + m2.amount)
}
// ファントム型を使った例
fun main() {
val usdAmount: Money<USD> = createMoneyUSD(100.0)
val usdAmount2: Money<USD> = createMoneyUSD(50.0)
val eurAmount: Money<EUR> = createMoneyEUR(80.0)
val totalUSD: Money<USD> = addMoney(usdAmount, usdAmount2) // 正しい
println("Total USD: $totalUSD")
// 型安全性により、異なる通貨同士の加算はコンパイル時にエラーが発生します
// val errorExample = addMoney(usdAmount, eurAmount) // コンパイルエラー
}
Kotlinで継続モナドの例を再現するには、まず継続モナドの基本構造をKotlinで定義する必要があります。Kotlinでは、継続を表現するために高階関数とラムダを使用します。
Kotlinでの継続モナドの基本構造
以下のコードは、Kotlinで継続モナドを実装し、Scalaで示された例を再現するものです。
継続モナドの定義
// 継続モナドの基本クラス
class Cont<R, A>(val run: (A -> R) -> R) {
// map 関数 (Functor の実装)
fun <B> map(f: (A) -> B): Cont<R, B> =
Cont { k -> run { a -> k(f(a)) } }
// flatMap 関数 (Monad の実装)
fun <B> flatMap(f: (A) -> Cont<R, B>): Cont<R, B> =
Cont { k -> run { a -> f(a).run(k) } }
}
-
run
:-
run
は(A -> R) -> R
型の関数で、次のステップを表すA -> R
を受け取って、最終的にR
を返します。
-
-
map
:-
map
はA
からB
への変換を行うファンクションを受け取り、それをCont
に適用します。 -
k
は次のステップの継続であり、f(a)
を適用した結果をk
に渡します。
-
-
flatMap
:-
flatMap
は、A
からCont<R, B>
への変換を行うファンクションを受け取り、それをCont
に適用します。 -
f(a)
にa
を渡して、新しいCont<R, B>
を得て、そのrun
を呼び出し、次のステップk
に渡します。
-
使用例:計算の遅延実行
継続モナドを使って、計算を遅延実行する例を示します。
// 加算操作の例
fun add(a: Int, b: Int): Cont<Int, Int> = Cont { k ->
println("Adding $a and $b")
k(a + b)
}
// 乗算操作の例
fun multiply(a: Int, b: Int): Cont<Int, Int> = Cont { k ->
println("Multiplying $a and $b")
k(a * b)
}
// メイン関数
fun main() {
// 継続モナドを使って遅延計算を構築
val delayedCalculation: Cont<Int, Int> = add(1, 2).flatMap { sum ->
multiply(sum, 3)
}
// 計算の実行
delayedCalculation.run { result ->
println("Result: $result")
}
}
-
add
:-
add
関数は、a
とb
を加算し、その結果を次のステップk
に渡します。
-
-
multiply
:-
multiply
関数は、a
とb
を乗算し、その結果を次のステップk
に渡します。
-
-
遅延計算:
-
add(1, 2)
を実行し、その結果(sum
)をmultiply(sum, 3)
に渡します。 - これらの計算は
flatMap
を使って連鎖されます。
-
-
計算の実行:
-
delayedCalculation.run
で計算が実行され、最終的な結果が出力されます。
-
使用例:フロー制御
継続モナドを使って、計算のフローを制御する例を示します。
// 正の値をチェックする例
fun checkPositive(value: Int): Cont<String, Int> = Cont { k ->
if (value > 0) {
println("Value $value is positive")
k(value)
} else {
println("Value $value is not positive")
"Value is not positive"
}
}
fun main() {
// 継続モナドを使ってフロー制御を構築
val controlledFlow: Cont<String, Int> = checkPositive(5).flatMap { value ->
checkPositive(value - 10)
}
// フローの実行
controlledFlow.run { result ->
when (result) {
is Int -> println("Flow succeeded with result: $result")
is String -> println("Flow failed with error: $result")
}
}
}
-
checkPositive
:-
value
が正であることをチェックし、正なら次のステップk
に渡します。 - 負の場合は、エラーメッセージを返します。
-
-
フローの制御:
-
checkPositive(5)
を実行し、その結果(value
)をcheckPositive(value - 10)
に渡します。 - 2番目のチェックで負の値になると、エラーメッセージが返されます。
-
-
フローの実行:
-
controlledFlow.run
でフローが実行され、結果が出力されます。
-
まとめ
-
継続モナド(Continuation Monad):
- 継続モナドは、計算の次のステップを引数として受け取り、計算のフローを制御する手段です。
- 計算の遅延実行や非同期処理、フロー制御を実現するのに適しています。
-
Kotlinでの実装:
- Kotlinでは、継続モナドを
Cont
クラスとして定義し、map
とflatMap
を使って操作します。 - 使用例を通じて、計算の遅延実行やフロー制御の仕組みを示しました。
- Kotlinでは、継続モナドを
継続モナドは、制御フローを柔軟に扱うための強力なツールであり、特に複雑な非同期処理やバックトラッキングが必要な場面で有効です。
Kotlinでは多重継承はサポートされていませんが、インターフェースを使って多重継承のような構造を構成することが可能です。ここでは、複数のsealed interface
を使用して多重継承関係のADTを構成し、それをパターンマッチする例を示します。
Step 1: 多重継承関係のADTを定義
sealed interface Animal
sealed interface CanFly : Animal
sealed interface CanSwim : Animal
data class Duck(val name: String) : CanFly, CanSwim
data class Eagle(val name: String) : CanFly
data class Dolphin(val name: String) : CanSwim
Step 2: 多重継承関係の型でパターンマッチを行う関数の定義
fun describeAnimal(animal: Animal): String = when (animal) {
is CanFly -> when (animal) {
is CanSwim -> describeFlyingSwimmingAnimal(animal)
else -> describeFlyingAnimal(animal)
}
is CanSwim -> describeSwimmingAnimal(animal)
}
fun describeFlyingSwimmingAnimal(animal: CanFly & CanSwim): String = when (animal) {
is Duck -> "A duck named ${animal.name} that can fly and swim"
else -> "Unknown animal that can fly and swim"
}
fun describeFlyingAnimal(animal: CanFly): String = when (animal) {
is Eagle -> "An eagle named ${animal.name} that can fly"
else -> "Unknown animal that can fly"
}
fun describeSwimmingAnimal(animal: CanSwim): String = when (animal) {
is Dolphin -> "A dolphin named ${animal.name} that can swim"
else -> "Unknown animal that can swim"
}
Step 3: 使用例
fun main() {
val animals: List<Animal> = listOf(
Duck("Donald"),
Eagle("Majestic"),
Dolphin("Flipper")
)
animals.forEach { animal ->
println(describeAnimal(animal))
}
}
このコードは、以下のような出力を生成します。
A duck named Donald that can fly and swim
An eagle named Majestic that can fly
A dolphin named Flipper that can swim
説明
-
sealed interface Animal
は、動物の基底インターフェースです。 -
sealed interface CanFly
およびsealed interface CanSwim
は、それぞれ飛ぶことと泳ぐことができる動物を表すインターフェースです。 -
Duck
はCanFly
とCanSwim
の両方を実装しています。 -
Eagle
はCanFly
を実装し、Dolphin
はCanSwim
を実装しています。 -
describeAnimal
関数では、Animal
レベルでパターンマッチを行い、CanFly
やCanSwim
インターフェースに基づいてさらに具体的な関数を呼び出します。 -
describeFlyingSwimmingAnimal
、describeFlyingAnimal
、およびdescribeSwimmingAnimal
関数では、それぞれの具体的な型に応じたパターンマッチを行います。
これにより、Kotlinで多重継承関係のADTを定義し、それをパターンマッチする方法がわかります。
Kotlinのデストラクチャリング宣言
データクラスの例
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("John", 30)
val (name, age) = person
println(name) // John
println(age) // 30
}
sealed class Hand {
object Rock : Hand()
object Paper : Hand()
object Scissors : Hand()
}
enumよりこの方がいい?
アクティブパターン
ディシジョンテーブル(意思決定のマッピング)とディシジョンロジック(分類の決定ロジック)の分離。
// アドレスを表すデータクラス
data class Address(val country: String, val state: String)
// 配送先の種類を表すシールドクラス
sealed class ShippingDestination {
object UsLocalState : ShippingDestination()
object UsRemoteState : ShippingDestination()
object International : ShippingDestination()
}
// 注文を表すデータクラス
data class ValidatedOrder(val shippingAddress: Address)
// 配送先を決定する関数
fun determineDestination(address: Address): ShippingDestination = when {
address.country == "US" -> when (address.state) {
"CA", "OR", "AZ", "NV" -> ShippingDestination.UsLocalState
else -> ShippingDestination.UsRemoteState
}
else -> ShippingDestination.International
}
// 配送コストを計算する関数
fun calculateShippingCost(order: ValidatedOrder): Double {
val destination = determineDestination(order.shippingAddress)
return when (destination) {
is ShippingDestination.UsLocalState -> 5.0
is ShippingDestination.UsRemoteState -> 10.0
is ShippingDestination.International -> 20.0
}
}
// 使用例
fun main() {
val localOrder = ValidatedOrder(Address("US", "CA"))
val remoteOrder = ValidatedOrder(Address("US", "NY"))
val internationalOrder = ValidatedOrder(Address("JP", "Tokyo"))
fun processOrder(order: ValidatedOrder) {
val cost = calculateShippingCost(order)
println("Shipping cost for ${order.shippingAddress}: $cost")
}
processOrder(localOrder)
processOrder(remoteOrder)
processOrder(internationalOrder)
}
// MultOf3拡張関数
fun Int.isMultOf3(): Boolean = this % 3 == 0
// MultOf5拡張関数
fun Int.isMultOf5(): Boolean = this % 5 == 0
// fizzBuzz関数
fun fizzBuzz(i: Int): String {
return when {
i.isMultOf3() && i.isMultOf5() -> "FizzBuzz"
i.isMultOf3() -> "Fizz"
i.isMultOf5() -> "Buzz"
else -> i.toString()
}
}
// テスト
fun main() {
(1..20).forEach {
val result = fizzBuzz(it)
print("$result, ")
}
}
// 3の倍数をチェックする関数
fun isMultOf3(i: Int): Boolean = i % 3 == 0
// 5の倍数をチェックする関数
fun isMultOf5(i: Int): Boolean = i % 5 == 0
// fizzBuzz関数
fun fizzBuzz(i: Int): String {
return when {
isMultOf3(i) && isMultOf5(i) -> "FizzBuzz"
isMultOf3(i) -> "Fizz"
isMultOf5(i) -> "Buzz"
else -> i.toString()
}
}
// テスト
fun main() {
(1..20).forEach {
val result = fizzBuzz(it)
print("$result, ")
}
}
sealed class FizzBuzzPatterns {
object Fizz : FizzBuzzPatterns()
object Buzz : FizzBuzzPatterns()
object FizzBuzz : FizzBuzzPatterns()
object Number : FizzBuzzPatterns()
companion object {
fun categorizeNumber(number: Int): FizzBuzzPatterns {
return when {
number % 15 == 0 -> FizzBuzz
number % 3 == 0 -> Fizz
number % 5 == 0 -> Buzz
else -> Number
}
}
}
}
fun fizzBuzz(number: Int): String {
return when (FizzBuzzPatterns.categorizeNumber(number)) {
FizzBuzzPatterns.Fizz -> "Fizz"
FizzBuzzPatterns.Buzz -> "Buzz"
FizzBuzzPatterns.FizzBuzz -> "FizzBuzz"
FizzBuzzPatterns.Number -> number.toString()
}
}
fun main() {
for (i in 1..100) {
println(fizzBuzz(i))
}
}
- object: toString(), hashCode(), equals() の基本的な実装のみ。
- data object: データクラスと同様に、toString(), hashCode(), equals() の実装が自動生成され、プロパティの値を考慮します。
ユニオンの列挙はdata object
便利そう
タイトル
// 意思決定材料のinputデータ
data class Customer(
val userId: String,
val country: String,
val address: Address,
)
sealed class Address {
data class Domestic(
val prefecture: String, // 簡単のためStringで
val detail: String
) : Address()
data class International(
val detail: String // Tips: "海外"だが"居住県"が入力されるという事態はそもそも型で起こさない
) : Address()
}
// 決定のロジック
data class ShippingEstimate(
val fee: Int,
val days: TimeCommitment
)
sealed class TimeCommitment {
data class Days(val count: Int) : TimeCommitment()
data object BestEffort : TimeCommitment()
}
fun estimateShippingCost(address: Address, orderCount: Int): ShippingEstimate {
val destination = DestinationCategory.pattern(address)
val membership = MembershipCategory.pattern(orderCount)
return when (destination) {
is JapanNearby -> when (membership) {
is General ->
ShippingEstimate(fee = 500, days = TimeCommitment.Days(3))
is VIP ->
ShippingEstimate(fee = 0, days = TimeCommitment.Days(1))
}
is JapanDistant -> when (membership) {
is General ->
ShippingEstimate(fee = 1000, days = TimeCommitment.Days(5))
is VIP ->
ShippingEstimate(fee = 0, days = TimeCommitment.Days(3))
}
is Overseas ->
ShippingEstimate(fee = 3000, days = TimeCommitment.BestEffort)
}
}
// 知識による分類ロジック
sealed class DestinationCategory {
data object JapanNearby : DestinationCategory() // Tips: ユニオンの列挙にdata objectが便利
data object JapanDistant : DestinationCategory()
data object Overseas : DestinationCategory()
companion object {
private val nearbyPrefectures = setOf(
"東京都", "神奈川県", "埼玉県", "千葉県",
"山梨県", "群馬県", "栃木県"
)
fun pattern(address: Address): DestinationCategory {
return when (address) {
is Address.Domestic -> when {
// "近隣県"というドメイン用語の定義
nearbyPrefectures.contains(address.prefecture) -> JapanNearby
// "遠隔県"というドメイン用語の定義
else -> JapanDistant
}
is Address.International -> Overseas
}
}
}
}
sealed class MembershipCategory {
data object General : MembershipCategory() // 一般会員
data object VIP : MembershipCategory() // VIP会員
companion object {
fun pattern(count: Int): MembershipCategory = when {
count >= 10 -> VIP
else -> General
}
}
}
// shippingWorkflowが発行する想定のドメインイベント
data class OrderShipped(
val orderId: String,
val user: Customer
)
// このドメインイベントを元に、遅延補償ワークフローがトリガーする想定
data class CustomerReceived(
val userId: String,
val daysTaken: Int
)
sealed class DelayCategory {
data object NoDelay : DelayCategory()
data object Delay : DelayCategory()
data object SignificantDelay : DelayCategory()
companion object {
fun pattern(daysTaken: Int, estimatedDays: Int): DelayCategory {
val delayDays = daysTaken - estimatedDays
return when {
delayDays <= 0 -> NoDelay
delayDays in 1..4 -> Delay
else -> SignificantDelay
}
}
}
}
data class Compensation(
val freeShippingCoupon: Boolean,
val giftCardValue: Int
)
fun compensationPolicy(
order: CustomerReceived,
orderShipped: OrderShipped
): Compensation {
val membershipAtShipped = MembershipCategory.pattern(totalOrderCount(orderShipped.user.userId))
val estimatedDays = estimateShippingCost(orderShipped.user.address, totalOrderCount(orderShipped.user.userId)).days
val delayType = when (estimatedDays) {
is TimeCommitment.Days -> DelayCategory.pattern(order.daysTaken, estimatedDays.count)
is TimeCommitment.BestEffort -> DelayCategory.NoDelay
}
return when (delayType) {
is DelayCategory.NoDelay -> Compensation(freeShippingCoupon = false, giftCardValue = 0)
is DelayCategory.Delay -> when (membershipAtShipped) {
is General -> Compensation(freeShippingCoupon = true, giftCardValue = 0)
is VIP -> Compensation(freeShippingCoupon = true, giftCardValue = 2000)
}
is DelayCategory.SignificantDelay -> when (membershipAtShipped) {
is General -> Compensation(freeShippingCoupon = true, giftCardValue = 1000)
is VIP -> Compensation(freeShippingCoupon = true, giftCardValue = 2000)
}
}
}
// 使用例のテスト
fun main() {
val customer = Customer(
userId = "new_user",
country = "日本",
address = Address.Domestic(prefecture = "東京都", detail = "千代田区")
)
val customer2 = Customer(
userId = "vip_user",
country = "日本",
address = Address.Domestic(prefecture = "神奈川県", detail = "横浜")
)
val customer3 = Customer(
userId = "user",
country = "アメリカ",
address = Address.International(detail = "カリフォルニア")
)
val orderShipped1 = OrderShipped(orderId = "order1", user = customer)
val orderShipped2 = OrderShipped(orderId = "order2", user = customer2)
val orderShipped3 = OrderShipped(orderId = "order3", user = customer3)
val receivedOrder1 = CustomerReceived(userId = "user", daysTaken = 4)
val receivedOrder2 = CustomerReceived(userId = "vip_user", daysTaken = 3)
val receivedOrder3 = CustomerReceived(userId = "new_user", daysTaken = 6)
println(compensationPolicy(receivedOrder1, orderShipped1))
// Output: Compensation(freeShippingCoupon=true, giftCardValue=0)
println(compensationPolicy(receivedOrder2, orderShipped2))
// Output: Compensation(freeShippingCoupon=true, giftCardValue=2000)
println(compensationPolicy(receivedOrder3, orderShipped3))
// Output: Compensation(freeShippingCoupon=false, giftCardValue=0)
}
fun shippingWorkflow(customer: Customer) {
totalOrderCount(customer.userId)
.let { orderCount -> estimateShippingCost(customer.address, orderCount) }
.let { estimate -> shipOrder(estimate) }
}
fun totalOrderCount(userId: String): Int {
val orderHistoryState = mapOf(
"user" to 8,
"vip_user" to 15,
)
return orderHistoryState[userId] ?: 0
}
fun shipOrder(estimate: ShippingEstimate) {
println("Order is being shipped with the following details:")
println("送料: ¥${estimate.fee}")
println("配達日数目標: ${(estimate.days)}")
}
2.1のwhen guard
// 各タスクを定義する純粋関数
fun curriedTask1(): (Int) -> (Int) -> Int = { x ->
{ y ->
x * y
}
}
fun task2(x: Int): Int = x * 3
fun task3(x: Int): Int = x + 10
// ワークフローを動的に構成する関数
fun createWorkflow(input: Int): (Int) -> Int {
val curriedTask1PartiallyApplied = curriedTask1()(input) // curriedTask1をinputで部分適用する
val tasks: List<(Int) -> Int> = when {
input < 5 -> listOf(curriedTask1PartiallyApplied, ::task3) // task1は入力がbakeされた状態
else -> listOf(curriedTask1PartiallyApplied, ::task2, ::task3) // 同様にtask1は入力がbakeされた状態
}
// ワークフローを構成する関数リストを合成して関数を返す
return tasks.reduce { acc, task -> { x -> task(acc(x)) } }
}
// テスト
fun main() {
val input1 = 3
val input2 = 6
val workflow1 = createWorkflow(input1)
val result1 = workflow1(4) // curriedTask1(3)(4) = 12, then task3(12) = 22
println("Result for input1: $result1") // タスク1とタスク3のみ実行
val workflow2 = createWorkflow(input2)
val result2 = workflow2(5) // curriedTask1(6)(5) = 30, task2(30) = 90, task3(90) = 100
println("Result for input2: $result2") // 全てのタスクを実行
}
import java.time.LocalDate
// 保険契約者の情報
data class Customer(
val age: Int,
val smoker: Boolean,
val healthConditions: List<String>
)
// 保険の種類
enum class InsuranceType { LIFE, HEALTH, AUTO }
// 保険料計算のための環境
data class PricingEnv(
var baseRate: Double,
val currentDate: LocalDate
)
// Writer モナドの簡易実装
data class Writer<T>(val value: T, val log: List<String>)
// PricingM モナド(ReaderT + Writer の簡易実装)
typealias PricingM<T> = (PricingEnv) -> Writer<T>
// モナド操作のヘルパー関数
fun <T> PricingM<T>.flatMap(f: (T) -> PricingM<T>): PricingM<T> = { env ->
val (value, log1) = this(env)
val (newValue, log2) = f(value)(env)
Writer(newValue, log1 + log2)
}
fun <T> PricingM<T>.map(f: (T) -> T): PricingM<T> = { env ->
val (value, log) = this(env)
Writer(f(value), log)
}
// DSL 関数
fun setBaseRate(rate: Double): PricingM<Unit> = { env ->
env.baseRate = rate
Writer(Unit, listOf("基本料金を設定: $rate"))
}
fun adjustForAge(customer: Customer): PricingM<Double> = { env ->
val factor = if (customer.age > 50) 1.5 else 1.0
Writer(env.baseRate * factor, listOf("年齢による調整係数: $factor"))
}
fun adjustForSmoking(customer: Customer): PricingM<Double> = { env ->
val factor = if (customer.smoker) 1.5 else 1.0
Writer(env.baseRate * factor, listOf("喫煙状態による調整係数: $factor"))
}
fun adjustForHealth(customer: Customer): PricingM<Double> = { env ->
val factor = 1.0 + (0.1 * customer.healthConditions.size)
Writer(env.baseRate * factor, listOf("健康状態による調整係数: $factor"))
}
fun finalAdjustment(insuranceType: InsuranceType): PricingM<Double> = { env ->
val factor = when (insuranceType) {
InsuranceType.LIFE -> 1.2
InsuranceType.HEALTH -> 1.1
InsuranceType.AUTO -> 1.0
}
Writer(env.baseRate * factor, listOf("保険種類による最終調整係数: $factor"))
}
// 保険料計算のメイン関数
fun calculatePremium(customer: Customer, insType: InsuranceType): PricingM<Double> = { env ->
val result = setBaseRate(1000.0)
.flatMap { adjustForAge(customer) }
.flatMap { adjustForSmoking(customer) }
.flatMap { adjustForHealth(customer) }
.flatMap { finalAdjustment(insType) }
result(env)
}
// DSLの使用例
fun main() {
val env = PricingEnv(0.0, LocalDate.now())
val customer = Customer(age = 55, smoker = true, healthConditions = listOf("高血圧"))
val (premium, logs) = calculatePremium(customer, InsuranceType.LIFE)(env)
println("保険料計算ログ:")
logs.forEach(::println)
println("最終保険料: $premium")
}
import kotlin.math.max
data class InventoryItem(val id: String, val name: String, var quantity: Int)
typealias InventoryState = Map<String, InventoryItem>
class State<S, A>(val run: (S) -> Pair<A, S>) {
companion object {
fun <S, A> pure(a: A): State<S, A> = State { s -> Pair(a, s) }
}
fun <B> flatMap(f: (A) -> State<S, B>): State<S, B> = State { s ->
val (a, s1) = this.run(s)
f(a).run(s1)
}
fun <B> map(f: (A) -> B): State<S, B> = flatMap { a -> pure(f(a)) }
}
object InventoryDSL {
// 共通の環境を定義
var logger: (String) -> Unit = { println(it) }
var maxCapacity: Int = 1000
var lowStockThreshold: Int = 10
private fun log(message: String) = State<InventoryState, Unit> { state ->
logger(message)
Pair(Unit, state)
}
fun addItem(item: InventoryItem): State<InventoryState, Unit> = State { state ->
val newState = state + (item.id to item)
if (newState.values.sumOf { it.quantity } > maxCapacity) {
throw IllegalStateException("Exceeding max capacity")
}
Pair(Unit, newState)
}.flatMap { log("Added item: ${item.id}") }
fun removeItem(id: String): State<InventoryState, Unit> = State { state ->
val newState = state - id
Pair(Unit, newState)
}.flatMap { log("Removed item: $id") }
fun updateQuantity(id: String, delta: Int): State<InventoryState, Unit> = State { state ->
val item = state[id] ?: throw IllegalArgumentException("Item not found: $id")
val updatedItem = item.copy(quantity = max(0, item.quantity + delta))
val newState = state + (id to updatedItem)
Pair(Unit, newState)
}.flatMap {
log("Updated quantity for item $id: delta=$delta")
}.flatMap {
checkLowStock(id)
}
fun getItemQuantity(id: String): State<InventoryState, Int> = State { state ->
val quantity = state[id]?.quantity ?: throw IllegalArgumentException("Item not found: $id")
Pair(quantity, state)
}
fun listItems(): State<InventoryState, List<InventoryItem>> = State { state ->
Pair(state.values.toList(), state)
}
private fun checkLowStock(id: String): State<InventoryState, Unit> = getItemQuantity(id).flatMap { quantity ->
if (quantity <= lowStockThreshold) {
log("Low stock alert for item $id: quantity=$quantity")
} else {
State.pure(Unit)
}
}
}
fun manageInventory(): State<InventoryState, Unit> {
return InventoryDSL.run {
addItem(InventoryItem("A001", "Widget", 100))
.flatMap { addItem(InventoryItem("B002", "Gadget", 50)) }
.flatMap { updateQuantity("A001", -95) }
.flatMap { getItemQuantity("A001").map { quantity -> println("Current quantity of Widget: $quantity") } }
.flatMap { updateQuantity("B002", 30) }
.flatMap { removeItem("A001") }
.flatMap { listItems().map { items -> println("Remaining items: ${items.joinToString { it.name }}") } }
}
}
fun runInventoryManagement(initialState: InventoryState): InventoryState {
// カスタム環境の設定
InventoryDSL.logger = { message -> println("LOG: $message") }
InventoryDSL.maxCapacity = 500
InventoryDSL.lowStockThreshold = 20
return manageInventory().run(initialState).second
}
fun main() {
val initialState = emptyMap<String, InventoryItem>()
val finalState = runInventoryManagement(initialState)
println("Final inventory state: $finalState")
}
inline fun <reified T> 実行時に型情報を保持するイディオム
Kotlinでのinline fun <reified T>
の使用例を関数型プログラミングの文脈で示すと、型安全な処理の実行や型に基づく条件分岐を利用した関数を作成することができます。特に、ジェネリクスと組み合わせることで、型に基づいた高階関数の実行や処理パイプラインの構築が可能です。
以下では、関数型プログラミングの考え方を利用し、reified
を活用した使用例を紹介します。
1. 型に応じた処理の選択
ここでは、リストの要素の型に応じた変換を行う例を示します。reified
を使って、型安全な変換処理を型に応じて実行することで、関数型プログラミングのスタイルで処理を定義できます。
inline fun <reified T> List<Any>.mapIfType(transform: (T) -> T): List<Any> {
return this.map {
if (it is T) {
transform(it)
} else {
it
}
}
}
// 使用例
val mixedList = listOf(1, "Hello", 2, "World")
val result = mixedList.mapIfType<String> { it.toUpperCase() }
println(result) // 出力: [1, "HELLO", 2, "WORLD"]
説明
-
mapIfType
: この関数は、リスト内の要素の型に応じて特定の変換を行います。reified T
を使うことで、T
の型に基づいてis
チェックを行い、対応する型にのみ変換処理を適用しています。 - 関数型のスタイルで、型安全に部分的な処理をパイプラインとして適用できます。
2. 型に基づいた関数の実行
関数型プログラミングでは、関数を引数として渡すことで処理の柔軟性を高めます。以下の例では、reified
を活用して、型ごとに異なる関数を実行する関数を作成します。
inline fun <reified T> executeIfType(value: Any, ifTrue: (T) -> Unit, ifFalse: () -> Unit) {
if (value is T) {
ifTrue(value)
} else {
ifFalse()
}
}
// 使用例
val data: Any = "Kotlin"
executeIfType<String>(data,
ifTrue = { println("String detected: $it") },
ifFalse = { println("Not a string") }
) // 出力: "String detected: Kotlin"
executeIfType<Int>(data,
ifTrue = { println("Int detected: $it") },
ifFalse = { println("Not an Int") }
) // 出力: "Not an Int"
説明
-
executeIfType
: これは、与えられた値が指定した型に一致する場合に処理を行う関数です。reified
によって、ジェネリック型T
に対して型チェックを行い、型が一致した場合にifTrue
の関数が実行されます。型が一致しない場合には、ifFalse
が実行されます。 - これにより、型に基づいた柔軟な処理パイプラインを構築でき、型安全に異なる処理を組み合わせて実行できます。
3. 型ごとの変換処理を高階関数で表現
関数型プログラミングでは、リストやコレクションの変換処理がよく行われます。ここでは、reified
を使って特定の型ごとに異なる変換を行う例を示します。
inline fun <reified T, R> transformIfType(list: List<Any>, transform: (T) -> R): List<Any> {
return list.map {
if (it is T) {
transform(it)
} else {
it
}
}
}
// 使用例
val mixedList2 = listOf(1, "Hello", 2, "World")
val transformedList = transformIfType<String, String>(mixedList2) { it.reversed() }
println(transformedList) // 出力: [1, "olleH", 2, "dlroW"]
val intTransformedList = transformIfType<Int, Int>(mixedList2) { it * 2 }
println(intTransformedList) // 出力: [2, "Hello", 4, "World"]
説明
-
transformIfType
: 与えられたリスト内で特定の型にのみ変換処理を適用する関数です。型安全にリスト内の要素を特定の型に変換することで、型ごとの変換処理が簡潔に書けます。 - 高階関数
transform
を利用することで、関数型プログラミングの柔軟性を持たせています。
4. 型安全なフィルタリングと変換
次の例では、reified
を使ってリスト内の特定の型の要素を抽出し、さらにそれを変換するパターンを示します。
inline fun <reified T, R> filterAndTransform(list: List<Any>, transform: (T) -> R): List<R> {
return list.filterIsInstance<T>().map(transform)
}
// 使用例
val mixedList3 = listOf(1, "Hello", 2, "World", 3)
val resultStrings = filterAndTransform<String, String>(mixedList3) { it.toUpperCase() }
println(resultStrings) // 出力: ["HELLO", "WORLD"]
val resultInts = filterAndTransform<Int, Int>(mixedList3) { it * 10 }
println(resultInts) // 出力: [10, 20, 30]
説明
-
filterAndTransform
: これは、リスト内で特定の型の要素だけをフィルタリングし、それを変換する処理です。filterIsInstance<T>()
は、リスト内の要素を指定した型T
に一致するものだけにフィルタリングします。さらに、それらの要素を関数transform
で変換します。 - このパターンは、型に応じたフィルタリングと変換を関数型プログラミングスタイルで実現しています。
まとめ
Kotlinのinline fun <reified T>
を関数型プログラミングの文脈で利用することで、以下のメリットがあります。
-
型安全な関数パイプラインの構築:
-
reified
を使うことで、実行時に型に基づいた安全な処理が行え、型チェックや型変換が容易になります。
-
-
柔軟な高階関数の実行:
- 型ごとに異なる処理を高階関数として渡し、それを型安全に実行することができます。
-
簡潔で表現力豊かな型ベースの操作:
-
reified
を使うことで、リフレクションを使わずに型に応じたフィルタリングや変換、条件分岐を行うコードが書けます。
-
このように、reified
を関数型プログラミングと組み合わせることで、Kotlinの型安全性と柔軟性をさらに活用できる処理が実現できます。
// 依存関係にkotlin-resultを追加(build.gradle.ktsの場合)
// implementation("com.michael-bull.kotlin-result:kotlin-result:1.1.16")
// 必要なインポート
import com.github.michaelbull.result.*
import com.github.michaelbull.result.coroutines.*
import com.github.michaelbull.result.mapBoth
// アカウントとエラーの定義
data class Account(val balance: Int)
sealed class TransactionError {
object InsufficientFunds : TransactionError() {
override fun toString() = "残高不足"
}
}
// Transactionモナドの定義
typealias Transaction<A> = suspend (Account) -> Result<Pair<A, Account>, TransactionError>
// 入金操作の定義
fun deposit(amount: Int): Transaction<Unit> = { account ->
val newAccount = account.copy(balance = account.balance + amount)
Ok(Unit to newAccount)
}
// 出金操作の定義
fun withdraw(amount: Int): Transaction<Unit> = { account ->
if (account.balance >= amount) {
val newAccount = account.copy(balance = account.balance - amount)
Ok(Unit to newAccount)
} else {
Err(TransactionError.InsufficientFunds)
}
}
// Transactionモナドの合成関数
fun <A, B> Transaction<A>.flatMap(f: (A) -> Transaction<B>): Transaction<B> = { account ->
this(account).flatMap { (a, newAccount) ->
f(a)(newAccount)
}
}
fun <A> pure(a: A): Transaction<A> = { account ->
Ok(a to account)
}
// 取引シーケンスのDSL
val transactionScript: Transaction<Unit> =
deposit(1000).flatMap {
withdraw(500).flatMap {
withdraw(600) // この操作でエラーが発生
}
}
// トランザクションの実行関数
suspend fun runTransaction(transaction: Transaction<Unit>, account: Account): Result<Account, TransactionError> {
return transaction(account).map { it.second }
}
// 実行例
suspend fun main() {
val initialAccount = Account(balance = 0)
val result = runTransaction(transactionScript, initialAccount)
result.mapBoth(
success = { println("取引成功: 残高は${it.balance}円です。") },
failure = { println("取引失敗: $it") }
)
}
// 出力結果:
// 取引失敗: 残高不足
実務の業務フローは、より複雑で、多くのステップや条件分岐、エラー処理が絡むことが多いです。さらに、外部システムとの連携や複数のデータベース、認証処理なども加わる可能性があります。以下のように、実務のシナリオにさらに近づけた形でDSLを改善します。
実務に近いシナリオ
-
注文受付:
Order
を受け付ける。 -
在庫確認と引当:
Inventory
を確認し、在庫がなければ外部倉庫から取り寄せを依頼する。引当できない場合はキャンセル。 - 顧客の支払い認証:認証システムと連携して支払いを認証。失敗した場合はキャンセル。
-
請求書生成:
Order
からInvoice
を生成する。 - 出荷準備:複数の出荷オプション(標準、翌日配送、同日配送)から選択する。特定の地域は配達不可。
- 顧客通知:注文完了を顧客に通知する。
追加要素
- 外部API連携(在庫管理システムや認証システム)
- 条件付きの業務フロー(地域制限や特定条件での分岐)
- 複数の依存関係(支払いが完了しなければ次に進まない、など)
Kotlinによる実務に近いDSLの実装
1. データクラスの拡張
data class Order(val orderId: String, val customerId: String)
data class Inventory(val inventoryId: String, val stock: Int, val location: String)
data class Invoice(val invoiceId: String, val relatedOrder: Order)
data class Payment(val paymentId: String, val relatedInvoice: Invoice)
data class Shipment(val shipmentId: String, val relatedOrder: Order, val method: String)
data class Notification(val message: String)
data class Customer(val customerId: String, val email: String, val region: String)
2. 新しいエラー型の定義
sealed class DomainError {
object InventoryUnavailable : DomainError()
object PaymentFailed : DomainError()
object ShipmentFailed : DomainError()
object ExternalApiError : DomainError() // 外部API呼び出しに失敗
object InvalidShipmentMethod : DomainError() // 出荷方法が不正
object UnauthorizedPayment : DomainError() // 支払い認証に失敗
data class UnknownError(val message: String) : DomainError()
}
3. 外部システム連携をシミュレートする関数
ここでは、実際の外部API呼び出しは行いませんが、外部システムとの連携を模倣します。
fun externalInventoryCheck(order: Order): Result<Inventory, DomainError> {
println("外部在庫システムに問い合わせています: ${order.orderId}")
// 仮定として外部倉庫で在庫が見つかった場合
val inventoryFound = true
return if (inventoryFound) {
Ok(Inventory("INV-EXT-${order.orderId}", 50, "External Warehouse"))
} else {
Err(DomainError.InventoryUnavailable)
}
}
fun paymentAuthorization(order: Order): Result<Boolean, DomainError> {
println("支払い認証システムと連携しています: ${order.orderId}")
// 支払い認証が成功する仮定
val paymentAuthorized = true
return if (paymentAuthorized) {
Ok(true)
} else {
Err(DomainError.UnauthorizedPayment)
}
}
4. 業務処理の関数
各関数は、Result<T, DomainError>
でエラー処理を扱い、エラーが発生した場合は即座に処理を中断します。
fun acceptOrder(orderId: String, customerId: String): Result<Order, DomainError> {
println("注文を受け付けました: $orderId")
return Ok(Order(orderId, customerId))
}
fun allocateInventory(order: Order): Result<Inventory, DomainError> {
println("在庫を確認しています: ${order.orderId}")
val stockAvailable = false // 仮定: 自社倉庫には在庫がない
return if (stockAvailable) {
Ok(Inventory("INV-${order.orderId}", 100, "Main Warehouse"))
} else {
println("自社倉庫に在庫がありません。外部システムに問い合わせます...")
externalInventoryCheck(order) // 外部在庫システムに問い合わせ
}
}
fun generateInvoice(order: Order): Result<Invoice, DomainError> {
val invoiceId = "INV-${order.orderId}"
println("請求書を生成しました: $invoiceId")
return Ok(Invoice(invoiceId, order))
}
fun selectShipmentMethod(order: Order, region: String): Result<String, DomainError> {
println("出荷方法を選択しています: ${order.orderId}")
return when (region) {
"restricted_area" -> Err(DomainError.InvalidShipmentMethod)
else -> Ok("Standard Shipping") // 地域に応じた出荷方法
}
}
fun createShipment(order: Order, method: String): Result<Shipment, DomainError> {
val shipmentId = "SHIP-${order.orderId}"
println("出荷情報を生成しています: $shipmentId, method: $method")
return Ok(Shipment(shipmentId, order, method))
}
fun sendNotification(shipment: Shipment, customer: Customer): Result<Notification, DomainError> {
val message = "注文 ${shipment.relatedOrder.orderId} が出荷されました。"
println("顧客 ${customer.email} に通知を送信しました: $message")
return Ok(Notification(message))
}
5. 業務フロー
業務フロー内では、各ステップで条件分岐や外部システム連携が必要です。result
コンテキストとbind()
を使って複雑なエラー処理と外部API連携をシンプルに表現します。
suspend fun orderWorkflow(orderId: String, customer: Customer): Result<Unit, DomainError> = result {
val order = acceptOrder(orderId, customer.customerId).bind()
val inventory = allocateInventory(order).bind()
val paymentAuthorized = paymentAuthorization(order).bind()
if (!paymentAuthorized) {
throw DomainError.UnauthorizedPayment
}
val invoice = generateInvoice(order).bind()
val shipmentMethod = selectShipmentMethod(order, customer.region).bind()
val shipment = createShipment(order, shipmentMethod).bind()
val notification = sendNotification(shipment, customer).bind()
}
6. メイン関数で実行
メイン関数では、実際の注文データと顧客データを使って業務フローを実行します。
fun main() = runBlocking {
val customer = Customer("CUST123", "customer@example.com", "standard_area")
when (val result = orderWorkflow("ORDER1234", customer)) {
is Ok -> println("注文処理が完了しました。")
is Err -> when (result.error) {
DomainError.InventoryUnavailable -> println("在庫が不足しています。")
DomainError.PaymentFailed -> println("支払いに失敗しました。")
DomainError.ShipmentFailed -> println("出荷に失敗しました。")
DomainError.ExternalApiError -> println("外部システムとの連携に失敗しました。")
DomainError.InvalidShipmentMethod -> println("この地域には出荷できません。")
DomainError.UnauthorizedPayment -> println("支払い認証に失敗しました。")
is DomainError.UnknownError -> println("不明なエラーが発生しました: ${result.error.message}")
}
}
}
実行結果の例
正常な場合:
注文を受け付けました: ORDER1234
在庫を確認しています: ORDER1234
自社倉庫に在庫がありません。外部システムに問い合わせます...
外部在庫システムに問い合わせています: ORDER1234
支払い認証システムと連携しています: ORDER1234
請求書を生成しました: INV-ORDER1234
出荷方法を選択しています: ORDER1234
出荷情報を生成しています: SHIP-ORDER1234, method: Standard Shipping
顧客 customer@example.com に通知を送信しました: 注文 ORDER1234 が出荷されました。
注文処理が完了しました。
エラーが発生した場合(在庫不足):
注文を受け付けました: ORDER1234
在庫を確認しています: ORDER1234
自社倉
GADTs
sealed class Message<T>
data class TextMessage(val content: String) : Message<String>()
data class ImageMessage(val url: String, val size: Int) : Message<ByteArray>()
data class SystemMessage(val code: Int) : Message<Unit>()
fun <T> handleMessage(message: Message<T>): T {
return when (message) {
is TextMessage -> {
println("Text: ${message.content}")
message.content as T // 明示的キャストが必要
}
is ImageMessage -> {
println("Image URL: ${message.url}, Size: ${message.size}")
downloadImage(message.url) as T
}
is SystemMessage -> {
println("System Code: ${message.code}")
Unit as T
}
}
}
fun downloadImage(url: String): ByteArray {
println("Downloading image from $url...")
return ByteArray(10) { it.toByte() }
}
fun main() {
val textMessage = TextMessage("Hello, world!")
val textResult = handleMessage(textMessage)
val imageMessage = ImageMessage("https://example.com/image.png", 1024)
val imageResult = handleMessage(imageMessage)
val systemMessage = SystemMessage(404)
val systemResult = handleMessage(systemMessage)
}
import java.math.BigDecimal
sealed class CurrencyUnit()
data object USD : CurrencyUnit()
data object EUR : CurrencyUnit()
data object JPY : CurrencyUnit()
sealed class Precision(val scale: Int)
data object Decimal0 : Precision(0)
data object Decimal2 : Precision(2)
data object Decimal4 : Precision(4)
// Amountクラスに通貨と精度を保持
data class Amount<TCurrency : CurrencyUnit, TPrecision : Precision>(
val value: BigDecimal,
val currency: TCurrency,
val precision: TPrecision
)
// 使用例
val amount1 = Amount(BigDecimal("100.25"), USD, Decimal2)
fun identifyAmount(amount: Amount<*, *>) {
when (amount.currency) {
is USD -> println("Currency is USD")
is EUR -> println("Currency is EUR")
else -> println("Unknown currency")
}
when (amount.precision) {
is Decimal0 -> println("Precision is 0 decimal places")
is Decimal2 -> println("Precision is 2 decimal places")
is Decimal4 -> println("Precision is 4 decimal places")
else -> println("Unknown precision")
}
}
fun main() {
identifyAmount(amount1)
}
sealed class Nat
object Zero : Nat()
class Succ<N : Nat> : Nat()
// 長さがNであるようなリスト
data class SizedList<N : Nat, A>(val list: List<A>)
// 空のリストを作成
fun <A> emptySizedList(): SizedList<Zero, A> {
return SizedList(emptyList())
}
// 要素を追加する
fun <N : Nat, A> cons(head: A, tail: SizedList<N, A>): SizedList<Succ<N>, A> {
val newList = listOf(head) + tail.list
return SizedList(newList)
}
// リストの先頭要素を取得(長さが1以上であることを型で保証)
fun <N : Nat, A> head(list: SizedList<Succ<N>, A>): A {
return list.list.first()
}
fun main() {
val empty = emptySizedList<Int>() // SizedList<Zero, Int>
// val firstElement = head(empty) // コンパイルエラー(長さが0なので取得できない)
val list1 = cons(1, empty) // SizedList<Succ<Zero>, Int>
val list2 = cons(2, list1) // SizedList<Succ<Succ<Zero>>, Int>
val firstElement = head(list1) // OK
println("First element: $firstElement") // 出力: First element: 1
val secondElement = head(list2) // OK
println("Second element: $secondElement") // 出力: Second element: 2
val empty2 = emptySizedList<Int>() // SizedList<Zero, Int>
// コンパイルエラー: SizedList<Succ<N>, A> が必要
// val firstElement2 = head(empty2) // エラー!
}
面白いご質問ですね。業務系のアプリケーションでコモナドを応用できる例を考えてみましょう。技術的な関心事だけでなく、ビジネスオペレーションのドメインをコードに写像する際に、コモナドがどのように有用かを具体的に示します。
コモナドのビジネスアプリケーションへの応用
業務系アプリケーションでは、コンテキスト依存の計算や、部分(値)の変化が全体(文脈)に影響を与えるケースが多く存在します。コモナドはこのようなシナリオをモデル化するのに適しています。
例:サプライチェーンにおける在庫管理
サプライチェーンでは、各拠点(倉庫や店舗)の在庫レベルが、全体の在庫状況や需要予測に影響を与えます。また、全体の需要予測や供給状況も、各拠点の在庫管理に影響を与えます。
この相互作用をコモナドでモデル化することで、在庫管理や需要予測の精度を向上させることができます。
1. ビジネスドメインのモデル化
1.1 拠点(ロケーション)の定義
各拠点を表すデータクラスを定義します。
data class Location(
val name: String,
val inventory: Int,
val demand: Int
)
1.2 サプライチェーンのコンテキスト
全体のサプライチェーンを表すクラスを定義します。
data class SupplyChain(
val locations: List<Location>
)
2. コモナドの実装
SupplyChain
をコモナドとして扱い、各拠点の在庫管理が全体に影響を与えるモデルを構築します。
2.1 コモナドインターフェースの定義
interface Comonad<W, A> {
fun extract(): A
fun duplicate(): W
fun <B> extend(f: (W) -> B): W
}
2.2 SupplyChainのコモナド実装
data class SupplyChain(
val locations: List<Location>
) : Comonad<SupplyChain, List<Location>> {
override fun extract(): List<Location> = locations
override fun duplicate(): SupplyChain = this
override fun <B> extend(f: (SupplyChain) -> B): SupplyChain {
// 各拠点に対して計算を適用し、新しいSupplyChainを生成
val newLocations = locations.map { location ->
val updatedChain = this.copy(locations = listOf(location))
val newValue = f(updatedChain) as Location
newValue
}
return this.copy(locations = newLocations)
}
}
3. 部分から全体への影響をモデル化
各拠点の在庫レベルや需要が全体のサプライチェーンに影響を与えるケースを考えます。
3.1 在庫補充計画の策定
各拠点の在庫レベルと需要に基づいて、補充が必要かどうかを判断します。また、補充の必要性が全体の供給計画に影響を与えます。
fun replenishmentPlan(chain: SupplyChain): Location {
val location = chain.locations.first()
val restockThreshold = 50
val needsRestock = location.inventory < restockThreshold
// 全体の需要を考慮して、補充量を決定
val totalDemand = chain.locations.sumBy { it.demand }
val restockAmount = if (needsRestock) totalDemand / chain.locations.size else 0
val updatedInventory = location.inventory + restockAmount
return location.copy(inventory = updatedInventory)
}
3.2 extendを使用して全体を更新
extend
を用いて、各拠点に対して補充計画を適用します。
fun main() {
val locations = listOf(
Location("Warehouse A", inventory = 30, demand = 20),
Location("Warehouse B", inventory = 70, demand = 40),
Location("Store C", inventory = 20, demand = 30)
)
val supplyChain = SupplyChain(locations)
val updatedChain = supplyChain.extend(::replenishmentPlan)
// 結果を表示
updatedChain.locations.forEach { location ->
println("${location.name}: Inventory=${location.inventory}, Demand=${location.demand}")
}
}
実行結果:
Warehouse A: Inventory=53, Demand=20
Warehouse B: Inventory=93, Demand=40
Store C: Inventory=46, Demand=30
ここで、各拠点の在庫レベルが全体の需要に基づいて更新されています。各拠点の補充量が全体の供給計画に影響を与えており、部分から全体への影響が表現されています。
4. 全体から部分への影響をモデル化
今度は、全体のサプライチェーンの状況が各拠点の戦略に影響を与えるケースを考えます。
4.1 需要予測に基づく戦略変更
全体の需要や他の拠点の在庫状況に応じて、各拠点が価格戦略やプロモーションを変更することができます。
fun adjustPricingStrategy(chain: SupplyChain): Location {
val location = chain.locations.first()
// 全体の在庫レベルと需要を計算
val totalInventory = chain.locations.sumBy { it.inventory }
val totalDemand = chain.locations.sumBy { it.demand }
// 在庫過多の場合、価格を下げて需要を喚起
val pricingStrategy = if (totalInventory > totalDemand * 1.2) {
"Discount"
} else {
"Standard"
}
// 各拠点の需要に応じて戦略を適用
val adjustedDemand = if (pricingStrategy == "Discount") {
(location.demand * 1.1).toInt() // 需要が10%増加
} else {
location.demand
}
return location.copy(demand = adjustedDemand)
}
4.2 extendを使用して戦略を適用
fun main() {
val locations = listOf(
Location("Warehouse A", inventory = 100, demand = 80),
Location("Warehouse B", inventory = 120, demand = 90),
Location("Store C", inventory = 110, demand = 85)
)
val supplyChain = SupplyChain(locations)
val updatedChain = supplyChain.extend(::adjustPricingStrategy)
// 結果を表示
updatedChain.locations.forEach { location ->
println("${location.name}: Inventory=${location.inventory}, Demand=${location.demand}")
}
}
実行結果:
Warehouse A: Inventory=100, Demand=88
Warehouse B: Inventory=120, Demand=99
Store C: Inventory=110, Demand=93
全体の在庫レベルが高いため、価格戦略を「ディスカウント」に変更し、各拠点の需要が増加しています。全体から部分への影響が表現されています。
5. 部分と全体の双方向の相互作用
これらの例を組み合わせて、部分(各拠点)と全体(サプライチェーン)が双方向に影響を与え合うモデルを構築できます。
5.1 複合的な計算
fun updateSupplyChain(chain: SupplyChain): Location {
val location = chain.locations.first()
// 在庫補充計画の適用
val updatedLocation = replenishmentPlan(chain.copy(locations = listOf(location)))
// 戦略変更の適用
val finalLocation = adjustPricingStrategy(chain.copy(locations = listOf(updatedLocation)))
return finalLocation
}
5.2 extendを使用して全体を更新
fun main() {
val locations = listOf(
Location("Warehouse A", inventory = 30, demand = 80),
Location("Warehouse B", inventory = 70, demand = 90),
Location("Store C", inventory = 20, demand = 85)
)
val supplyChain = SupplyChain(locations)
val updatedChain = supplyChain.extend(::updateSupplyChain)
// 結果を表示
updatedChain.locations.forEach { location ->
println("${location.name}: Inventory=${location.inventory}, Demand=${location.demand}")
}
}
実行結果:
Warehouse A: Inventory=96, Demand=88
Warehouse B: Inventory=136, Demand=99
Store C: Inventory=86, Demand=93
この例では、各拠点の在庫レベルと需要が、全体のサプライチェーンの状況に応じて更新されています。また、各拠点の変化が全体の供給計画に影響を与えています。
6. コモナドの利点
-
コンテキスト依存の計算: 各拠点が全体のサプライチェーンの状況を考慮して計算を行うことができます。
-
双方向の影響: 部分から全体、全体から部分への影響を明示的にモデル化できます。
-
型安全性: Kotlinの型システムを活用し、計算の過程での誤りを防止できます。
7. まとめ
業務系アプリケーションでコモナドを活用することで、複雑な相互依存関係を持つビジネスロジックを明確にモデル化できます。
-
サプライチェーンの例では、在庫管理や需要予測、価格戦略などのビジネスオペレーションをコモナドで表現しました。
-
部分と全体の相互作用をコモナドの
extend
関数を通じて実装し、ビジネスロジックの複雑さをコードに反映しました。
8. 他のビジネスドメインへの応用
コモナドは他にも以下のような業務系アプリケーションで応用できます。
-
金融業務: 各取引のリスク評価がポートフォリオ全体のリスクに影響を与えるケース。
-
物流最適化: 配送ルートの最適化で、各配送先の変更が全体の配送計画に影響を与えるケース。
-
人的資源管理: 個々の従業員のスケジュール変更が、プロジェクト全体の進捗に影響を与えるケース。
9. 最後に
コモナドを業務系アプリケーションで活用することで、ビジネスオペレーションにおける複雑な相互依存関係をコード上で明確に表現できます。
Kotlinを用いてコモナドを実装する際には、以下の点に注意すると良いでしょう。
-
高カインド型の制約: Kotlinでは高カインド型を直接サポートしていないため、具体的な型で実装する工夫が必要です。
-
ビジネスロジックの明確化: 業務ドメインの知識をコードに反映させるために、モデル化の段階でビジネスロジックを明確に定義することが重要です。
-
関数型プログラミングの活用: コモナドは関数型プログラミングの概念であるため、Kotlinの関数型機能を積極的に活用すると、より自然に実装できます。
これにより、業務系アプリケーションにおいても、コモナドの利点を活かした高度なモデル化が可能になります。