Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Error Handling and Logging in a Functional World

Error Handling and Logging in a Functional World

Learning Functional Programming in the field. Taking examples from two chapters of my book "From Objects to Functions" (pragprog.com) we will see how using a functional approach changes the error handling and the logging parts of our application.

Using exceptions to recover from errors in our programs goes against the principle of functional programming because it will prevent us from defining the logic composing pure functions since an Exception cannot be a valid return value for a pure function.

In this talk, I'll present some possible techniques to handle errors differently and, arguably, make our code easier to read and maintain.

Functional programming also allows us to centralize the logic to log significant events with their context. Instead of using simple string messages scattered along our code, we can define more specific domain events and log them with the full context of the error, removing the need to configure log levels.

The example code presented is in Kotlin, but applying the same techniques in Java or other languages should be easy.

Uberto Barbini

October 14, 2022
Tweet

More Decks by Uberto Barbini

Other Decks in Programming

Transcript

  1. #Devoxx @ramtop Error Handling and Logging in a Functional World

    Uberto Barbini @ramtop medium.com/@ramtop
  2. #Devoxx @ramtop Tweet about this talk mentioning me to win

    a copy of my book (courtesy of PragProg) Why Another Book?
  3. #Devoxx @ramtop In this talk you will learn: 1. Exceptions

    are overused 2. Log levels are redundant 3. Static loggers… just say no!
  4. #Devoxx @ramtop Functional Programming misconceptions: 1-It consists in using list.map{}

    and lambdas in your objects 2-It can only be done with special libraries or languages 3-It’s not clearly defined (I blogged about it https://medium.com/@ramtop/c317f857bcea)
  5. #Devoxx @ramtop Functional Programming is a paradigm inspired by Lambda

    Calculus Programs are constructed by composing pure functions. (the name is a bit of giveaway) Alonzo Church
  6. #Devoxx @ramtop Pure functions follow two rules: 1.don’t use any

    other input that their arguments (no global variables/singleton) 2.don’t produce any other output that their result (no mutable state) They must be referentially transparent
  7. #Devoxx @ramtop Thinking in Morphisms "/todo/{user}/{list}" bind GET to ::getToDoList

    fun getToDoList(request: Request): Response = request.let(::extractListData) .let(::fetchListContent) .let(::renderHtml) .let(::createResponse) // createResponse( renderHtml( fetchListContent( extractListData(request) )))
  8. #Devoxx @ramtop What about impure actions/computations? (i.e. FileSystem or Network

    IO) Functional Effects instead of Side Effects! Monads
  9. #Devoxx @ramtop is that once you get the epiphany, once

    you understand - "oh that's what it is" you lose the ability to explain it to anybody. Doug Crockford The Curse of the Monad
  10. #Devoxx @ramtop First Hint:A Monad is a Functor (an Endofunctor

    to be precise) Functors are Transformers! A DataStructure<T> that can be transformed by a function T U → into a DataStructure<U>
  11. #Devoxx @ramtop The most useful functor is the ContextReader data

    class ContextReader<CTX, T>(val runWith: (CTX) -> T) { fun <U> transform(f: (T) -> U): ContextReader<CTX, U> = ContextReader { ctx -> f(runWith(ctx)) } } A Reader can be used to work with a Cache, a File, a Socket, a Database, a Thread etc. All different kind of functional effects.
  12. #Devoxx @ramtop How to use a Reader data class ContextReader<CTX,

    T>(val runWith: (CTX) -> T) { fun <U> transform(f: (T) -> U): ContextReader<CTX, U> = ContextReader { ctx -> f(runWith(ctx)) } } fun openConn(conn: DbConn): DbSession = //some code fun readList(sess: DbSession, id: ListId): ToDoList fun renderListToHtml(list: ToDoList): HtmlPage fun listRepository() = ContextReader(::connectDb) val page = listRepository() //ContextReader<DbConn, DbSession> .transform{ readList(it, listId) } //ContextReader<DbConn, ToDoList> .transform(::renderListToHtml) //ContextReader<DbConn, HtmlPage> .runWith(dbConn) //HtmlPage
  13. #Devoxx @ramtop A monad is a Functor, but not any

    Functor are Monads, only Functors that can be bound together. (they have a monoid instance)
  14. #Devoxx @ramtop fun readList(sess: DbSession, id: ListId): ToDoList fun writeList(sess:

    DbSession, list: ToDoList): ToDoList val page = listRepository() //ContextReader<DbConn, DbSession> .transform{ readList(it, listId) } //ContextReader<DbConn, ToDoList> .transform{ renameList(“newName”) } //ContextReader<DbConn, ToDoList> .transform{ connectToDb().writeList(it) } //ContextReader<ContextReader<DbConn, ToDoList>> !!!! error !!! You cannot use transform() to bind Functors
  15. #Devoxx @ramtop data class ContextReader<CTX, T>(val runWith: (CTX) -> T)

    { fun <U> transform(f: (T) -> U): ContextReader<CTX, U> = ContextReader { ctx -> f(runWith(ctx)) } fun <U> bind(f: (T) -> ContextReader<CTX, U>): ContextReader<CTX, U> = ContextReader { ctx -> f(runWith(ctx)).runWith(ctx) } } fun readList(sess: DbSession, id: ListId): ToDoList fun writeList(sess: DbSession, list: ToDoList): ToDoList val page = listRepository() //ContextReader<DbConn, DbSession> .transform{ readList(it, listId) } //ContextReader<DbConn, ToDoList> .transform{ renameList(“newName”) } //ContextReader<DbConn, ToDoList> .bind{ listRepository().writeList(it) } //ContextReader<DbConn, ToDoList> .transform(::renderListToHtml) //ContextReader<DbConn, HtmlPage> .runWith(dbConn) //HtmlPage
  16. #Devoxx @ramtop Unchecked Exceptions break the flow unexpectedly lose the

    original context Checked Exceptions verbose to handle force us to handle the result twice
  17. #Devoxx @ramtop Functional solution: a Monad that can be Either

    a Result or an Error listRepository .retrieveList(listName) .transform { list -> renameList(list, newName)} .bind(::storeList) .wrapFailure("Failure while renaming $listName to $newName") sealed class Outcome<E, T> data class Success<T>(val value: T): Outcome<Nothing, T>() data class Failure<E>(val error: E): Outcome<E, Nothing>()
  18. #Devoxx @ramtop Kirk Pepperdine Log Principle Log as much as

    you can when things go wrong, log as little as you dare when things go well.
  19. #Devoxx @ramtop Outcome has the information we want to log

    typealias Logger = (Outcome<*, *>, LogContext) -> Unit class StreamLogger: Logger {…} class ConsoleLogger: Logger {…} listRepository .retrieveList(listName) .transform { list -> renameList(list, newName)} .bind(::storeList) .also{logger(it, logContext(user, “list rename”)}
  20. #Devoxx @ramtop What about debug statements? Exception in thread "main"

    java.lang.NullPointerException at java.util.HashMap.merge(HashMap.java:1216) at java.util.stream.Collectors.lambda$toMap$168(Collectors.java:1320) at java.util.stream.Collectors$$Lambda$5/1528902577.accept(Unknown Source) at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) at Main.main(Main.java:48)
  21. #Devoxx @ramtop No logger factories No log levels Log structured

    data No configuration files Better performance Easier to debug Easier to test
  22. #Devoxx @ramtop Towards useful error messages Error on operation “getAllLists”

    with user "Carol" and message "Transaction rolled back because org.postgresql.util.PSQLException: ERROR: column \"row_data\" does not exist” processing request “GET /todo/Carol” with headers: “HTTP/1.1\r\nUser-Agent: Jetty/11.0.6\r\nHost: localhost:8000\r\nAccept-Encoding: gzip\r\nContent-Length: 0\r\nContent-Type: application/octet-stream\r\n\r\n)"
  23. #Devoxx @ramtop Can we do better? Let’s separate the Domain

    from technical layers with the Ports And Adapter Architecture
  24. #Devoxx @ramtop Writing logs is an adapter concern, not a

    domain one! - logs all the domain calls - avoid duplicates - consistency and DRY - easier to test
  25. #Devoxx @ramtop Thanks! https://www.pexels.com for the pictures If interested follow

    me on Twitter and Medium (did I mention the book?) Uberto Barbini @ramtop medium.com/@ramtop