Skip to content

Instantly share code, notes, and snippets.

@jackcviers
Last active March 22, 2021 20:00
Show Gist options
  • Save jackcviers/079e67828318c548d9c6112147bce2ba to your computer and use it in GitHub Desktop.
Save jackcviers/079e67828318c548d9c6112147bce2ba to your computer and use it in GitHub Desktop.

Tagless Final with cats-effect and fs2

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.

import cats.effect.Async
import cats.syntax.functor._
import cats.syntax.flatMap._
import Dependency1
import Dependency2
import JavaAsyncLibProvider
import java.util.function.BiConsumer
import MyThingConverter
import MyThingConverter.ops._
abstract class MyApi[F[_]]{
def getMyThingFromFoo(foo: Foo): F[MyThing]
}
object MyApi{
def apply[F[_]: MyApi]: MyApi[F] = implicitly
implicit def defaultInstance[F[_]: Async: Dependency1: Dependency2: JavaAsyncLibFactory](implicit toMyThingConverter: MyThingConverter[ResourceResponse]: MyApi[F] = new MyApi[F]{
override def getMyThingFromFoo(foo: F[oo): F[MyThing] = for{
bar <- Dependency1[F].getBarFromFoo(foo)
baz <- Dependency2[F].getBazFromBar(bar)
asyncJavaClientResource <- JavaAsyncLibFactory[F].client
mything <- Async.async{ cb: (Either[Throwable, Int] => Unit) =>
val request = asyncJavaClientResource.use{ client =>
client.getResource(baz.myKey).wehenCompleteAsync(
new BiConsumer[ResourceResponse, Exception]{
override def accept(response:ResourceResponse, err:Error): Unit = Option(error)
.map[Unit] { t =>
cb(Left(t))
}
.getOrElse(cb(Right(response.itsInt)))
}
}
}.map{ r => r.toMyThing}}
} yield myThing
}
object ops{
implicit class FooOps(val underlying:Foo){
def getMyThingFromFoo[F:MyApi]: F[MyThing] = MyApi[F].getMyThingFromFoo(foo)
}
}
}
// in WhatMainCalls.scala
import MyApi.ops._
object class WhatMainCalls{
def stream[F[_]: MyApi]: fs2.Stream[F, Nothing] = fs2.Stream[F, Foo](Foo("hi")).evalMap(f => f.getMyThingFromFoo[F])
}
//in Main.scala
import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._
object Main extends IOApp {
def run(args: List[String]) =
WebsiteServer.stream[IO].compile.drain.as(ExitCode.Success)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment