Skip to content

Commit 1114f83

Browse files
committed
Fixed do notation label hack;
Updated README.md; Updated to 1.1.0-beta-22.
1 parent 0e074e5 commit 1114f83

File tree

8 files changed

+119
-133
lines changed

8 files changed

+119
-133
lines changed

README.md

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# kotlin-monads
22

3-
[![](https://jitpack.io/v/h0tk3y/kotlin-monads.svg)](https://jitpack.io/#h0tk3y/kotlin-monads) [![](https://img.shields.io/badge/kotlin-1.1--M04-blue.svg)](http://kotlinlang.org/)
3+
[![](https://jitpack.io/v/h0tk3y/kotlin-monads.svg)](https://jitpack.io/#h0tk3y/kotlin-monads) [![](https://img.shields.io/badge/kotlin-1.1--beta--22-blue.svg)](http://kotlinlang.org/)
44

5-
An attempt to implement monads in Kotlin.
5+
An attempt to implement monads in Kotlin, deeply inspired by Haskell monads, but restricted within the Kotlin type system.
66

7-
_Note: this project uses Kotlin 1.1 EAP build. Use the 1.1 EAP IDE plugin to work with it._
7+
_Note: this project uses Kotlin 1.1 Beta build. Use the 1.1 Beta IDE plugin to work with it._
88

99
## The monad type
1010

1111
Monadic types are represented by the `Monad<M, T>` interface,
12-
where `M` **should be the type of the implementation** with its `T` star-projected. Examples: `Maybe<T> : Monad<Maybe<*>, T>`, `State<S, T> : Monad<State<S, *>, T>`.
12+
where `M` **should be the type of the implementation** with only its `T` star-projected. Examples: `Maybe<T> : Monad<Maybe<*>, T>`, `State<S, T> : Monad<State<S, *>, T>`.
1313

14-
The purpose is: with `Monad` defined in this way, we
15-
are almost able to say that a function returns the same `Monad` implementation but with a different type parameter:
14+
With `Monad` defined in this way, we
15+
are almost able to say in terms of the Kotlin type system that a function returns the same `Monad` implementation but
16+
with a different type argument `R` instead of `T`:
1617

1718
fun <T, R, M : Monad<M, *>> Monad<M, T>.map(f: (T) -> R) = bind { returns(f(it)) }
1819

@@ -49,13 +50,20 @@ See the usage examples in [tests](https://github.com/h0tk3y/kotlin-monads/tree/m
4950

5051
The monad implementation should only provide one function `bind` (Haskell: `>>=`),
5152
no separate `return` is there, instead, if you look at the signature of `bind`, you'll see that the function to bind with is `f: Return<This>.(T) -> Monad<This, R>`.
52-
53-
It means that a monad implementation should provide the `Return<M>` as well and pass it to `f` each time, so that inside `f` its `returns` could be used:
53+
It means that a `Monad<M, T>` implementation should provide the `Return<M>` as well and pass it to `f` each time, so that inside `f` its `returns` could be used:
5454

5555
just(3) bind { returns(it * it) }
5656

57-
I found no direct equivalent to `return` in Haskell, which could be used even outside bind functions. Outside the `bind` blocks, you should either
58-
wrap the values into your monads manually or require a `Return<M>`, which can wrap `T` into `Monad<M, T>` for you.
57+
There seems to be no direct equivalent to Haskell `return`, which could be used outside any context like `bind` blocks. Outside the `bind` blocks, you should either
58+
wrap the values into your monads manually or require a `Return<M>`, which can wrap `T` into `Monad<M, T>` for you.
59+
60+
Mind the [monad laws](https://wiki.haskell.org/Monad_laws). A correct monad implementation follows these three rules (rephrased in terms of `kotlin-monads`):
61+
62+
1. **Left identity**: `returns(x) bind { f(it) }` should be equivalent to `f(x)`
63+
64+
2. **Right identity**: `m bind { returns(it) }` should be equivalent to `m`
65+
66+
3. **Associativity**: `m bind { f(it) } bind { g(it) }` should be equivalent to `m bind { f(it) bind { g(it) } }`
5967

6068
Also, it's good to make the return type of `bind` narrower, e.g. `bind` of `Maybe<T>` would rather return `Maybe<R>` than `Monad<Maybe<*>, R>`, it allows not to cast
6169
the result of a `bind` called on a known monad type.
@@ -89,21 +97,46 @@ Example implementation:
8997

9098
## Do notation
9199

92-
With the power of Kotlin coroutines, we can have a limited variant of do notation:
100+
With the power of Kotlin coroutines, we can even have an equivalent of the [*Haskell do notation*](https://en.wikibooks.org/wiki/Haskell/do_notation):
101+
102+
Simple example that performs a monadic list nondeterministic expansion:
93103

94-
val m = doWith(monadListOf(0)) {
104+
val m = doReturning(MonadListReturn) {
95105
val x = bind(monadListOf(1, 2, 3))
96-
val y = bind(monadListOf(x, x))
97-
then(monadListOf(y, y + 1))
98-
} as MonadList
106+
val y = bind(monadListOf(x, x + 1))
107+
monadListOf(y, x * y)
108+
}
99109

100110
assertEquals(monadListOf(1, 2, 1, 2, 2, 3, 2, 3, 3, 4, 3, 4), m)
101111

102-
The limitation is that the intermediate results in a single _do_ block are restricted to the same value type `T`. You can, however, use nested _do_ blocks to use different result types.
112+
Or applied to an existing monad for convenience:
113+
114+
val m = monadListOf(1, 2, 3).bindDo { x ->
115+
val y = bind(monadListOf(x, x + 1))
116+
monadListOf(y, x * y)
117+
}
118+
119+
This is effectively equivalent to the following code written with only simple `bind`:
120+
121+
val m = monadListOf(1, 2, 3).bind { x ->
122+
monadListOf(x, x + 1).bind { y ->
123+
monadList(y, x * y)
124+
}
125+
}
126+
127+
Note that, with simple `bind`, each *transformation* requires another inner scope if it uses the variables bound outside,
128+
which would lead to some kind of callback hell.
129+
This problem is effectively solved using the Kotlin coroutines: the compiler performs the CPS transformation of a plain
130+
code block under the hood. However, this coroutines use case is somewhat out of conventions: it might resume the same continuation
131+
several times and uses quite a dirty hack to do that.
132+
133+
The result type parameter (`R` in `Monad<M, R`) is usually inferred, and the compiler controls the flow inside a *do block*, but still you need to
134+
downcast the `Monad<M, R>` to your actual monad type (e.g. `Monad<Maybe<*>, R>` to `Maybe<R>`), because the type system doesn't seem to allow this to be done
135+
automatically (if you know a way, please tell me).
103136

104-
**Be careful with mutable state** in _do_ blocks, since all continuation calls will share it, resulting into something unexpected:
137+
**Be careful with mutable state** in _do_ blocks, since all continuation calls will share it, sometimes resulting into counter-intuitive results:
105138

106-
val m = doWith(monadListOf(0)) {
139+
val m = doReturning(MonadListReturn) {
107140
for (i in 1..10)
108141
bind(monadListOf(0, 0))
109142
} as MonadList

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
buildscript {
2-
ext.kotlin_version = '1.1.0-beta-17'
2+
ext.kotlin_version = '1.1.0-beta-22'
33

44
repositories {
55
mavenCentral()
@@ -20,7 +20,7 @@ repositories {
2020
dependencies {
2121
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
2222
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
23-
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.3-beta'
23+
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.4-beta'
2424
testCompile "junit:junit:4.12"
2525
}
2626

gradlew

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ function splitJvmOpts() {
161161
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162162
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163163

164-
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
164+
# by default we should be in the correct project dir, but when runCont from Finder on Mac, the cwd is wrong
165165
if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
166166
cd "$(dirname "$0")"
167167
fi

src/main/kotlin/com/github/h0tk3y/kotlinMonads/DoNotation.kt

Lines changed: 37 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,99 +2,66 @@
22

33
package com.github.h0tk3y.kotlinMonads
44

5-
import kotlinx.coroutines.experimental.Here
65
import java.io.Serializable
7-
import java.util.*
86
import kotlin.coroutines.Continuation
9-
import kotlin.coroutines.CoroutineContext
7+
import kotlin.coroutines.EmptyCoroutineContext
108
import kotlin.coroutines.RestrictsSuspension
119
import kotlin.coroutines.intrinsics.SUSPENDED_MARKER
1210
import kotlin.coroutines.intrinsics.suspendCoroutineOrReturn
1311
import kotlin.coroutines.startCoroutine
1412

15-
fun <M : Monad<M, *>, T> doWith(m: Monad<M, T>,
16-
c: suspend DoController<M, T>.(T) -> Unit): Monad<M, T> =
17-
m.bind { t -> doWith(this, t, c) }
13+
fun <M : Monad<M, *>, T, R> Monad<M, T>.bindDo(c: suspend DoController<M>.(T) -> Monad<M, R>): Monad<M, R> =
14+
bind { t ->
15+
val controller = DoController(this)
16+
val f: suspend DoController<M>.() -> Monad<M, R> = { c(t) }
17+
f.startCoroutine(controller, controller)
18+
controller.returnedMonad as Monad<M, R>
19+
}
1820

19-
fun <M : Monad<M, *>, T> doWith(aReturn: Return<M>,
20-
defaultValue: T,
21-
c: suspend DoController<M, T>.(T) -> Unit): Monad<M, T> {
22-
val controller = DoController(aReturn, defaultValue)
23-
val f: suspend DoController<M, T>.() -> Unit = { c(defaultValue) }
24-
f.startCoroutine(controller, object : Continuation<Unit> {
25-
override fun resumeWithException(exception: Throwable) {}
26-
override fun resume(value: Unit) {}
27-
override val context: CoroutineContext = Here
28-
})
29-
return controller.lastResult
21+
fun <M : Monad<M, *>, R> doReturning(aReturn: Return<M>,
22+
c: suspend DoController<M>.() -> Monad<M, R>): Monad<M, R> {
23+
val controller = DoController(aReturn)
24+
val f: suspend DoController<M>.() -> Monad<M, R> = { c() }
25+
f.startCoroutine(controller, controller)
26+
return controller.returnedMonad as Monad<M, R>
3027
}
3128

3229
private val labelField by lazy {
3330
val jClass = Class.forName("kotlin.jvm.internal.CoroutineImpl")
3431
return@lazy jClass.getDeclaredField("label").apply { isAccessible = true }
3532
}
3633

37-
private val innerContinuationField by lazy {
38-
val jClass = Class.forName("kotlinx.coroutines.experimental.DispatchedContinuation")
39-
return@lazy jClass.getDeclaredField("continuation").apply { isAccessible = true }
40-
}
41-
4234
private var <T> Continuation<T>.label
43-
get() = labelField.get(innerContinuationField.get(this))
44-
set(value) = labelField.set(innerContinuationField.get(this@label), value)
45-
46-
private fun <T, R> backupLabel(c: Continuation<T>, block: Continuation<T>.() -> R): R {
47-
val backupLabel = c.label
48-
val r = block(c)
49-
c.label = backupLabel
50-
return r
51-
}
35+
get() = labelField.get(this)
36+
set(value) = labelField.set(this@label, value)
5237

5338
@RestrictsSuspension
54-
class DoController<M : Monad<M, *>, T>(val returning: Return<M>,
55-
val value: T) : Serializable, Return<M> by returning {
56-
var lastResult: Monad<M, T> = returning.returns(value)
57-
internal set
39+
class DoController<M : Monad<M, *>>(private val returning: Return<M>) :
40+
Serializable, Return<M> by returning, Continuation<Monad<M, *>> {
5841

59-
private val stackSignals = Stack<Boolean>().apply { push(false) }
42+
override val context = EmptyCoroutineContext
6043

61-
suspend fun bind(m: Monad<M, T>): T = suspendCoroutineOrReturn { c ->
62-
stackSignals.pop()
63-
stackSignals.push(true)
64-
var anyCont = false
65-
val o = m.bind { x ->
66-
stackSignals.push(false)
67-
backupLabel(c) {
68-
c.resume(x)
69-
}
70-
val contHasMonad = stackSignals.pop()
71-
if (contHasMonad) {
72-
anyCont = true
73-
lastResult
74-
} else {
75-
returns(x)
76-
}
77-
}
78-
lastResult = if (anyCont) o else m
79-
SUSPENDED_MARKER
44+
override fun resume(value: Monad<M, *>) {
45+
returnedMonad = value //here is where the biggest magic happens
46+
}
47+
48+
override fun resumeWithException(exception: Throwable) {
49+
throw exception
8050
}
8151

82-
suspend fun then(m: Monad<M, T>) = suspendCoroutineOrReturn<Unit> { c ->
83-
stackSignals.pop()
84-
stackSignals.push(true)
85-
var anyCont = false
86-
val o = m.bind { x ->
87-
stackSignals.push(false)
88-
backupLabel(c) { c.resume(Unit) }
89-
val contHasMonad = stackSignals.pop()
90-
if (contHasMonad) {
91-
anyCont = true
92-
lastResult
93-
} else {
94-
returning.returns(x)
95-
}
52+
internal lateinit var returnedMonad: Monad<M, *>
53+
54+
suspend fun <T> bind(m: Monad<M, T>): T = suspendCoroutineOrReturn { c ->
55+
val labelHere = c.label // save the label
56+
returnedMonad = m.bind { x ->
57+
c.label = labelHere
58+
c.resume(x)
59+
returnedMonad
9660
}
97-
lastResult = if (anyCont) o else m
9861
SUSPENDED_MARKER
9962
}
63+
64+
suspend fun <T> then(m: Monad<M, T>): Unit {
65+
bind(m)
66+
}
10067
}
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
package com.github.h0tk3y.kotlinMonads
22

33
sealed class Maybe<out T> : Monad<Maybe<*>, T> {
4-
data class Just<T>(val value: T) : Maybe<T>() {
5-
override fun toString() = "Just $value"
6-
}
7-
84
override fun <R> bind(f: Binder<Maybe<*>, T, R>): Maybe<R> = when (this) {
95
is Just -> f(MaybeReturn, value) as Maybe
106
is None -> none()
117
}
8+
}
129

13-
object None : Maybe<Nothing>() {
14-
override fun toString() = "None"
15-
}
10+
data class Just<T>(val value: T) : Maybe<T>() {
11+
override fun toString() = "Just $value"
12+
}
13+
14+
object None : Maybe<Nothing>() {
15+
override fun toString() = "None"
1616
}
1717

1818
object MaybeReturn : Return<Maybe<*>> {
19-
override fun <T> returns(t: T) = just(t)
19+
override fun <T> returns(t: T) = Just(t)
2020
}
2121

2222
@Suppress("UNCHECKED_CAST")
23-
fun <T> none(): Maybe<T> = Maybe.None
23+
fun <T> none(): Maybe<T> = None
2424

25-
fun <T> just(t: T) = Maybe.Just(t)
25+
fun <T> just(t: T) = Just(t)

src/test/kotlin/com/github/h0tk3y/kotlinMonads/DoNotationTest.kt

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,62 +8,48 @@ import org.junit.Test
88

99
class DoNotationTest {
1010
@Test fun testLinearDo() {
11-
val m = doWith(just(1)) {
12-
val j = bind(returns(value * 2))
11+
val m = just(1).bindDo { x ->
12+
val j = bind(returns(x * 2))
1313
val k = bind(returns(j * 3))
14-
then(returns(k + 1))
14+
returns(k + 1)
1515
}
1616
assertEquals(just(7), m)
1717
}
1818

1919
@Test fun testControlFlow() {
2020
var called = false
21-
val m = doWith(just(1)) {
22-
val j = bind(returns(value * 2))
21+
val m = just(1).bindDo { x ->
22+
val j = bind(returns(x * 2))
2323
val k = bind(if (j % 2 == 0) none() else just(j))
2424
called = true
25-
then(returns(k))
25+
returns(k)
2626
}
27-
assertEquals(Maybe.None, m)
27+
assertEquals(None, m)
2828
assertFalse(called)
2929
}
3030

3131
@Test fun testMultipleBranches() {
3232
var iIterations = 0
3333
var jIterations = 0
34-
var kIterations = 0
35-
val m = doWith(monadListOf(0)) {
34+
val m = doReturning(MonadListReturn) {
3635
val i = bind(monadListOf(1, 2, 3))
3736
++iIterations
3837
val j = bind(monadListOf(i, i))
3938
++jIterations
40-
then(monadListOf(j, j + 1))
41-
++kIterations
39+
monadListOf(j, j + 1)
4240
}
4341
assertEquals(monadListOf(1, 2, 1, 2, 2, 3, 2, 3, 3, 4, 3, 4), m)
4442
assertEquals(3, iIterations)
4543
assertEquals(6, jIterations)
46-
assertEquals(12, kIterations)
4744
}
4845

4946
@Test fun testMultipleApplications() {
50-
val m = doWith(monadListOf(1)) {
51-
then(monadListOf(1, 1))
47+
val m = doReturning(MonadListReturn) {
5248
then(monadListOf(1, 1))
5349
then(monadListOf(1, 1))
5450
then(monadListOf(1, 1))
51+
monadListOf(1, 1)
5552
} as MonadList
5653
assertEquals(16, m.size)
5754
}
58-
59-
@Test fun testBindLastStatement() {
60-
val results = mutableListOf<Int>()
61-
val m = doWith(monadListOf(2)) {
62-
val x = bind(monadListOf(value + 1, value * value))
63-
val z = bind(returns(x))
64-
results.add(z)
65-
}
66-
assertEquals(listOf(3, 4), results)
67-
assertEquals(results, m)
68-
}
6955
}

0 commit comments

Comments
 (0)