Kotlin brings sealed classes and functional programming paradigms to the table. A type safe design can do miracles for your codebase so let's take a look how we can model runtime errors in our type system and what advantages FP concepts offer.
Union, Sum type “A data structure used to hold a value that could take several different, but fixed, types. Only one of the types can be in use at any one time.
id: String?, val name: String?, val lastName: String?, val email: String?, val isEmailVerified: Boolean, val isAnonymous: Boolean ) DOMAIN LOGIC!? WHEN CAN WHAT FIELD BE NULL? BOOLEAN FLAGS?
{ data class VerifiedUser(...): Visitor() data class UnverifiedUser(...): Visitor() object Anonymous : Visitor() } //some.hackery.HackyUser.kt data class HackyUser(val hackedId: String): Visitor() MIGHT BREAK MY LOGIC
{ data class VerifiedUser(...): Visitor() data class UnverifiedUser(...): Visitor() object Anonymous : Visitor() } //some.hackery.HackyUser.kt data class HackyUser(val hackedId: String): Visitor() SEALED (CLOSED) INHERITANCE
{ data class VerifiedUser(...): Visitor() data class UnverifiedUser(...): Visitor() object Anonymous : Visitor() } //some.hackery.HackyUser.kt data class HackyUser(val hackedId: String): Visitor() SEALED (CLOSED) INHERITANCE
data class VerifiedUser(...): Visitor() data class UnverifiedUser(...): Visitor() object Anonymous : Visitor() } fun sayHello(visitor: Visitor): String = when (visitor) { is Visitor.VerifiedUser -> "Hello ${visitor.name}," + "here are you settings/${visitor.id}" is Visitor.UnverifiedUser -> "Hello ${visitor.name}," + "we have sent a verification email to ${visitor.email}" is Visitor.Anonymous -> "Hello, would you like to register?" }
data class VerifiedUser(...): Visitor() data class UnverifiedUser(...): Visitor() object Anonymous : Visitor() } fun sayHello(visitor: Visitor): String = when (visitor) { is Visitor.VerifiedUser -> "Hello ${visitor.name}," + "here are you settings/${visitor.id}" is Visitor.UnverifiedUser -> "Hello ${visitor.name}," + "we have sent a verification email to ${visitor.email}" is Visitor.Anonymous -> "Hello, would you like to register?" } SMART CASTING
class Add( val left: MathExpression, val right: MathExpression ) : MathExpression() data class Subtract( val left: MathExpression, val right: MathExpression ) : MathExpression() data class Number(val value: Int) : MathExpression() }
class Add(left, right) : MathExpression() data class Subtract(left, right) : MathExpression() data class Number(val value: Int) : MathExpression() fun eval(): Int = when (this) { } } Error:(9,23) Kotlin:’when’ expression must be exhaustive, add `is Add`, `is Subtract` and `is Number` branches or `else` branch instead.
class Add(left, right) : MathExpression() data class Subtract(left, right) : MathExpression() data class Number(val value: Int) : MathExpression() fun eval(): Int = when (this) { is MathExpression.Add -> left.eval() + right.eval() is MathExpression.Subtract -> left.eval() - right.eval() is MathExpression.Number -> value } }
sealed class OOPMathExpression { abstract fun eval(): Int data class Add(left, right) : OOPMathExpression() { override fun eval() = left.eval() + right.eval() } data class Subtract(left, right) : OOPMathExpression() { override fun eval() = left.eval() - right.eval() } data class Number(value) : OOPMathExpression() { override fun eval() = value } }
sealed class OOPMathExpression { abstract fun eval(): Int data class Add(left, right) : OOPMathExpression() { override fun eval() = left.eval() + right.eval() } data class Subtract(left, right) : OOPMathExpression() { override fun eval() = left.eval() - right.eval() } data class Number(value) : OOPMathExpression() { override fun eval() = value } data class Division(left, right) : OOPMathExpression() } COMPILER DEMANDS IMPLEMENTATION
sealed class OOPMathExpression { abstract fun eval(): Int data class Add(left, right) : OOPMathExpression() { override fun eval() = left.eval() + right.eval() } data class Subtract(left, right) : OOPMathExpression() { override fun eval() = left.eval() - right.eval() } data class Number(value) : OOPMathExpression() { override fun eval() = value } data class Division(left, right) : OOPMathExpression() { override fun eval() = left.eval() / right.eval() } }
sealed class MathExpression { data class Add(left, right) : MathExpression() data class Subtract(left, right) : MathExpression() data class Number(value) : MathExpression() data class Division(left, right): MathExpression() fun eval(): Int = when (this) { is MathExpression.Add -> left.eval() + right.eval() is MathExpression.Subtract -> left.eval() - right.eval() is MathExpression.Number -> value is MathExpression.Add -> left.eval() / right.eval() } }
sealed class MathExpression { data class Add(left, right) : MathExpression() data class Subtract(left, right) : MathExpression() data class Number(value) : MathExpression() data class Division(left, right): MathExpression() fun eval(): Int = when (this) { is MathExpression.Add -> left.eval() + right.eval() is MathExpression.Subtract -> left.eval() - right.eval() is MathExpression.Number -> value is MathExpression.Add -> left.eval() / right.eval() } } ANY ISSUES HERE???
sealed class MathExpression { data class Add(left, right) : MathExpression() data class Subtract(left, right) : MathExpression() data class Number(value) : MathExpression() data class Division(left, right): MathExpression() fun eval(): Int = when (this) { is MathExpression.Add -> left.eval() + right.eval() is MathExpression.Subtract -> left.eval() - right.eval() is MathExpression.Number -> value is MathExpression.Add -> left.eval() / right.eval() } } ANY ISSUES HERE??? JAVA.LANG.ARITHMETICEXCEPTION: / BY ZERO AT MATHEXPRESSION2.EVAL(MATHEXPRESSION2.KT:13)
data class Add(left, right) : MathExpression() data class Number(value: Int) : MathExpression() data class Division(left, right) : MathExpression() fun eval(): MathResult = when (this) { ... } }
when (this) { is MathExpression.Add -> { val lR: MathResult = left.eval() when (lR) { is Failure -> lR is Success -> { } } } is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Add -> { val lResult: MathResult = left.eval() when (lResult) { is Failure -> lResult is Success -> { val rR: MathResult = right.eval() } } } is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Add -> { val lResult: MathResult = left.eval() when (lResult) { is Failure -> lResult is Success -> { val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lR.value + rR.value) } } } } is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> { val lR: MathResult = left.eval() } is MathExpression.Add -> {…} is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> { val lR = left.eval() when (lR) { is Failure -> lR is Success -> { } } is MathExpression.Add -> {…} is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> { val lR = left.eval() when (lR) { is Failure -> lR is Success -> { val rR = right.eval() } } is MathExpression.Add -> {…} is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> { val lR = left.eval() when (lR) { is Failure -> lR is Success -> { val rR = right.eval() when (rR) { is Failure -> rR is Success -> if (rR.value != 0) Success(lR.value / rR.value) else Failure(ArithmeticException(“Cannot / by zero")) } } } } is MathExpression.Add -> {…} is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> if (rR.value != 0) Success(lR.value / rR.value) else Failure(ArithmeticException(“Cannot / by zero")) is MathExpression.Add -> Success(lR.value + rR.value) is MathExpression.Number -> Success(value) }
when (this) { is MathExpression.Division -> if (rR.value != 0) lR.value / rR.value else ArithmeticException(“Cannot / by zero”) is MathExpression.Add -> lR.value + rR.value is MathExpression.Number -> value }
{ val lResult: MathResult = left.eval() when (lResult) { is Failure -> lResult is Success -> { val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lR.value + rR.value) } } } }
{ val lResult: MathResult = left.eval() when (lResult) { is Failure -> lResult is Success -> { val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lR.value + rR.value) } } } } IF FAILURE IMMEDIATELY RETURN ELSE COMPUTE NEW MATH RESULT
{ val lResult: MathResult = left.eval() when (lResult) { is Failure -> lResult is Success -> { val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lR.value + rR.value) } } } } IF FAILURE IMMEDIATELY RETURN ELSE COMPUTE NEW MATH RESULT fun flatMap(f: (Int) -> MathResult): MathResult = when (this) { is MathResult.Failure -> this is MathResult.Success -> f(value) }
{ left.eval().flatMap { lValue -> val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lValue + rR.value) } } } IF FAILURE IMMEDIATELY RETURN ELSE COMPUTE NEW VALUE
{ left.eval().flatMap { lValue -> val rR: MathResult = right.eval() when (rR) { is Failure -> rResult is Success -> Success(lValue + rR.value) } } } IF FAILURE IMMEDIATELY RETURN ELSE COMPUTE NEW VALUE fun map(f: (Int) -> Int): MathResult = when(this) { is MathResult.Failure -> this is MathResult.Success -> MathResult.Success(f(value)) }
▸ Bypasses type system ▸ Abused to model non existing values. ▸ Nulls can appear anywhere, in any value or variable. ▸ No way to control them as they can be anything. ▸ Hard to debug and leads to bugs
class Success<T>(val value: T) : Try<T>() data class Failure<T>(val exception: Throwable) : Try<T>() } sealed class Option<out T> { data class Some<out T>(val value: T) : Option<T>() object None : Option<Nothing>() } sealed class Either<out L, out R> { data class Left<out T>(val left: T) : Either<T, Nothing>() data class Right<out R>(val right: R) : Either<Nothing, R>() }