Skip to content

Instantly share code, notes, and snippets.

@pvillega
Created February 27, 2019 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pvillega/1e39b1e68c27ee242ec26baab88c98eb to your computer and use it in GitHub Desktop.
Save pvillega/1e39b1e68c27ee242ec26baab88c98eb to your computer and use it in GitHub Desktop.
On 'Tagless final death'
/**
I've read http://degoes.net/articles/zio-environment and I am confused.
Note this may be me not understanding this, it wouldn't be the first time I miss the obvious point on something
But I wonder about the supossed benefits, and I am not sure I see them all.
Note that I am not trying to bash John's post, just understand what I am missing from it.
Also, disclaimer, I like MTL but I am open to any improvements :)
Let's discuss the points raised as negative about Tagless Final first:
1) Knowledge ramp up:
As I see them, they are basic concepts to do FP in Scala. You'll hardly do FP without knowing (at a basic level) typeclasses.
Or functional effects. Some, like the monad hierarchy, may not be needed to start using the technique at all, and explained
when you need them.
I am not convinced any FP technique/tool, including ZIO, doesn't have similar ramp up.
2) Type class abuse
I can agree we are twisting the 'concept' so we can reduce boilerplate, but is that so bad?
I see it a bit like 'Try is not a monad'. Sure, it is not, but it is useful sometimes to use it like a Monad.
3) Big bang refactor
Yes, I agree it is ideal to convert the full codebase to this style. This may be the more compelling point.
That said, I understand if you are moving a codebase to MTL style probably that codebase is not 'so pure' and FP compliant.
So is there that much harm to select a point in your services that 'runs the IO' and all the logic underneath uses MTL?
Something like:
**/
class MyService() {
def myRefactoredMethod = program[IO].unsafeRunSync()
}
/**
Yes, it is not FP and not pure, but if your codebase is already very pure adding MTL/other FP techniques shouldn't be too complex
as you may be already using them. If you have to migrate, you'll have to go incremental anyway...
4) Repetition (both tedious and stubborn)
It may be that I am working in the wrong codebases, and I am misunderstanding the issue. But I wonder, as the blog post seems to show
the solution to this. Assume we want:
**/
def program[F[_]: Monad: Console: B : C] = ???
// Where we have the following traits/typeclasses:
trait Console[F[_]] {
def putStrLn(line: String): F[Unit]
val getStrLn: F[String]
}
object Console {
def apply[F[_]](implicit F: Console[F]): Console[F] = F
}
implicit val ConsoleIO: Console[IO] = new Console[IO] {
def putStrLn(line: String): IO[Unit] =
IO.effect(println(line))
val getStrLn: IO[String] =
IO.effect(scala.io.StdIn.readLine())
}
trait B[F[_]] {
def b(): F[String]
}
object B {
def apply[F[_]](implicit F: B[F]): B[F] = F
}
implicit val BIO: B[IO] = new B[IO] {
def b(): IO[String] = IO("B")
}
trait C[F[_]] {
def c(): F[String]
}
object C {
def apply[F[_]](implicit F: C[F]): C[F] = F
}
implicit val CIO: C[IO] = new C[IO] {
def c(): IO[String] = IO("C")
}
/**
and we want to avoid propagating the same list across all signatures to avoid long copy-pasted lists.
Why can't we just create this shareable stack:
**/
abstract class HasStack[F[_]: Monad] {
// we need a Monad, or applicative, or...
implicit val monad = implicitly[Monad[F]]
def console: Console[F]
def b: B[F]
def c: C[F]
}
implicit val HasStackIO: HasStack[IO] = new HasStack[IO] {
def console: Console[IO] = Console[IO]
def b: B[IO] = B[IO]
def c: C[IO] = C[IO]
}
object HasStack {
def apply[F[_]](implicit F: HasStack[F]): HasStack[F] = F
}
//and then at the edges of our program we can do:
def program2[F[_]](implicit H: HasStack[F]): F[String] = {
import H._
for {
_ <- console.putStrLn("Good morning, what's your name?")
name <- console.getStrLn
_ <- console.putStrLn(s"Great to meet you, $name")
_ <- b.b()
} yield name
}
val program2IO: IO[String] = program2[IO]
/**
This allows us to share the full stack across most of the calls. When I have a stack the main
areas of duplication are usually:
- outside boundaries when everything is being wired
- some particular vertical (module) in the stack that may be sharing a set of constraints, as in John's example
On both cases we can build these agregators for these areas to reduce repetition.
We can compose these stacks (via extends) if needed.
And we can keep other methods that don't need to know about much more than Monad or Async at that level,
without having to pass them the full stack.
5) Fake parametric guarantess
The main critique from FP people towards Scala is that you may run side effects at any point, due to its syntax.
I don't see how ZIO avoids this, you could do the same inside any of the services set as examples, unless I am missing something.
I understand this is just an improvement (performance wise) over a ReaderT with IO, as mentioned in the post, with better performance.
But I just don't see it as such a revolution over Tagless final.
So, I am consufed. Seeing everyone so excited, I am sure I am missing something crucial. What am I missing?
**/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment