A problem I had with Free interpreters was seemingly being unable to call functions defined inside any given interface from sibling members to the same interface.
Recently, I converted https://github.com/twilio/guardrail to Tagless Final, and in so doing discovered a way to use sibling functions while preserving the ability to provide different implementations of those functions.
The following is a sample, note that the desired behaviour is that my
overridden function in second
is the desired behaviour for all attempted
invocations, and in production code would be explicitly brought into scope
right before I execute my initial function.
abstract class FooInterface[F[_]] {
def doFoo(): F[Unit]
def doBar(): F[Unit]
def doBaz()(implicit impl: FooInterface[F]): F[Unit]
}
object first extends FooInterface[cats.Id] {
def doFoo() = println(s"first")
def doBar() = doFoo()
def doBaz()(implicit impl: FooInterface[F]) = impl.doFoo()
}
implicit object second extends FooInterface[cats.Id] {
def doFoo() = println(s"second")
def doBar() = first.doBar()
def doBaz()(implicit impl: FooInterface[F]) = first.doBaz()
}
first.doBar() // prints first
second.doBar() // also prints first
// compare with:
first.doBaz() // prints second
second.doBaz() // also prints second
This is useful, as so long as you make the implementation you want the only implicit interpreter, you won't accidentally completely embed one implementation without giving the ability to override.