Finally Tagless:

The cake is a lie

Dependency injection

  • DI strategies have three components: services, objects and programs
  • Objects implement services, and programs use services
  • All of them can have disparate implementations
  • They also depend on each other in specific ways

DI in Scala

  • OO: Compile-time annotation frameworks, runtime annotation frameworks, constructor/parameter injection
  • FP: Free structures, finally tagless structures (dual)
  • Scala: Cake
  • Each has different notions of what an "operation" is, and how to bundle operations into a "service"

Dependencies

  • Services provide other services 
  • Objects depend on services
  • Objects use programs that require any of the services they depend on or implement
  • Programs should be able to depend on services
  • Programs should be able to use other programs that use subsets of the same services

Cakes

  • Services are traits full of methods that, to be implemented, *must* side-effect or keep internal state (optionally accompanied by component definition composing multiple services)
  • Objects depend on services using self-type annotations
  • Object definition amounts to defining a trait inheriting from the implemented services' traits
  • The only programs that exist are methods on the concrete cake

Parameters

  • Services: a trait full of (probably) side-effecting methods keeping (probably) internal state
  • Objects: defining a concrete subclass of the traits describing the services satisfied by the object  
  • Dependencies are declared using constructor parameters *or* function parameters
  • Programs are thus either public members of the class or the functions themselves

Free Structures + Algebras

  • Services: initial algebras, a pure-data representation of operations provided by the service
  • Objects: interpreters, which can output free programs themselves (horizontal object composition)
  • Dependencies: interpreters which output other algebras, to be interpreted yet later (horizontal object composition)
  • Programs: free structures over a typeclass for program composition, with coproducts for multiple service dependencies

Benefits of the Free approach

  • Explicit programs can be transformed; for example, adding compression to a program that touches a key/value-storage
  • Not all programs are naturally expressed as monads: the free construct + algebra approach generalizes to functors, applicatives, monoids and recursion

Downsides of the Free approach

  • Explicit programs must be allocated; this typically takes the form of an allocation per service operation and an allocation per composition of the operation
  • Programs are deconstructed with pattern matching, which does not scale for large match statements and in Java is capable of cache thrashing

Finally Tagless

  • Services: traits with type parameters, methods inside
  • Objects: classes/objects implementing traits (classes provide horizontal object composition)
  • Programs: functions polymorphic over a type parameter, with implicit parameters for service dependencies, as well as typeclass constraints which provide composition

Concretely

trait Log[F[_]] { self =>
  def info(out: String): F[Unit]
  def warn(out: String): F[Unit]
}

final class NoisyLog[F[_]](log: Log[F[_]]) {
  def info(out: String): F[Unit] = log.warn(out)
  def warn(out: String): F[Unit] = log.warn(out)
}

object SilentLog extends Log[Task] {
  def info(out: String): Task[Unit] = Task.eval(())
  def warn(out: String): Task[Unit] = Task.eval(())
}

object JavaLog extends Log[Task] {
  def info(out: String): Task[Unit] = Task.eval(System.out.println(out))
  def warn(out: String): Task[Unit] = Task.eval(System.err.println(out))
}

object Main {
  def mkLog(silent: Boolean, noisy: Boolean): Log[Task] = 
    if (silent) SilentLog
    else if (noisy) NoisyLog(JavaLog)
    else JavaLog

  implicit val log = mkConfig(silent = false, noisy = false)
}

Free -> Final Tagless

  • Idiom: passing a Free program to a function, which interprets the program in a way unknown to the caller
  • Replacement: passing finally tagless actions
// finally tagless action constrained with Constraint
// that uses the service Service
trait Action[Service[_[_]], Constraint[_[_]], A] {
  def apply[G[_] : Constraint: Service]: G[A]
}

Good uses of Free, as a special case of finally tagless

  • Free provides a means to describe a call tree as data
  • Testing that a call tree has a particular shape thus is particularly well suited to Free
  • Thus, Free provides a purely functional alternative to mocking libraries

Good uses of Free, as a special case of finally tagless

  • Free structures (at least some form of them) are also serializable, and so can be used in producer/consumer arrangements with intermediate queues

Good uses of Free

  • Other free structures can offer performance advantages in some situations over the direct finally tagless approach
  • Monoids, Monads, Functors can all benefit from their Free varieties

Horizontal object composition

  • Horizontal object composition amounts to running a program with several service implementations, "concurrently"
  • Note that actual concurrency is not related, but the actions are interleaved into a single program call tree

Horizontal object composition

  • Free interpreters can be horizontally composed using products of natural transformations
  • Finally tagless objects can be horizontally composed in the same way, using the Cartesian typeclass
  • Both of these are useful because they traverse the call tree once while running many interpreters and receiving many values
trait Cartesian[F[_]] {
  def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}
def and[F[_], G[_], H[_]]
    (first: F ~> G,
     second: F ~> H): F ~> Prod[G, H, ?] =
      Lambda[F ~> Prod[G, H, ?]].apply(fa => Prod(first(fa), second(fa)))

Title Text

Subtitle

Modular Architecture

Finally Tagless:

By edmundnoble

Finally Tagless:

  • 106