Supanat Potiwarakorn (aka. I'Boss)
Software Choreographer / Consultant @ Thoughtworks
it's designed to be connected
composed component can still be to composed further
(no adapter needed)
The pieces are reusable in many context
the act of putting together pieces is associative
Douglas McIlroy
The Original Creator of Unix Pipeline
( ... 2 more )
> ls -a | egrep "\.md$" | wc -lthe programs are designed to be connected
piped program can still be piped furthur
they are reusable in many context
the act of putting together pieces is associative
> lsmd () { ls -a | egrep "\.md$" }
> countmd() { egrep "\.md$" | wc -l }
> # =================================
> lsmd | wc -l
> # is equal to
> ls -a | countmddata
operation
Object
I just want the data!
I just want the operation!
data
operation
data class
data structure
function
Sum type
Product Type
Product type
Pair(1, "K") // Building block!
Pair(1, Pair("K", 3))
// is equal up to isomorphism to
Pair(Pair(1, "K"), 3)
// is equal up to isomorphism to
Triple(1, "K", 3)
// Product type compose! And we can build product type with any length
data class Event(
name: String,
owner: User,
mods: List<User>,
tickets: List<Ticket>
)Sum type
sealed class UserEvent {
object Shake: UserEvent()
data class Touch(val x: Double, val y: Double): UserEvent()
data class Type(val char: Char): UserEvent()
}
sealed class Tree<T> {
data class Leaf<T>(val value: T) : Tree<T>()
data class Node<T>(val left: Tree<T>, val right: Tree<T>) : Tree<T>()
}Now we have data with composable data types!
separate data from operation
=
separation of concern
now we can start designing by starting with
"Ignoring about how are they going to behave.
How should things that is going to live in our system look like?"
Going further
"Making illegal state irrepresentable"
sealed class Switch(open val resistance: Double) {
data class On(val current: Double, override val resistance: Double): Switch(resistance)
data class Off(override val resistance: Double): Switch(resistance)
}
fun measureCurrent(switch: Switch) = when (switch) {
is Switch.On -> switch.current
is Switch.Off -> 0.0
}
var allEven = true
fun isAllEven(list: List<Int>): Unit {
list.map {
if (it % 2 != 0) {
allEven = false
}
}
}fun sumUntilEq(target: Int, numbers: List<Int>): Int {
var sum = 0
numbers.forEach {
if (it == target) return sum else sum += it
}
return sum
}It's time to compose!
// compose with `*`
// (f * g)(x) = f(g(x))
operator fun <A, B, C> ((B) -> C).times(f: (A) -> B): (A) -> C = { this(f(it)) }
val add1 = { n: Int -> n + 1 }
val times2 = { n: Int -> n * 2 }
val f = times2 * add1
f(8) // 18fun measureVoltage(switch: Switch) =
measureCurrent(switch) * switch.resistance
fun addResistance(newResistance: Double) = { switch: Switch ->
when (switch) {
is Switch.On -> switch.copy(resistance = switch.resistance + newResistance)
is Switch.Off -> switch.copy(resistance = switch.resistance + newResistance)
}
}measureVoltageOfAdded10Ohm(Switch.On(10.0, 25.0))
// V = I * R = 10 * (25 + 5 + 5) = 350
measureVoltageOfAdded10Ohm(Switch.Off(10.0))
// 0.0val measureVoltageOfAdded10Ohm: (Switch) -> Double =
::measureVoltage * addResistance(5.0) * addResistance(5.0)
`let`'s fix it!
and now we have something like UNIX pipe!
Switch.On(10.0, 25.0)
.let(addResistance(5.0))
.let(addResistance(5.0))
.let(::measureVoltage)
// 350.0Functional core, imperative shell
Composable Effects!
Arrow-kt
fun switchOn(switch: Switch, inCurrent: Double) = when (switch) {
is Switch.On -> throw BlowUpException()
is Switch.Off -> Switch.On(inCurrent, switch.resistance)
}
fun trySwitchOn(switch: Switch, inCurrent: Double) = Try {
when (switch) {
is Switch.On -> throw BlowUpException()
is Switch.Off -> Switch.On(inCurrent, switch.resistance)
}
}
Switch.On(10.0, 25.0)
.let(addResistance(5.0))
.let(addResistance(5.0))
.let(::measureVoltage)
// 350.0
trySwitchOn(Switch.Off(25.0), 10.0)
.map(addResistance(5.0))
.map(addResistance(5.0))
.map(::measureVoltage)
.fold(
ifFailure = { 0.0 },
ifSuccess = { it }
)
// 350.0
trySwitchOn(Switch.On(10.0, 25.0), 10.0)
.map(addResistance(5.0))
.map(addResistance(5.0))
.map(::measureVoltage)
.fold(
ifFailure = { 0.0 },
ifSuccess = { it }
)
// 0.0