Using tagless final, MyApi
and any dependencies don't need to be written against the full interface of IO
, only the parts that they use, here Async
.
By putting the implicit def
default instance in the MyApi
companion object, the instance is made available automatically without needing an explicit import statement. The compiler looks for implicit declarations in import statements first, so by putting the def in the companion we allow for overriding it in tests at a local level with a fake of our choosing, for example. As all dependecies are declared by the context refs: [F[_]: Dependency]
, which are also defined in their companion objects, we get DI wiring for free.
In Main we call stream, providing the type of effect (cats.effect.IO
) to use for our application, which doesn't have to be aware of all of the methods of IO, internally.
This means that we can switch Main out to use a different effect type (Monix Task, or scala.concurrent.Future for example) without having to change any of our program's internals should we decide to do so in the future, and our context bounds will give us compiler errors if that effect type is incompatible with our program's definition.
The notation looks complex, so a quick breakdown is as follows
F[_] -- Any type that takes exactly one other type. A box named `F`.
[F[_]: N: N1: ... NN] - A box named `F` that has implicit
dependencies in scope of type N[F], N1[F], ..., NN[F]
(implicit x: MyType) - A dependency on an implicit instance of MyType --
used for dependencies that are not side-effectful. That is
operations that cannot throw an error, cannot be null, and
do not write to disk or read from standard input or write to standard output.
Often, these are implemented as typeclasses -- that is a generic interface that
takes a concrete type rather than a type constructor or box (An A rather than a F[_]).
Typeclass dependencies gives us the same flexibility of implementation for regular type
dependencies that tagless final gives us for effectful dependencies
The advantage of using this style is that we are just implementing standard interfaces, and using the features of scala 2 to do our dependency injection.