Skip to content

Commit 8b6242d

Browse files
pakoitoffgiraldez
authored andcommitted
Add EitherT implementation (#74)
* Add initial implementation of EitherT. Fix function parameters in OptionT * Fix another inline function * Add tailRecM to EitherTMonad * Add first batch of tests *BROKEN* * Fix most tests for EitherT. Cartesian tests are *BROKEN* * Fix StackOverflow on cartesian by using EitherT map instance function * Fix indentation * Revert generics temporarily * Fix inference for EitherT constructors. Add remaining tests * Add tests for tailCallM * Fix non-descriptive naming in tailRecM * Rename EitherT#impure() to left(). Add right(). Express pure() in terms of right(). * Fix formatting * Fix tailRecM to use when instead of fold to reduce stack use * Fix non-descriptive naming * Fix documentation
1 parent 0b50070 commit 8b6242d

File tree

6 files changed

+315
-12
lines changed

6 files changed

+315
-12
lines changed
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package katz
2+
3+
typealias EitherTKind<F, A, B> = HK3<EitherT.F, F, A, B>
4+
typealias EitherTF<F, L> = HK2<EitherT.F, F, L>
5+
6+
/**
7+
* [EitherT]`<F, A, B>` is a light wrapper on an `F<`[Either]`<A, B>>` with some
8+
* convenient methods for working with this nested structure.
9+
*
10+
* It may also be said that [EitherT] is a monad transformer for [Either].
11+
*/
12+
data class EitherT<F, A, B>(val MF: Monad<F>, val value: HK<F, Either<A, B>>) : EitherTKind<F, A, B> {
13+
14+
class F private constructor()
15+
16+
companion object {
17+
18+
inline operator fun <reified F, A, B> invoke(value: HK<F, Either<A, B>>, MF: Monad<F> = monad<F>()): EitherT<F, A, B> = EitherT(MF, value)
19+
20+
inline fun <reified F, A, B> pure(b: B, MF: Monad<F> = monad<F>()): EitherT<F, A, B> = right(b, MF)
21+
22+
inline fun <reified F, A, B> right(b: B, MF: Monad<F> = monad<F>()): EitherT<F, A, B> = EitherT(MF, MF.pure(Either.Right(b)))
23+
24+
inline fun <reified F, A, B> left(a: A, MF: Monad<F> = monad<F>()): EitherT<F, A, B> = EitherT(MF, MF.pure(Either.Left(a)))
25+
26+
inline fun <reified F, A, B> fromEither(value: Either<A, B>, MF: Monad<F> = monad<F>()): EitherT<F, A, B> = EitherT(MF, MF.pure(value))
27+
}
28+
29+
inline fun <C> fold(crossinline l: (A) -> C, crossinline r: (B) -> C): HK<F, C> =
30+
MF.map(value, { either -> either.fold(l, r) })
31+
32+
inline fun <C> flatMap(crossinline f: (B) -> EitherT<F, A, C>): EitherT<F, A, C> =
33+
flatMapF({ it -> f(it).value })
34+
35+
inline fun <C> flatMapF(crossinline f: (B) -> HK<F, Either<A, C>>): EitherT<F, A, C> =
36+
EitherT(MF, MF.flatMap(value, { either -> either.fold({ MF.pure(Either.Left(it)) }, { f(it) }) }))
37+
38+
inline fun <C> cata(crossinline l: (A) -> C, crossinline r: (B) -> C): HK<F, C> =
39+
fold(l, r)
40+
41+
fun <C> liftF(fa: HK<F, C>): EitherT<F, A, C> =
42+
EitherT(MF, MF.map(fa, { Either.Right(it) }))
43+
44+
inline fun <C> semiflatMap(crossinline f: (B) -> HK<F, C>): EitherT<F, A, C> =
45+
flatMap({ liftF(f(it)) })
46+
47+
inline fun <C> map(crossinline f: (B) -> C): EitherT<F, A, C> =
48+
EitherT(MF, MF.map(value, { it.map(f) }))
49+
50+
inline fun exists(crossinline p: (B) -> Boolean): HK<F, Boolean> =
51+
MF.map(value, { it.exists(p) })
52+
53+
inline fun <C, D> transform(crossinline f: (Either<A, B>) -> Either<C, D>): EitherT<F, C, D> =
54+
EitherT(MF, MF.map(value, { f(it) }))
55+
56+
inline fun <C> subflatMap(crossinline f: (B) -> Either<A, C>): EitherT<F, A, C> =
57+
transform({ it.flatMap(f) })
58+
59+
fun toOptionT(): OptionT<F, B> =
60+
OptionT(MF, MF.map(value, { it.toOption() }))
61+
62+
}

katz/src/main/kotlin/katz/data/OptionT.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ data class OptionT<F, A>(val MF: Monad<F>, val value: HK<F, Option<A>>) : Option
2525
}
2626

2727
inline fun <B> fold(crossinline default: () -> B, crossinline f: (A) -> B): HK<F, B> =
28-
MF.map(value, { option -> option.fold({ default() }, { f(it) }) })
28+
MF.map(value, { option -> option.fold(default, f) })
2929

3030
inline fun <B> cata(crossinline default: () -> B, crossinline f: (A) -> B): HK<F, B> =
31-
fold({ default() }, { f(it) })
31+
fold(default, f)
3232

3333
inline fun <B> flatMap(crossinline f: (A) -> OptionT<F, B>): OptionT<F, B> = flatMapF({ it -> f(it).value })
3434

3535
inline fun <B> flatMapF(crossinline f: (A) -> HK<F, Option<B>>): OptionT<F, B> =
36-
OptionT(MF, MF.flatMap(value, { option -> option.fold({ MF.pure(Option.None) }, { f(it) }) }))
36+
OptionT(MF, MF.flatMap(value, { option -> option.fold({ MF.pure(Option.None) }, f) }))
3737

3838
fun <B> liftF(fa: HK<F, B>): OptionT<F, B> = OptionT(MF, MF.map(fa, { Option.Some(it) }))
3939

katz/src/main/kotlin/katz/instances/EitherMonad.kt

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ class EitherMonad<L> : Monad<EitherF<L>> {
44

55
override fun <A> pure(a: A): Either<L, A> = Either.Right(a)
66

7-
override fun <A, B> flatMap(fa: EitherKind<L, A>, f: (A) -> EitherKind<L, B>): Either<L, B> {
8-
return fa.ev().flatMap { f(it).ev() }
9-
}
7+
override fun <A, B> flatMap(fa: EitherKind<L, A>, f: (A) -> EitherKind<L, B>): Either<L, B> =
8+
fa.ev().flatMap { f(it).ev() }
109

1110
tailrec override fun <A, B> tailRecM(a: A, f: (A) -> HK<EitherF<L>, Either<A, B>>): Either<L, B> {
1211
val e = f(a).ev().ev()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package katz
2+
3+
class EitherTMonad<F, L>(val MF : Monad<F>) : Monad<EitherTF<F, L>> {
4+
override fun <A> pure(a: A): EitherT<F, L, A> =
5+
EitherT(MF, MF.pure(Either.Right(a)))
6+
7+
override fun <A, B> map(fa: EitherTKind<F, L, A>, f: (A) -> B): EitherT<F, L, B> =
8+
fa.ev().map { f(it) }
9+
10+
override fun <A, B> flatMap(fa: EitherTKind<F, L, A>, f: (A) -> EitherTKind<F, L, B>): EitherT<F, L, B> =
11+
fa.ev().flatMap { f(it).ev() }
12+
13+
override fun <A, B> tailRecM(a: A, f: (A) -> HK<EitherTF<F, L>, Either<A, B>>): EitherT<F, L, B> =
14+
EitherT(MF, MF.tailRecM(a, {
15+
MF.map(f(it).ev().value) { recursionControl ->
16+
when (recursionControl) {
17+
is Either.Left<L> -> Either.Right(Either.Left(recursionControl.a))
18+
is Either.Right<Either<A, B>> ->
19+
when (recursionControl.b) {
20+
is Either.Left<A> -> Either.Left(recursionControl.b.a)
21+
is Either.Right<B> -> Either.Right(Either.Right(recursionControl.b.b))
22+
}
23+
}
24+
}
25+
}))
26+
}
27+
28+
fun <F, A, B> EitherTKind<F, A, B>.ev(): EitherT<F, A, B> = this as EitherT<F, A, B>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package katz
2+
3+
import io.kotlintest.KTestJUnitRunner
4+
import io.kotlintest.matchers.shouldBe
5+
import io.kotlintest.properties.Gen
6+
import io.kotlintest.properties.forAll
7+
import org.junit.runner.RunWith
8+
9+
@RunWith(KTestJUnitRunner::class)
10+
class EitherTTest : UnitSpec() {
11+
init {
12+
"map should modify value" {
13+
forAll { a: String ->
14+
val right = EitherT(Id(Either.Right(a)))
15+
val mapped = right.map({ "$it power" })
16+
val expected = EitherT(Id(Either.Right("$a power")))
17+
18+
mapped == expected
19+
}
20+
}
21+
22+
"flatMap should modify entity" {
23+
forAll { a: String ->
24+
val right = EitherT(NonEmptyList.of(Either.Right(a)))
25+
val mapped = right.flatMap { EitherT(NonEmptyList.of(Either.Right(3))) }
26+
val expected = EitherT.pure<NonEmptyList.F, Int, Int>(3)
27+
28+
mapped == expected
29+
}
30+
31+
forAll { ignored: String ->
32+
val right: EitherT<NonEmptyList.F, Int, String> = EitherT(NonEmptyList.of(Either.Right(ignored)))
33+
val mapped = right.flatMap { EitherT(NonEmptyList.of(Either.Left(3))) }
34+
val expected = EitherT.left<NonEmptyList.F, Int, Int>(3)
35+
36+
mapped == expected
37+
}
38+
39+
forAll { _: String ->
40+
val right = EitherT.left<NonEmptyList.F, Int, Int>(3)
41+
val mapped = right.flatMap { EitherT(NonEmptyList.of<Either<Int, Int>>(Either.Right(2))) }
42+
val expected = EitherT(NonEmptyList.of(Either.Left(3)))
43+
44+
mapped == expected
45+
}
46+
}
47+
48+
"cata should modify the return" {
49+
forAll { num: Int ->
50+
val right = EitherT.pure<NonEmptyList.F, Int, Int>(num)
51+
val expected = NonEmptyList.of(true)
52+
val result = right.cata({ false }, { true })
53+
54+
expected == result
55+
}
56+
57+
forAll { num: Int ->
58+
val right = EitherT.left<NonEmptyList.F, Int, Int>(num)
59+
val expected = NonEmptyList.of(true)
60+
val result = right.cata({ true }, { false })
61+
62+
expected == result
63+
}
64+
}
65+
66+
"semiFlatMap should map the right side of the inner either" {
67+
forAll { num: Int ->
68+
val right: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Right(num)))
69+
val calculated = right.semiflatMap { NonEmptyList.of(it > 0) }
70+
val expected = EitherT(NonEmptyList.of(Either.Right(num > 0)))
71+
72+
calculated == expected
73+
}
74+
75+
forAll { num: Int ->
76+
val left: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Left(num)))
77+
val calculated = left.semiflatMap { NonEmptyList.of(it > 0) }
78+
val expected = EitherT(NonEmptyList.of(Either.Left(num)))
79+
80+
calculated == expected
81+
}
82+
}
83+
84+
85+
"subFlatMap should map the right side of the Either wrapped by EitherT" {
86+
forAll { num: Int ->
87+
val right: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Right(num)))
88+
val calculated = right.subflatMap { Either.Right(it > 0) }
89+
val expected = EitherT(NonEmptyList.of(Either.Right(num > 0)))
90+
91+
calculated == expected
92+
}
93+
94+
forAll { num: Int ->
95+
val left: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Right(num)))
96+
val calculated = left.subflatMap { Either.Left(num) }
97+
val expected = EitherT(NonEmptyList.of(Either.Left(num)))
98+
99+
calculated == expected
100+
}
101+
102+
forAll { num: Int ->
103+
val left: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Left(num)))
104+
val calculated = left.subflatMap { Either.Right(num > 0) }
105+
val expected = EitherT(NonEmptyList.of(Either.Left(num)))
106+
107+
calculated == expected
108+
}
109+
110+
forAll { num: Int ->
111+
val left: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Left(num)))
112+
val calculated = left.subflatMap { Either.Left(num + 1) }
113+
val expected = EitherT(NonEmptyList.of(Either.Left(num)))
114+
115+
calculated == expected
116+
}
117+
}
118+
119+
"exists evaluates a predicate on the right side and lifts it to the wrapped monad" {
120+
forAll { num: Int ->
121+
val right: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Right(num)))
122+
val calculated = right.exists { it > 0 }.ev()
123+
val expected = NonEmptyList.of(num > 0)
124+
125+
calculated == expected
126+
}
127+
128+
forAll { num: Int ->
129+
val left: EitherT<NonEmptyList.F, Int, Int> = EitherT(NonEmptyList.of(Either.Left(num)))
130+
val calculated = left.exists { true }.ev()
131+
val expected = NonEmptyList.of(false)
132+
133+
calculated == expected
134+
}
135+
}
136+
137+
"to OptionT should transform to a correct OptionT" {
138+
forAll { a: String ->
139+
val right: EitherT<NonEmptyList.F, String, String> = EitherT(NonEmptyList.of(Either.Right(a)))
140+
val expected = OptionT.pure<NonEmptyList.F, String>(a)
141+
val calculated = right.toOptionT()
142+
143+
expected == calculated
144+
}
145+
146+
forAll { a: String ->
147+
val left: EitherT<NonEmptyList.F, String, String> = EitherT(NonEmptyList.of(Either.Left(a)))
148+
val expected = OptionT.none<NonEmptyList.F>()
149+
val calculated = left.toOptionT()
150+
151+
expected == calculated
152+
}
153+
}
154+
155+
"from option should build a correct EitherT" {
156+
forAll { a: String ->
157+
EitherT.fromEither<NonEmptyList.F, Int, String>(Either.Right(a)) == EitherT.pure<NonEmptyList.F, Int, String>(a)
158+
}
159+
}
160+
161+
"EitherTMonad#flatMap should be consistent with EitherT#flatMap" {
162+
forAll { a: Int ->
163+
val x = { b: Int -> EitherT.pure<Id.F, Int, Int>(b * a) }
164+
val option = EitherT.pure<Id.F, Int, Int>(a)
165+
option.flatMap(x) == EitherTMonad<Id.F, Int>(Id).flatMap(option, x)
166+
}
167+
}
168+
169+
"EitherTMonad#tailRecM should execute and terminate without blowing up the stack" {
170+
forAll { a: Int ->
171+
val value: EitherT<Id.F, Int, Int> = EitherTMonad<Id.F, Int>(Id).tailRecM(a) { b ->
172+
EitherT.pure<Id.F, Int, Either<Int, Int>>(Either.Right(b * a))
173+
}
174+
val expected = EitherT.pure<Id.F, Int, Int>(a * a)
175+
176+
expected == value
177+
}
178+
179+
forAll(Gen.oneOf(listOf(10000))) { limit: Int ->
180+
val value: EitherT<Id.F, Int, Int> = EitherTMonad<Id.F, Int>(Id).tailRecM(0) { current ->
181+
if (current == limit)
182+
EitherT.left<Id.F, Int, Either<Int, Int>>(current)
183+
else
184+
EitherT.pure<Id.F, Int, Either<Int, Int>>(Either.Left(current + 1))
185+
}
186+
val expected = EitherT.left<Id.F, Int, Int>(limit)
187+
188+
expected == value
189+
}
190+
}
191+
192+
"EitherTMonad#binding should for comprehend over option" {
193+
val M = EitherTMonad<NonEmptyList.F, Int>(NonEmptyList)
194+
val result = M.binding {
195+
val x = !M.pure(1)
196+
val y = M.pure(1).bind()
197+
val z = bind { M.pure(1) }
198+
yields(x + y + z)
199+
}
200+
result shouldBe M.pure(3)
201+
}
202+
203+
"Cartesian builder should build products over option" {
204+
EitherTMonad<Id.F, Int>(Id).map(EitherT.pure(1), EitherT.pure("a"), EitherT.pure(true), { (a, b, c) ->
205+
"$a $b $c"
206+
}) shouldBe EitherT.pure<Id.F, Int, String>("1 a true")
207+
}
208+
209+
"Cartesian builder works inside for comprehensions" {
210+
val M = EitherTMonad<NonEmptyList.F, Int>(NonEmptyList)
211+
val result = M.binding {
212+
val (x, y, z) = !M.tupled(M.pure(1), M.pure(1), M.pure(1))
213+
val a = M.pure(1).bind()
214+
val b = bind { M.pure(1) }
215+
yields(x + y + z + a + b)
216+
}
217+
result shouldBe M.pure(5)
218+
}
219+
}
220+
}

katz/src/test/kotlin/katz/data/EitherTest.kt

-6
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ class EitherTest : UnitSpec() {
1919

2020
"flatMap should modify entity" {
2121
forAll { a: Int, b: String ->
22-
{
2322
val left: Either<Int, Int> = Left(a)
2423

2524
Right(a).flatMap { left } == left
2625
&& Right(a).flatMap { Right(b) } == Right(b)
2726
&& left.flatMap { Right(b) } == left
28-
}()
2927
}
3028
}
3129

@@ -39,26 +37,22 @@ class EitherTest : UnitSpec() {
3937

4038
"exits should evaluate value" {
4139
forAll { a: Int ->
42-
{
4340
val left: Either<Int, Int> = Left(a)
4441

4542
Right(a).exists { it > a - 1 } == true
4643
&& Right(a).exists { it > a + 1 } == false
4744
&& left.exists { it > a - 1 } == false
48-
}()
4945
}
5046
}
5147

5248
"filterOrElse should filters value" {
5349
forAll { a: Int, b: Int ->
54-
{
5550
val left: Either<Int, Int> = Left(a)
5651

5752
Right(a).filterOrElse({ it > a - 1 }, { b }) == Right(a)
5853
&& Right(a).filterOrElse({ it > a + 1 }, { b }) == Left(b)
5954
&& left.filterOrElse({ it > a - 1 }, { b }) == Left(a)
6055
&& left.filterOrElse({ it > a + 1 }, { b }) == Left(a)
61-
}()
6256
}
6357
}
6458

0 commit comments

Comments
 (0)