Modules

Freestyle @module serves the purpose of logically combining related Algebras that are frequently used together. In the same way that architectures are traditionally built in layers where separation of concerns is paramount, Modules help organize algebras in groups that can be arbitrarily nested.

Let’s first define a few algebras to illustrate how Modules work. We will start with some basic low-level style ops related to persistence. In our Persistence related algebras, we have a few ops that can go against a DB and others to a Cache service or system. On the presentation side, an application can display or perform input validation.

import freestyle._
// import freestyle._

object algebras {
    @free trait Database {
      def get(id: Int): FS[Int]
    }
    @free trait Cache {
      def get(id: Int): FS[Option[Int]]
    }
    @free trait Presenter {
      def show(id: Int): FS[Int]
    }
    @free trait IdValidation {
      def validate(id: Option[Int]): FS[Int]
    }
}
// defined object algebras

At this point, we can group these different application concerns in modules. Modules can be further nested so they become part of the tree that conforms an application or library:

import algebras._
// import algebras._

object modules {
    @module trait Persistence {
      val database: Database
      val cache: Cache
    }
    @module trait Display {
      val presenter: Presenter
      val validator: IdValidation
    }
    @module trait App {
      val persistence: Persistence
      val display: Display
    }
}
// defined object modules

This enables one to build programs that are properly typed and parameterized in a modular and composable way:

import modules._
// import modules._

def program[F[_]](
	implicit
	  app: App[F]): FreeS[F, Int] = {
  import app.display._, app.persistence._
  for {
    cachedToken <- cache.get(1)
    id <- validator.validate(cachedToken)
    value <- database.get(id)
    view <- presenter.show(value)
  } yield view
}
// program: [F[_]](implicit app: modules.App[F])freestyle.FreeS[F,Int]

The @module annotation works in a similar way as the @free annotation, by generating all the boilerplate and implicit machinery necessary to assemble apps and libraries based on Free.

Automatic method implementations

From the abstract sub-module references, Freestyle creates implementations based on the implicit evidence of the members it finds declared. When App is expanded in the case above, a companion object will be generated that includes a class definition implementing App[F[_]], where both persistence and display are based on the implicit evidence generated by @free and other @module annotations.

Dependency Injection

Freestyle automatically generates implicit default instances and summoners in the ‘@free’ and ‘@module’ annotated companions, so that instances can be summoned implicitly at any point in an application. Scala uses implicit instances in companion objects as part of its implicit resolution rules. To learn more about Scala implicits take a look at this great post, Implicit Design Patterns in Scala by Li Haoyi.

This gives the caller an opportunity to override the instances generated automatically by Freestyle at any point with explicit ones.

Freestyle also creates an implicit method which requires implicitly all the module dependencies and a convenient apply method which allows obtaining Module instances in an easy way. As in @free this effectively enables implicits based Dependency Injection where you may choose to override implementations using the implicits scoping rules, to place different implementations where appropriate. This also solves all Dependency Injection problems automatically for all modules in applications that model their layers on modules with the @module annotation.

def doWithApp[F[_]](implicit app: App[F]) = ???
// doWithApp: [F[_]](implicit app: modules.App[F])Nothing

Convenient type aliases

All companions generated with @module contain a convenient type alias Op that you can refer to and that points to the root ADT nodes of the most inner @free leaves. Freestyle recursively walks the Module dependencies until it finds the deepest modules that contain @free smart constructors generating a properly aligned Coproduct for all it’s contained algebras. This allows contained algebras to be composed.

If you were to create this by hand, in the case of the example above, it will look like this:

import iota._
// import iota._

import KList.:::
// import KList.$colon$colon$colon

type ManualAppCoproduct[A] = CopK[
  IdValidation.Op ::: Presenter.Op ::: Cache.Op ::: Database.Op ::: KNil, A]
// defined type alias ManualAppCoproduct

Things get more complicated once the number of Algebras grows. Fortunately, Freestyle automatically aligns all those for you and gives you an already aligned Coproduct of all algebras contained by a Module, whether directly referenced or transitively through its modules dependencies.

implicitly[App.Op[_] =:= ManualAppCoproduct[_]]

So far, we’ve covered how Freestyle can help with building and composing module programs based on Free, but Free programs are useless without a runtime interpreter that can evaluate the Free structure.

Next, we will show you how Freestyle helps you simplify the way you define runtime interpreters for Free applications.