turbolift
Type members
Classlikes
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
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
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.
-
For effect requests, Effect provides syntax for invoking effect's operations. The syntax is defined using perform method.
-
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
.
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 intoResult[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
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 typeX
.
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
Alias for Computation type. Meant to be used in infix form.
Alias for Computation type. Meant to be used in infix form.