Code Without Exceptions
What Is Functional?
Immutable?
Provability?
Side Effects?
An answer:
"What would code look like if we wrote only functions?"
And What are exceptions?
Goto statements
Poor abstractions
Difficult to know about
"What would code look like if we wrote no exceptions?"
Let's Write Some Code
See how it feels, what we can learn.
What is an exception?
Exceptions solve the same problem that GOTO statements solve, skipping large blocks of code execution.
fun doThing() {
println("Final value:" + doBusinessLogic())
}
fun doBusinessLogic(): Int {
val x = doDatabaseThing()
return x + 1
}
fun doDatabaseThing(): Int {
// todo a real database connection
return 5
}
doThing()
What is an exception?
import java.io.IOException
fun doDatabaseThing(): Int {
// todo a real database connection
// but we can't connect to a database, our function signature REQUIRES an Int, we can't
// meet that, we can throw!
throw IOException("Badness")
}
fun doBusinessLogic(): Int {
val x = doDatabaseThing()
// we can't try/catch the database exception here, if we do
// we get back an Exception, but our function signature REQUIRES an Int, so if we
// get an exception, we don't know how to add +1 to an Exception, so we allow
// the exception to bubble up
return x + 1
}
fun doThing() {
// okay we're finally at a level of abstraction that understands
// I wanted to do stuff, but it failed, I need to do something else
// I try/catch all Exception, the underlying type is meaningless to me
// no matter what happened to cause it to fail, it failed, I need to just display a message
try {
println("Final value:" + doBusinessLogic())
} catch(ex: Exception) {
println("Can't calculate value due to: " + ex.message)
}
}
doThing()
// Can't calculate value due to: Badness
Key Insights
- It's hard to know that a layer under you throws an exception without reading all of your underlying source code (this is impossible for any real application).
- We don't usually care about Exception type, we care about outcome and short circuiting code execution
- Exception handling can be slow
- Logic just went everywhere (spooky action at a distance with Exceptions).
// What if we don't want to throw?
fun doDatabaseThing(): Int? {
// what if we captured some notion of success or failure using the type system?
// if we can't get info from the Database, maybe a null type would do?
return null
}
fun doBusinessLogicOne(): Int? {
val x = doDatabaseThing()
// hmm, now our null leaked out here, no worry, we can just propagate the null
return if (x != null) { // forget kotlin syntactic sugar, we want to think of all languages
x + 1
} else {
x
}
}
fun doBusinessLogicTwo(x: Int?): Int? {
// now we need more null checks here :/
return if (x != null) {
x * 2
} else {
x
}
}
fun doThing() {
val finalValue = doBusinessLogicTwo(doBusinessLogic())
if (finalValue != null) {
println("Final value:" + finalValue)
} else {
println("Somewhere, an error!")
}
}
doThing()
What did we learn trying to avoid Throws?
- Null checks EVERYWHERE, it leaked into our code
- We actually lost information, the final null check just says "Something wrong in the system".
- We see code like this sometimes, think about this pattern when you see it in your code.
Can We Consolidate responsibility for Null Checking?
fun doDatabaseThing(): Int? {
return null
}
fun doBusinessLogicOne(): Int? {
val x = doDatabaseThing() // maybe tight coupling to the db hurt us
// here, we still have a null
return if (x != null) {
x + 1
} else {
x
}
}
fun doBusinessLogicTwo(x: Int): Int {
return x * 2
}
fun doThing() {
val initialValue = doBusinessLogic()
val finalValue = if (initialValue != null) {
println("Final value: " + doBusinessLogicTwo(initialValue))
} else {
println("There was an error in businessLogic()!")
}
println("Final value:" + finalValue)
}
}
doThing()
Can We Consolidate responsibility for Null Checking?
fun doDatabaseThing(): Int? {
return null
}
fun doBusinessLogicOne(x: Int): Int { // look at these functions, so clean and easy again!
return x + 1
}
fun doBusinessLogicTwo(x: Int): Int {
return x * 2
}
fun doThing() {
val initialValue = doDatabaseThing() // as an aside, this is
// now closer to the "clean architecture"
val intermediate = if (initialValue != null) {
doBusinessLogicOne(initialValue)
} else {
// wait, what's going on here?!!?!
return
}
println("Final value: " + doBusinessLogicTwo(intermediate))
}
doThing()
This is looking better but...
- What if each of those Business Logic steps could fail?
- It feels like the "plumbing" between these function calls over in doThing() is messy
- Because doThing() still needs to know if each step failed and what to do, we got a return statement appear in the middle, we're still kind of trying to goto
- The alternative to that return might be more null checks:
fun doThing() {
val initialValue = doDatabaseThing()
val intermediate: Int? = if (initialValue != null) {
doBusinessLogicOne(initialValue)
} else { null }
if (intermediate != null) {
println("Final value: " + doBusinessLogicTwo(intermediate))
} else {
println("There was an error in businessLogic()!")
}
}
We Realize
- We are trying to leverage the type system to tell us (1) success/failure (2) in success state, that's the value? Two things!
- Can we write some code to reduce our plumbing?
Let's Make Types Work
We can hand write this all, but let's use a library (Arrow) instead!
class DatabaseConnectionError : Exception()
fun doDatabaseThing(): Either<DatabaseConnectionError, Int> {
return Right(5)
}
So now we know exactly what doDatabaseThing is able to return.
"Right" means "did the right thing" "Left" means "something else" by convention.
Let's Make Types Work
class DatabaseConnectionError : Exception()
fun doDatabaseThing(): Either<DatabaseConnectionError, Int> {
return Right(5)
}
fun doBusinessLogicOne(x: Int): Int {
return x + 1
}
fun doBusinessLogicTwo(x: Int): Int {
return x * 2
}
fun doThing() {
val result = doDatabaseThing()
.map(::doBusinessLogicOne) // not our standard kotlin map
.map(::doBusinessLogicTwo)
result.bimap(
{ value -> println("There was an error!! it was: " + value.message )},
{ value -> println("Final value: " + value)}
)
}
Final value: 12
Let's Make Types Work
class DatabaseConnectionError(message: String) : Exception(message)
fun doDatabaseThing(): Either<DatabaseConnectionError, Int> {
return Left(DatabaseConnectionError("Badness"))
}
...
fun doThing() {
val result = doDatabaseThing()
.map(::doBusinessLogicOne) // not our standard kotlin map
.map(::doBusinessLogicTwo)
result.bimap(
{ value -> println("There was an error!! it was: " + value.message )},
{ value -> println("Final value: " + value)}
)
}
There was an error!! it was: Badness
Dense Lingo
- This is a "functor". A "functor" is a value that's >in< something.
- In this case we have a value (an Int or an Exception). It's inside of a class. That class is capturing information for us and allowing us to use the type system without endless "if" statements.
- In order to "do stuff" to a "functor" we need to use "map"
Last One (alternate syntax)
fun main(args: Array<String>) {
val result = doThing()
result.bimap(
{ value -> println("There was an error!! it was: " + value.message )},
{ value -> println("Final value: " + value)}
)
}
class DatabaseConnectionError(message: String) : Exception(message)
fun doDatabaseThing(): Either<DatabaseConnectionError, Int> {
return Left(DatabaseConnectionError("Badness"))
}
fun doBusinessLogicOne(x: Int): Int {
return x + 1
}
fun doBusinessLogicTwo(x: Int): Int {
return x * 2
}
fun doThing(): Either<DatabaseConnectionError, Int> =
Either.monad<DatabaseConnectionError>().binding {
val dbvalue = doDatabaseThing().bind()
val resultOne = doBusinessLogicOne(dbvalue)
val resultTwo = doBusinessLogicTwo(resultOne)
yields(resultTwo)
}.ev()
Conclusion
We tried an experiment, what would code look like without exceptions?
There's a lot of nice stuff, but it gets clunky to code.
There's libraries that help us code this way.
The "functor" is something that happens when you try to write code this way, and it makes life easier.
Reading
https://github.com/arrow-kt/arrow/blob/3fdf22311dbdf07c68ee73ab05cd2f21561d6636/arrow-core/src/main/kotlin/arrow/core/Either.kt
http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
Code Without Exceptions
By Philip Doctor
Code Without Exceptions
- 1,203