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:

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)
// )