Final Tagless seen alive

For Main, we only need sequential composition: describing an operation which first runs one effectful computation, and then a second one.

This is what a Monad gives us.

For LiveConsole, we need a way of wrapping side-effecting computations.

This is what Sync from cats-effect (if we are using cats) represents.

Our code now becomes (class Main[F[_]: Monad] is just a shorthand notation for class Main[F[_]](implicit fm: Monad[F])):trait Console[F[_]] { def putStrLn(line: String): F[Unit] def getStrLn: F[String]}class LiveConsole[F[_]: Sync] extends Console[F] { def putStrLn(line: String): F[Unit] = Sync[F].

effect(println(line)) def getStrLn: F[String] = Sync[F].




readLine())}class Main[F[_]: Monad](console: Console[F]) { def run(): F[String] = { for { _ <- console.

putStrLn("Good morning, what’s your name?") name <- console.

getStrLn _ <- console.

putStrLn(s"Great to meet you, $name") } yield name }}And that’s the whole idea behind Final Tagless — instead of using concrete effectful wrapper, we declare what kind of interface is needed for the wrapper in a particular class or method.

Final Tagless as a way to track effectsWe can extend the idea presented above to track what kind of side effects our code uses in more detail.

Scala gives us quite a wide range of possibilities here.

The only question is: how fine-grained the effect tracking should be?Of course: it depends!First, we have the option to track no effects at all.

That’s what we’ve seen in the very first code snippet: the LiveConsole implementation was just doing uncontrolled and unconstrained side effects (in this case, printing/reading from the console).

Improving on this, we can track side effects at a binary level: “has effects” or “has no effects”.

Looking at a method signature, we know if it’s declared as being pure (e.


f: List[User] => Statistics) or if it is declared to have side effects (f: List[User] => IO[SentEmails]).

Making this jump, from tracking no effects at all, to a has effect/no effect distinction is what makes the biggest difference in most code bases.

And in many cases you can stop here.

However, if you want to, you can go further, and track in the method signatures what kind of effects exactly does a method use.

IO means “some effect”, while you could want to know — does it mean using a database?.Sending emails?.Interacting with the console?That’s what various effect systems in Scala want to solve.

And that’s also what ZIO Environment is about.

And it’s not the only proposed possibile solution; contenders in this space include:the reader monadfinal taglessfree monadsimplicit function types in Dotty/Scala 3and now ZIO EnvironmentAs a side note: ZIO Environment is not about “injecting dependencies”.

Dependency injection and tracking effects are distinct things.

The first one, dependency injection, is about creating a static object graph (module graph), where the dependencies are hidden from the use sites.

Effect tracking is about making dependencies explicit to the use sites; dependencies become part of the interface.

That’s why the reader monad is not an alternative to dependency injection, but can be a complement of it.

How to extend Final Tagless to track effects?.Just as John has shown in his example.

Instead of passing Console[F] as a constructor parameter to Main, we require it as another constraint for our effect wrapper F on the method:trait Console[F[_]] { def putStrLn(line: String): F[Unit] def getStrLn: F[String]}object Console { def apply[F[_]](implicit F: Console[F]): Console[F] = F}class LiveConsole[F[_]: Sync] extends Console[F] { def putStrLn(line: String): F[Unit] = Sync[F].

effect(println(line)) def getStrLn: F[String] = Sync[F].




readLine())}object Main { def run[F[_]: Monad: Console](): F[String] = { for { _ <- Console[F].

putStrLn("Good morning, what’s your name?") name <- Console[F].

getStrLn _ <- Console[F].

putStrLn(s"Great to meet you, $name") } yield name }}While before the console dependency was hidden from Main’s users, now it is explicit.

The dependency must be provided by the caller of the method, not when creating the object graph!.Hence, each use-site also needs to have the Console dependency, and so on.

We are now clear that Main.

run has side effects (as it uses an effect wrapper at all) and additionally what kind of side-effects exactly (it interacts with the console).

Note that the : Sync and : Monad constraints are different in their nature from the : Console constraint.

The first ones describe the capabilities that the F effect wrapper has, and form lawful, “true”, typeclasses.

The second doesn’t say anything new about F, it just constraints the possible side-effects.

Such constraints shouldn’t be considered a typeclass in the first place (which is also one of John’s points).

Is Final Tagless a good way to track effects?It might be —or it might be not.

All of the criticism of Final Tagless from John’s talk is of course valid, however it applies only to using Final Tagless for effect tracking — not constraining effect wrappers!Is ZIO Environment the answer?.It might be — if you need to track effects in your application!.Or, it might as well be the case that just knowing if a function has side effects or not is sufficient; that is, using “plain old” IO[_] (or an abstract F[_]).

We all know what are the shortcomings of Final Tagless, thanks to John’s excellent talk.

But we still have to wait for field reports of using ZIO Environment in real projects.

Some of the potential shortcomings have already been pointed out by Oleg Nizhnik, so I will refer you to this twitter thread instead of repeating them here.

What I would add, is that the necessity to use the cake pattern (let’s call it what it is 🙂 ) is quite an intrusive change and might require adapting your codebase.

It’s not as simple as just using a “plain old” class with methods.

Do you have to be a functional programming expert to understand all this?No.

As John says in his talk, you don’t need to understand what a trifunctor is, why ZIO[R, E, A] forms a profunctor on its R/E and R/A type parameter pairs, or what a profunctor even is to use ZIO.

I would argue that the same is true for Final Tagless: to use it as a way to constrain effect wrappers, you don’t need to have a thorough understanding of type classes, higher kinded types or the Monad typeclass hierarchy.

It’s sufficient to understand why coding to an interface, instead of an implementation is preferable.

And being open to alternative ways of expressing dependencies (in this case: not as a parameter, but as an implicit type constraint).

It’s a very interesting quest to find out how these concept generalise; what a typeclass is; how pervasive monads are; how applicatives differ from monads; how free is equivalent to tagless final; what’s a profunctor; etc.

But that’s not necessary to start using them.

Fashion?Final Tagless is definitely a victim of hype and fashion.

It shouldn’t be used for everything, as that’s how we end up with monstrosities such as we’ve seen in John’s talk:def genFeed[F[_]: Monad: Logging: UserDatabase: ProfileDatabase: RedisCache: GeoIPService: AuthService: SessionManager: Localization: Config: EventQueue: Concurrent: Async: MetricsManager]: F[Feed] = ???A couple of years ago Free Monads were the fashionable thing to use in the functional programming community.

Using them as the main way to structure programs turned out to be cumbersome, the amount of boilerplate needed greatly exceeded benefits they bring.

However, Free Monads have found their niche.

They turn out to be a very good choice for representing general-purpose abstractions.

For example, when describing database operations (ConnectionIO from Doobie or DBIOAction from Slick), or when describing concurrent programs (IO in ZIO, Task in Monix, Behavior in Akka-Typed).

For these kinds of abstractions, it’s just much more convenient to work with a value-based representation.

Yes, IO/Task itself is another realisation of the “code to an interface” idea.

When describing computation using IO, you create a description of the computation (as a value), using IO’s primitives.

Only later that is being interpreted, that is, the IO primitives are given specific meaning.

In theory, you can have multiple IO interpretations, which correspond to multiple interface implementations.

The niche for Final Tagless is constraining effect wrappers.

But not — as John’s talk shows quite well — tracking effects in detail in an application.

I’m quite sure a niche will emerge for ZIO Environment.

Such as situations were you need detailed effect tracking.

Wrapping upAll tools have their proper use.

Neither Final Tagless, Free Monads, constructor-based dependency injection nor ZIO Environment should be used for everything.

There’s many ways in which you can realise the “code to an interface” idea in Scala.

In each of them, you have a basic “set of instructions” which you use to build your application logic.

These instructions can take various forms:calling methods on dependencies passed as parameters (constructor based dependency injection)final tagless: dependencies are passed as implicit capabilitiesfree monads: instructions are represented as valuesreader monad: a convenient way of passing a single dependencySumming up, what ZIO Environment is: an interesting combination of an optimized reader monad with the cake pattern.

What Final Tagless isn’t: dead.


. More details

Leave a Reply