turbolift

package turbolift

Type members

Classlikes

sealed trait Computation[+A, -U]

Monad of extensible effects. Use the !! infix type alias instead.

Monad of extensible effects. Use the !! infix type alias instead.

For example:

type MyComputationType1 = String !! (MyState & MyError)

type MyComputationType2 = String !! Any

MyComputationType1 is a type of computations that return String and reqest 2 effects: MyState and MyError.

MyComputationType2 is a type of computations that return String and reqest no effects (type Any means empty set).


All requested effects must be handled (discharged from the computation), by using Handlers, before the result can be obtained as a plain (non monadic) value.

To handle some or all requested effects, use handleWith:

val myComputation2 = myComputation.handleWith(myHandler)

As soon as all effects are handled, the result can be obtained with run:

val result = someComputation
  .handleWith(someHandler1)
  .handleWith(someHandler2)
  .handleWith(someHandler3)
  .run
Type parameters:
A

Result type of the computation

U

Type-level set of effects, expressed as an intersection type, that are requested by this computation. Type Any means empty set.

Companion:
object
object Computation extends ComputationExtensions with ComputationInstances

Use the !! alias to access methods of this companion object.

Use the !! alias to access methods of this companion object.

Example:

val myComputation: Int !! Any = !!.pure(42)
Companion:
class
trait Effect[Z <: Signature] extends CanPerform[Z] with CanInterpret

Base trait for any user-defined effect.

Base trait for any user-defined effect.

An instance of Effect serves as a bridge, between effects request and effect handlers.

  1. For effect requests, Effect provides syntax for invoking effect's operations. The syntax is defined using perform method.

  2. For effect handlers, Effect provides environment for implementing an Interpreter, which can be subsequently transformed to a Handler.

Typically, a custom defined Signature is 1-1 paired with a custom defined Effect.


Usage

Assuming the following Signature defined:

import turbolift.Signature

trait GoogleSignature extends Signature:
  def countPicturesOf(topic: String): Int !@! ThisEffect

...then, the corresponding Effect should look like this:

import turbolift.{!!, Effect}

trait Google extends Effect[GoogleSignature] with GoogleSignature:
  final override def countPicturesOf(topic: String): Int !! this.type = perform(_.countPicturesOf(topic))

⚠️ Google uses its GoogleSignature twice: first as the type parameter, and second as the super trait.

⚠️ Effect trait finally-overrides !@! as !!, and ThisEffect as this.type. The (re)definintion of countPicturesOf in Google uses those overrides. This is not necessary, but it improves readability of error messages.

Effect instance defines unique identity for the effect, both in type and value spaces. In order for our Google to be usable, such instance must be made accessible. Assuming global scope:

case object MyGoogle extends Google   // unique value
type MyGoogle = MyGoogle.type         // unique type (Scala's singleton type)

The type alias type MyGoogle is for convenience only.

Now, we can finally invoke the effect's operations:

val myComputation: Int !! MyGoogle = MyGoogle.countPicturesOf("cat")

⚠️ Unlike in most effect systems in Scala and Haskell scene, in Turbolift it is possible to have more than 1 instance of given effect, and even use them simultaneously in the same computation. Turbolift's runtime will treat them as completely separate effects, with each expecting a separate handler instance.

Related reading: Labelled Effects in Idris. In Idris, the label is optional. In Turbolift, effects are always "labelled". Scala's dependent typing can be used to write code polymorphic over effect's identity. Such as a Handler, that can handle any instance of Google effect. This is how default handlers for standard effects (Reader, State, etc.) are implemented.

Example of 2 instances of our Google effect:

case object MyGoogle1 extends Google
case object MyGoogle2 extends Google
type MyGoogle1 = MyGoogle1.type
type MyGoogle2 = MyGoogle2.type

val myComputation: Int !! (MyGoogle1 & MyGoogle2) =
  for
    a <- MyGoogle1.countPicturesOf("cat")
    b <- MyGoogle2.countPicturesOf("dog")
  yield a + b

If, for some reasons, this property is undesirable, we can hardcode the effect to be limited to 1 instance forever. All we need to do, is to replace trait Google with case object Google:

trait Google extends Effect[GoogleSignature] with GoogleSignature:
  final override def countPicturesOf(topic: String): Int !! this.type = perform(_.countPicturesOf(topic))

...with:

case object Google extends Effect[GoogleSignature] with GoogleSignature:
  final override def countPicturesOf(topic: String): Int !! Google = perform(_.countPicturesOf(topic))

type Google = Google.type

⚠️ Even though we have just limited the number of Google effect instances to 1, we can still define multiple Handlers for Google.

Type parameters:
Z

The Signature of this effect.

Companion:
object
object Effect
Companion:
class
sealed trait Handler[Result[_], Elim, Intro]

Handler is an object used to transform a Computation, by discharging some or all of its requested effects.

Handler is an object used to transform a Computation, by discharging some or all of its requested effects.

For example, having:

val myComputation2 = myComputation1.handleWith(myHandler)

...then, myComputation2 will have the type of myComputation1, modified as follows:

  • Elim effects will be removed from the set of requested effects.
  • Intro effects (if any) will be inserted to the set of requested effects.
  • The result type A, will be transformed into Result[A].

Handlers can be obtained in 3 ways:

  • By implementing an Interpreter for an Effect, and then transforming it into a Handler.
  • By transforming a preexisting handler, e.g: val myHandler2 = myHandler1.map(...)
  • By composing 2 preexisting handlers, e.g: val myHandler3 = myHandler1 &&&! myHandler2
Type parameters:
Elim

Type-level set of effects, expressed as an intersection type, that this handler eliminates from the computation.

Intro

Type-level set of effects, expressed as an intersection type, that this handler introduces into the computation. This is often an empty set, expressed as Any.

Result

Type constructor (e.g. Option[_]), in which the computation's result is wrapped, after application of this handler. This is often an identity.

Companion:
object
object Handler extends HandlerExtensions

Defines convenience extensions and type aliases for Handler.

Defines convenience extensions and type aliases for Handler.

Companion:
class
object Implicits extends AllOrphanedExtensions
trait Signature extends AnyRef

Base trait for any user-defined effect signature.

Base trait for any user-defined effect signature.

Effect signature is a trait, where the effect's operations are declared as abstract methods.

Typically, a custom defined Signature is 1-1 paired with a custom defined Effect.

Effect signatures play the same role as:

  • Algebras in Tagless Final.
  • Services in ZIO.

Example:

import turbolift.Signature

trait GoogleSignature extends Signature:
  def countPicturesOf(topic: String): Int !@! ThisEffect

Effect operations must:

  • Be defined as abstract methods.
  • Have their return types of shape: X !@! ThisEffect, for some type X.

It may be helpful to think of !@![_, ThisEffect] as analogous to F[_] in Tagless Final. Except in Turbolift, it's meant to be used in the return type only.

In the parameters, plain !! should be used. Example of scoped operation:

trait ErrorSignature[E] extends Signature:
  def catchError[A, U <: ThisEffect](scope: A !! U)(f: E => A !! U): A !@! U

Types

type !![+A, -U] = Computation[A, U]

Alias for Computation type. Meant to be used in infix form.

Alias for Computation type. Meant to be used in infix form.

Value members

Concrete methods

def !!: Computation.type

Alias for Computation companion object.

Alias for Computation companion object.