Search code examples
scalafunctional-programmingpure-function

What is the best practice for Handling Log message with using functional way


I'm new in scala programming. I'm puzzled now for how to declare a biz method in a asynchronous and functional way, that the method implementation should include many log message. As a bad practice, I write the code like this :

// trait
trait StoreService {
    def create[Config]: Kleisli[Future, Config, Store]
}

// and the interpreter
trait StoreServiceInterpreter extends StoreService {
    def create[Config]: Kleisli[Future, Config, Store] = Kleisli {cfg => 
        // some implementation ...
        log.info("bla bla bla ...")
        // some implementation ...
        // return a store
        Store(...)
    }
}

It's bad, cause the implementation is with side-effect, log something int the place. So, I change the method declaration like this:

// trait
trait StoreService {
    def create[Config]: Kleisli[Future, Config, Writer[Vector[String], Store]]
}

// and the interpreter
trait StoreServiceInterpreter extends StoreService {
    def create[Config]: Kleisli[Future, Config, Writer[Vector[String], Store]] = Kleisli {cfg => 
        // some implementation ...
        // log.info("bla bla bla ...")
        // some implementation ...
        // return a store
        Writer(Vector("bla bla bla...", Store(...))
    }
}

Using the Writer, the side-effect is eliminated, but the code is not clear:

  • Why a writer returned? Writer[Vector[String], Store] has more noises than Store, has any way to avoid the boilerplate code and remain the no-side-effect?
  • Write log is not ad-hoc! I should build a vector of String to hold the message, using :+ or ++ operation to add log. I think it's not ad-hoc logging, just like write log.info(...) anywhere.

Solution

  • Most of Scala developers I know tend to consider logging as "non-side-effect", for convenience. However, if you really want to track them, you might want to take a look on "free monad" concept. Further information: general description, example with logging.

    Mine rough explanation is "let's model our program as some AST and interpret it". So, in AST you define a concept of "logging" but not implementation, that comes later, in interpretation. This approach allows you to keep an eye on logging and change the action (from write to /dev/null to async post to external service) without affecting the "business" part of your code.