Definition of Transformer.Fallible
and Mode
object Transformer {
trait Fallible[F[+x], Source, Dest] {
def transform(value: Source): F[Dest]
}
}
So a Fallible
transformer takes a Source
and gives back a Dest
wrapped in an F
where F
is the wrapper type for our transformations eg. if F[+x]
= Either[List[String], x]
then the transform
method will return an Either[List[String], Dest]
.
sealed trait Mode[F[+x]] {
type Self[+A] = F[A]
def pure[A](value: A): F[A]
def map[A, B](fa: F[A], f: A => B): F[B]
def traverseCollection[A, B, AColl <: Iterable[A], BColl <: Iterable[B]](
collection: AColl,
transformation: A => F[B]
)(using factory: Factory[B, BColl]): F[BColl]
}
object Mode {
inline def current(using mode: Mode[?]): mode.type = mode
extension [F[+x], M <: Mode[F]](self: M) {
inline def locally[A](inline f: M ?=> A): A = f(using self)
}
}
Moving on to Mode
, what exactly is it and why do we need it? So a Mode[F]
is typeclass that gives us two bits of information:
- a hint for the derivation mechanism which transformation mode to use (hence the name!)
- some operations on the abstract
F
wrapper type, namely:
pure
is for wrapping arbitrary values intoF
, eg. ifF[+x] = Either[List[String], x]
then callingpure
would involve just wrapping the value in aRight.apply
call.
map
is for operating on the wrapped values, eg. if we find ourselves with aF[Int]
in hand and we want to transform the value 'inside' to aString
we can call.map(_.toString)
to yield aF[String]
traverseCollection
is for the cases where we end up with a collection of wrapped values (eg. aList[F[String]]
) and we want to transform that into aF[List[String]]
according to the rules of theF
type wrapper and not blow up the stack in the process
As mentioned earlier, Modes
come in two flavors - one for error accumulating transformations (Mode.Accumulating[F]
) and one for fail fast transformations (Mode.FailFast[F]
):
object Mode {
trait Accumulating[F[+x]] extends Mode[F] {
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}
trait FailFast[F[+x]] extends Mode[F] {
def flatMap[A, B](fa: F[A], f: A => F[B]): F[B]
}
}
Each one of these exposes one operation that dictates its approach to errors, flatMap
entails a dependency between fallible transformations so if we chain multiple flatMaps
together our transformation will stop at the very first error - contrary to this, Mode.Accumulating
exposes a product
operation that given two independent transformations wrapped in an F
gives us back a tuple wrapped in an F
. What that really means is that each transformation is independent from one another so we're able to accumulate all of the errors produced by these.
For accumulating transformations ducktape
provides instances for Either
with any subtype of Iterable
on the left side, so that eg. Mode.Accumulating[[A] =>> Either[List[String], A]]
is available out of the box (under the subclass of Mode.Accumulating.Either[String, List]
).
For fail fast transformations, instances for Option
(Mode.FailFast.Option
) and Either
(Mode.FailFast.Either
) are avaiable out of the box.
As for the purpose of the Self[+A]
type member, it's to enable use cases like these:
import io.github.arainko.ducktape.*
val source =
(
Right(1),
Right("str"),
Right(List(3, 3, 3)),
Right(4)
)
// source: Tuple4[Right[Nothing, Int], Right[Nothing, String], Right[Nothing, List[Int]], Right[Nothing, Int]] = (
// Right(value = 1),
// Right(value = "str"),
// Right(value = List(3, 3, 3)),
// Right(value = 4)
// )
Mode.Accumulating.either[String, List].locally {
source.fallibleTo[Tuple.InverseMap[source.type, Mode.current.Self]]
}
// res0: Either[List[String], *:[Int, *:[String, *:[List[Int], *:[Int, EmptyTuple]]]]] = Right(
// value = (1, "str", List(3, 3, 3), 4)
// )
...where repeatedly referring to the F
wrapper becomes really unwieldly - that type is known to the compiler at each call site so we make it work for us in conjunction with Mode.current
which summons the Mode[F]
instance in the current implicit scope.
Since the introduction of SIP-56 - Proper Specification for Match Types in Scala 3.4+ some usages of Mode.Self
became illegal - the example above is one of those cases (more information as to why this is the case can be found here).
This construct can be reproduced in Scala 3.4+ by using an extension method, like so:
extension [A <: Tuple](self: A)
inline def parTupled[F[+x]](using Mode.Accumulating[F]) =
self.fallibleTo[Tuple.InverseMap[A, F]]
Mode.Accumulating.either[String, List].locally {
source.parTupled
}
// res1: Either[List[String], *:[Int, *:[String, *:[List[Int], *:[Int, EmptyTuple]]]]] = Right(
// value = (1, "str", List(3, 3, 3), 4)
// )