So, here goes my code snippet thinking. Looking at the problem, it feels like a program should return another program. In fact, in our case we have lots of instances of "program" and we need to return/pass these programs.
So lets go ahead and create a program that should be returned. In our case, the main one is Notification.
{
sealed trait Notification[A]
case class SendEmail(s: String) extends Notification[Unit]
case class SendMessage(s: String) extends Notification[Unit]
case class NothingJustLog(s: String) extends Notification[Unit]
}
I am yet to worry about whether its a Free Monad or any of those boiler plates. Needn't lift to Free just because it is looking like Free. (Although in real life, yeah lifting becomes useful most of the times)
I have a Table
. For the sake of simplicity (or complexity), let's assume table has a function that returns a program under an effect.
private def someInternalPureUnknownService(s: String): Option[Int] = Some(100)
// Consider your favorite F to be F =:= Option
def write(s: String): Option[Notification[Unit]] =
someOtherService(s).map { t => {
if (t == 100)
SendEmail(s)
else if (t < 100)
SendMessage(s)
else
NothingJustLog(s)
}
}
// Great. Let me test write Operation. I am so glad, no mocking of notification
write("afsal") === Some(SendEmail("afsal")) // Thats it
I have a real-world interpreter for notification algebra
def notify: Notification ~> scalaz.Id.Id = new (Notification ~> scalaz.Id.Id) {
def apply[A](fa: Notification[A]): scalaz.Id.Id[A] =
fa match {
case SendEmail(k) => println("Sent email to "+ k)
case SendMessage(k) => println("Sent message to " + k)
case NothingJustLog(k) => println("Just logging" + k)
}
}
// Let's test this interpreter
notify(SendEmail("afsal")) prints (go check the inbox!..
Well mostly you don't need to test an external library that does the mail/msg/logging .. Anyways)
Ok, now let's actually send a notification based on write operation. It is as easy as !
write("afsal").map(notify)
Please see the Free monad bits of the above solution in here
Advantages
- Pure separation of concerns between Table and Notification algebras.
- Differed the
notification
operation during atable
operation. Testing Table algebra becomes easy because I can pattern match on the final result and see what it "will" do in future. - Avoided dependency injection of
Notification Algebra
in the interpreter (implementation) ofTable
. (considering Table is just an interface). If you passed notification algebra as dependency injection to interpreter of Table, Finally Tagless is better. Finally tagless is better for inheritance and dependency injection. - Separately testing notification.
- Depending on the context where
SendEmail
exists (SendEmail during WriteOperation can be different from SendEmail during ReadOperation)
Now please consider doing exactly the same(without changing the usecase) with Finally tagless. It is possible, but remove the cap of Free monad thinker and solve it using finallytagless!
trait Notification[F[_], A] {
def sendEmail(s: A): F[Unit]
def sendMessage(s: A): F[Unit]
def log(s: A): F[Unit]
}
// Solution 1: Hmmm looks complex
trait SuperFunctionalTable[F[_]] {
// Write operation writes the user and based on certain logic, decides how to send a notification under an effect F.
def writeOperation(s: String): F[String => F[Unit]]
}
// Solution 2: Hmm looks a little more simpler
trait FunctionalTable[F[_]] {
// Write operation writes the user and based on certain logic, decides how to send a notification.
def writeOpetation(s: String): F[String => Unit]
}
// Solution 3: Way simpler
trait Table[F[_]] {
// Write operation writes the user and based on certain logic, send the notification. Hurray...
// Go and check the mail inbox if `writeOperation` worked.
def writeOperation(s: String): F[Unit]
}
// Solution 4: Super simpler
trait TableSolo[F[_]] {
// No i am not sending an email. i will write this one out. I am not concerned at all about notification. But return me the string that is to be passed to the next algebra.
def writeOperation(s: String): F[String]
}
trait ValidateAndSendNotification[F[_]] {
def sendNotification(s: String): Notification[F, String] => String
// So we got rid of dependency injection through Kleisli like operation (function).
// This seems to be the best of all solutions in finally tagless.
// However we still need to test each sink of mock notification to see if was mail, msg, or logging.
// Oh by the way, We could have a validation layer in Free algebra as well. At this
// stage I am convinced how easy is my implementation with Free !
}