Skip to content

Commit 80f6da7

Browse files
committed
ref: Either class evolution
1 parent b16160b commit 80f6da7

File tree

6 files changed

+282
-109
lines changed

6 files changed

+282
-109
lines changed

app/src/main/kotlin/com/fernandocejas/sample/core/functional/Either.kt

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
@file:Suppress("DEPRECATION")
17+
1618
package com.fernandocejas.sample.core.functional
1719

20+
import com.fernandocejas.sample.core.functional.Either.Left
21+
import com.fernandocejas.sample.core.functional.Either.Right
22+
1823
/**
1924
* Represents a value of one of two possible types (a disjoint union).
2025
* Instances of [Either] are either an instance of [Left] or [Right].
@@ -26,10 +31,14 @@ package com.fernandocejas.sample.core.functional
2631
*/
2732
sealed class Either<out L, out R> {
2833
/** * Represents the left side of [Either] class which by convention is a "Failure". */
29-
data class Left<out L>(val a: L) : Either<L, Nothing>()
34+
data class Left<out L>
35+
@Deprecated(".toLeft()", ReplaceWith("a.toLeft()"))
36+
constructor(val a: L) : Either<L, Nothing>()
3037

3138
/** * Represents the right side of [Either] class which by convention is a "Success". */
32-
data class Right<out R>(val b: R) : Either<Nothing, R>()
39+
data class Right<out R>
40+
@Deprecated(".toRight()", ReplaceWith("b.toRight()"))
41+
constructor(val b: R) : Either<Nothing, R>()
3342

3443
/**
3544
* Returns true if this is a Right, false otherwise.
@@ -44,30 +53,63 @@ sealed class Either<out L, out R> {
4453
val isLeft get() = this is Left<L>
4554

4655
/**
47-
* Creates a Left type.
56+
* Applies fnL if this is a Left or fnR if this is a Right.
4857
* @see Left
49-
*/
50-
fun <L> left(a: L) = Either.Left(a)
51-
52-
53-
/**
54-
* Creates a Left type.
5558
* @see Right
5659
*/
57-
fun <R> right(b: R) = Either.Right(b)
60+
fun <T> fold(fnL: (L) -> T, fnR: (R) -> T): T =
61+
when (this) {
62+
is Left -> fnL(a)
63+
is Right -> fnR(b)
64+
}
5865

5966
/**
6067
* Applies fnL if this is a Left or fnR if this is a Right.
68+
*
69+
* Kotlin Coroutines Support.
6170
* @see Left
6271
* @see Right
6372
*/
64-
fun fold(fnL: (L) -> Any, fnR: (R) -> Any): Any =
73+
suspend fun <T> coFold(fnL: suspend (L) -> T, fnR: suspend (R) -> T): T =
6574
when (this) {
6675
is Left -> fnL(a)
6776
is Right -> fnR(b)
6877
}
78+
79+
companion object {
80+
/**
81+
* Transforms a try/catch in an Either<Exception, Right>
82+
* See [mapException]
83+
* **/
84+
@Suppress("TooGenericExceptionCaught")
85+
suspend fun <Right> catch(
86+
operation: suspend () -> Right
87+
): Either<Exception, Right> =
88+
try {
89+
operation().toRight()
90+
} catch (e: Exception) {
91+
e.toLeft()
92+
}
93+
}
6994
}
7095

96+
/** If Left is of type Exception, allows to map it to another type.
97+
* Rethrow the exception if [operation] returns null **/
98+
fun <Left : Any, Right> Either<Exception, Right>.mapException(
99+
operation: (Exception) -> Left?
100+
): Either<Left, Right> = when (this) {
101+
is Either.Left -> operation(a)?.toLeft() ?: throw a
102+
is Either.Right -> this
103+
}
104+
105+
/** Represents the right side of [Either] class which by convention is a "Success". **/
106+
fun <T> T.toRight(): Right<T> =
107+
Right(this)
108+
109+
/** Represents the left side of [Either] class which by convention is a "Failure". */
110+
fun <T> T.toLeft(): Left<T> =
111+
Left(this)
112+
71113
/**
72114
* Composes 2 functions
73115
* See <a href="https://proandroiddev.com/kotlins-nothing-type-946de7d464fb">Credits to Alex Hart.</a>
@@ -82,37 +124,64 @@ fun <A, B, C> ((A) -> B).c(f: (B) -> C): (A) -> C = {
82124
*/
83125
fun <T, L, R> Either<L, R>.flatMap(fn: (R) -> Either<L, T>): Either<L, T> =
84126
when (this) {
85-
is Either.Left -> Either.Left(a)
86-
is Either.Right -> fn(b)
127+
is Left -> Left(a)
128+
is Right -> fn(b)
87129
}
88130

89131
/**
90-
* Right-biased map() FP convention which means that Right is assumed to be the default case
132+
* Right-biased flatMap() FP convention which means that Right is assumed to be the default case
91133
* to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged.
134+
* It works with Kotlin Coroutines (suspension functions).
92135
*/
93-
fun <T, L, R> Either<L, R>.map(fn: (R) -> (T)): Either<L, T> = this.flatMap(fn.c(::right))
94-
95-
/** Returns the value from this `Right` or the given argument if this is a `Left`.
96-
* Right(12).getOrElse(17) RETURNS 12 and Left(12).getOrElse(17) RETURNS 17
97-
*/
98-
fun <L, R> Either<L, R>.getOrElse(value: R): R =
136+
suspend fun <T, L, R> Either<L, R>.coFlatMap(fn: suspend (R) -> Either<L, T>): Either<L, T> =
99137
when (this) {
100-
is Either.Left -> value
101-
is Either.Right -> b
138+
is Left -> Left(a)
139+
is Right -> fn(b)
102140
}
103141

104142
/**
105143
* Left-biased onFailure() FP convention dictates that when this class is Left, it'll perform
106144
* the onFailure functionality passed as a parameter, but, overall will still return an either
107145
* object so you chain calls.
108146
*/
109-
fun <L, R> Either<L, R>.onFailure(fn: (failure: L) -> Unit): Either<L, R> =
110-
this.apply { if (this is Either.Left) fn(a) }
147+
infix fun <L, R> Either<L, R>.onFailure(fn: (failure: L) -> Unit): Either<L, R> =
148+
this.apply { if (this is Left) fn(a) }
111149

112150
/**
113151
* Right-biased onSuccess() FP convention dictates that when this class is Right, it'll perform
114152
* the onSuccess functionality passed as a parameter, but, overall will still return an either
115153
* object so you chain calls.
116154
*/
117-
fun <L, R> Either<L, R>.onSuccess(fn: (success: R) -> Unit): Either<L, R> =
118-
this.apply { if (this is Either.Right) fn(b) }
155+
infix fun <L, R> Either<L, R>.onSuccess(fn: (success: R) -> Unit): Either<L, R> =
156+
this.apply { if (this is Right) fn(b) }
157+
158+
/**
159+
* Right-biased map() FP convention which means that Right is assumed to be the default case
160+
* to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged.
161+
*/
162+
fun <L, R, T> Either<L, R>.map(fn: (R) -> (T)): Either<L, T> =
163+
when (this) {
164+
is Left -> Left(a)
165+
is Right -> Right(fn(b))
166+
}
167+
168+
/**
169+
* Right-biased map() FP convention which means that Right is assumed to be the default case
170+
* to operate on. If it is Left, operations like map, flatMap, ... return the Left value unchanged.
171+
* It works with Kotlin Coroutines (suspension functions).
172+
*/
173+
suspend fun <L, R, T> Either<L, R>.coMap(fn: suspend (R) -> (T)): Either<L, T> =
174+
when (this) {
175+
is Left -> Left(a)
176+
is Right -> Right(fn(b))
177+
}
178+
179+
/**
180+
* Returns the value from this `Right` or the given argument if this is a `Left`.
181+
* Right(12).getOrElse(17) RETURNS 12 and Left(12).getOrElse(17) RETURNS 17
182+
*/
183+
fun <L, R> Either<L, R>.getOrElse(value: R): R =
184+
when (this) {
185+
is Left -> value
186+
is Right -> b
187+
}

app/src/test/kotlin/com/fernandocejas/sample/AndroidTest.kt

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,4 @@ abstract class AndroidTest {
5959
statement
6060
}
6161
}
62-
63-
/**
64-
* Container for customized Android
65-
* specific Test Assertions.
66-
*/
67-
object AndroidAssertions {
68-
infix fun KClass<out AppCompatActivity>.shouldNavigateTo(
69-
nextActivity: KClass<out AppCompatActivity>
70-
): () -> Unit = {
71-
72-
val originActivity = Robolectric.buildActivity(this.java).get()
73-
val shadowActivity = Shadows.shadowOf(originActivity)
74-
val nextIntent = shadowActivity.peekNextStartedActivity()
75-
76-
nextIntent.component?.className shouldBeEqualIgnoringCase nextActivity.java.canonicalName!!
77-
}
78-
}
7962
}

0 commit comments

Comments
 (0)