Last active
November 27, 2017 18:00
-
-
Save jmfayard/552512b30d2b5771c1c23afd1a593d1b to your computer and use it in GitHub Desktop.
Functional datatypes & abstractions for Kotlin http://kategory.io/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package kategory | |
import io.kotlintest.matchers.* | |
import io.kotlintest.specs.FreeSpec | |
import kategory.Problem.* | |
import kotlin.reflect.KClass | |
/*** Kategory.io documentation as runnable code ***/ | |
class DataTypeExamples : FreeSpec() { init { | |
/** | |
* Option http://kategory.io/docs/datatypes/option/ | |
***/ | |
"Option: Some or None?" - { | |
val someValue: Option<Int> = Some(42) | |
val noneValue: Option<Int> = None | |
"getOrElse" { | |
someValue.getOrElse { -1 }.shouldBe(42) | |
noneValue.getOrElse { -1 }.shouldBe(-1) | |
} | |
"is it None?" { | |
(someValue is None) shouldBe false | |
(noneValue is None) shouldBe true | |
} | |
"When statement" { | |
// Option can also be used with when statements: | |
val msg = when (someValue) { | |
is Some -> "ok" | |
Option.None -> "ko" | |
} | |
msg shouldBe "ok" | |
} | |
"Functor/Foldable style operations" { | |
// An alternative for pattern matching is performing Functor/Foldable style operations. | |
// This is possible because an option could be looked at as a collection or foldable structure with either one or zero elements. | |
// One of these operations is map. This operation allows us to map the inner value to a different type while preserving the option | |
someValue.map { msg -> msg / 6 } shouldBe Some(7) | |
noneValue.map { msg -> msg / 6 } shouldBe None | |
} | |
"Fold" { | |
// Fold will extract the value from the option, or provide a default if the value is None | |
someValue.fold({ 1}, { it * 3}) shouldBe 126 | |
noneValue.fold({ 1}, { it * 3}) shouldBe 1 | |
} | |
"Applicative" { | |
// Computing over independent values | |
val tuple = Option.applicative().tupled(Option(1), Option("Hello"), Option(20.0)) | |
tuple shouldBe Some(Tuple3(a=1, b="Hello", c=20.0)) | |
} | |
"Monad" { | |
// Computing over dependent values ignoring absence | |
val six = Option.monad().binding { | |
val a = Option(1).bind() | |
val b = Option(1 + a).bind() | |
val c = Option(1 + b).bind() | |
yields(a + b + c) | |
} | |
six shouldBe Some(6) | |
val none = Option.monad().binding { | |
val a = Option(1).bind() | |
val b = noneValue.bind() | |
val c = Option(1 + b).bind() | |
yields(a + b + c) | |
} | |
none shouldBe None | |
} | |
} | |
// http://kategory.io/docs/datatypes/try/ | |
"Try and recover" - { | |
"Old school" { | |
val dollars = try { | |
playLottery(9) | |
} catch (e: AuthorizationException) { | |
0 | |
} | |
dollars shouldBe 0 | |
} | |
"Try { .. } " { | |
val gain: Try<Int> = Try { playLottery(9) } | |
gain shouldBe aFailureOfType(AuthorizationException::class) | |
gain.getOrElse { 0 } shouldBe 0 | |
} | |
"filter" { | |
// If you want to perform a check on a possible success, | |
// you can use filter to convert successful computations in failures if conditions aren’t met: | |
val tryJackpot = Try { playLottery(10) }.filter { it > 500 } | |
tryJackpot shouldBe aFailureOfType(TryException.PredicateException::class) | |
} | |
"Recover" { | |
val gain = Try { playLottery(99) } | |
gain.recover { 0 } shouldBe Try.Success(0) | |
gain.recoverWith { Try { playLottery(42)} } shouldBe Try.Success(1000) | |
} | |
"Fold" { | |
// When you want to handle both cases of the computation you can use fold. | |
// With fold we provide two functions, | |
// one for transforming a failure into a new value, | |
// the second one to transform the success value into a new one: | |
val gain = Try { playLottery(99) } | |
gain.fold({ 4 }, { error("not expected") }) shouldBe 4 | |
val jackPot = Try { playLottery(42) } | |
jackPot.fold({ error("not expected") }, { it * 100 }) shouldBe 100_000 | |
} | |
"Functor" { | |
// Transforming the value, if the computation is a success: | |
val actual = Try.functor().map(Try { "3".toInt() }, { it + 1}) | |
actual shouldBe Try.Success(4) | |
} | |
"Applicative" { | |
// Computing over independent values: | |
val tryHarder = Try.applicative().tupled( | |
Try { "3".toInt() }, | |
Try { "5".toInt() }, | |
Try { "nope".toInt() } | |
) | |
tryHarder shouldBe aFailureOfType(NumberFormatException::class) | |
} | |
} | |
// Either http://kategory.io/docs/datatypes/either/ | |
"Either left or right" - { | |
fun parse(s: String): ProblemOrInt = Try { s.toInt().right() }.getOrElse { invalidInt.left() } | |
fun reciprocal(i: Int) : Either<Problem, Double> = when(i) { | |
0 -> noReciprocal.left() | |
else -> Either.Right(1.0 / i) | |
} | |
fun magic(s: String): Either<Problem, String> = | |
parse(s).flatMap{ reciprocal(it) }.map{ it.toString() } | |
var either : ProblemOrInt | |
"Right" { | |
either = 5.right() | |
either shouldBe Either.Right(5) | |
either.getOrElse { 0 } shouldBe 5 | |
either.map { it+1 } shouldBe 6.right() | |
either.flatMap { 6.right() } shouldBe 6.right() | |
either.flatMap { somethingWentWRong.left() } shouldBe somethingWentWRong.left() | |
} | |
"Left" { | |
// either is right-biaised | |
either = Either.Left(somethingWentWRong) | |
either shouldBe somethingWentWRong.left() | |
either.getOrElse { 0 } shouldBe 0 | |
either.map { it + 1 } shouldBe either | |
either.flatMap { somethingExploded.left() } shouldBe either | |
} | |
"Either rather than exception" { | |
parse("Not an number") shouldBe invalidInt.left() | |
parse("2") shouldBe 2.right() | |
} | |
"Combinators" { | |
magic("0") shouldBe noReciprocal.left() | |
magic("Not a number") shouldBe invalidInt.left() | |
magic("1") shouldBe "1.0".right() | |
} | |
} | |
} | |
fun aFailureOfType(expected: KClass<*>): Matcher<Try<Int>> = object : Matcher<Try<Int>> { | |
override fun test(value: Try<Int>): Result = when (value) { | |
is Success -> Result(false, "Expected a failure, got $value") | |
is Failure -> { | |
val javaClass = value.exception.javaClass | |
Result(expected.java.isAssignableFrom(javaClass), "Expected Try.Failure(${expected.java}), got $value") | |
} | |
} | |
} | |
} | |
private typealias ProblemOrInt = Either<Problem, Int> | |
private enum class Problem(val message: String) { | |
somethingWentWRong("Something went wrong"), | |
somethingExploded("Something somethingExploded"), | |
invalidInt("This is not an integer"), | |
noReciprocal("Cannot take noReciprocal of 0.") | |
} | |
private open class GeneralException: Exception() | |
private object NoConnectionException: GeneralException() | |
private object AuthorizationException: GeneralException() | |
fun playLottery(guess: Int): Int { | |
return when (guess){ | |
42 -> 1000 // jackpot | |
in 10..41 -> 1 | |
in 0..9 -> throw AuthorizationException | |
else -> throw NoConnectionException | |
} | |
} |
Author
jmfayard
commented
Nov 27, 2017
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment