scalatagless-finalzio

Validation and capturing errors using an algebra


I came across this article on medium: https://medium.com/@odomontois/tagless-unions-in-scala-2-12-55ab0100c2ff. There is a piece of code that I have a hard time understanding. The full source code for the article can be found here: https://github.com/Odomontois/zio-tagless-err.

The code is this:

trait Capture[-F[_]] {
  def continue[A](k: F[A]): A
}

object Capture {
  type Constructors[F[_]] = F[Capture[F]]

  type Arbitrary

  def apply[F[_]] = new Apply[F]

  class Apply[F[_]] {
    def apply(f: F[Arbitrary] => Arbitrary): Capture[F] = new Capture[F] {
      def continue[A](k: F[A]): A = f(k.asInstanceOf[F[Arbitrary]]).asInstanceOf[A]
    }
  }
}

Here are my questions:

Thanks!

Update:

First question:

Based on the spec when the upper bound is missing it is assumed to be Any. So, Arbitrary is treated as Any, however, it doesn't seem interchangeable with Any.

This compiles:

object Test {
    type Arbitrary

    def test(x: Any): Arbitrary = x.asInstanceOf[Arbitrary]
  }

however, this doesn't:

object Test {
   type Arbitrary

   def test(x: Any): Arbitrary = x
}

Error:(58, 35) type mismatch;
 found   : x.type (with underlying type Any)
 required: Test.Arbitrary
    def test(x: Any): Arbitrary = x

See also this scala puzzler.


Solution

  • This is a little obscure, though legal usage of type aliases. In specification you can read type alias might be used to refer to some abstract type and be used as a type constraint, suggesting compiler what should be allowed.

    So the code from the question

        def apply(f: F[Arbitrary] => Arbitrary): Capture[F] = new Capture[F] {
          def continue[A](k: F[A]): A = f(k.asInstanceOf[F[Arbitrary]]).asInstanceOf[A]
        }
    

    can be read as: we have Arbitrary type we know nothing of, but we know that if we put F[Arbitrary] into a function, we get Arbitrary.

    Thing is, compiler will not let you pass any value as Arbitrary because it cannot prove that your value is of this type. If it could prove that Arbitrary=A you could just write:

        def apply(f: F[Arbitrary] => Arbitrary): Capture[F] = new Capture[F] {
          def continue[A](k: F[A]): A = f(k)
        }
    

    However, it cannot, which is why you are forced to use .asInstanceOf. That is why type X is not equal to saying type X = Any.

    There is a reason, why we aren't simply using generics though. How would you pass in a polymorphic function inside? One that does F[A] => A for any A? One way would be to use natural transformation (or ~> or FunctionK) from F[_] to Id[_]. But how messy it would be to use it!

    // no capture pattern or other utilities
    new Capture[F] {
      def continue[A](fa: F[A]): A = ...
    }
    
    // using FunctionK
    object Capture {
    
      def apply[F[_]](fk: FunctionK[F, Id]): Caputure[F] = new Capture[F] {
        def continue[A](fa: F[A]): A = fk(fa)
      }
    }
    
    Capture[F](new FunctionK[F, Id] {
      def apply[A](fa: F[A]): A = ...
    })
    

    Not pleasant. Problem is, you cannot pass something like polymorphic function (here [A]: F[A] => A). You can only pass instance with polymorphic method (that is FunctionK works).

    So we are hacking this by passing a monomorphoc function with A fixed to type that you cannot instantiate (Arbitrary) because for no type compiler can prove that it matches Arbitrary.

    Capture[F](f: F[Arbitrary] => Arbitrary): Capture[F]
    

    Then you are forcing the compiler into thinking that it is of type F[A] => A when you learn A.

    f(k.asInstanceOf[F[Arbitrary]]).asInstanceOf[A]
    

    The other part of the pattern is partial application of type parameters of sort. If you did things in one go:

    object Capture {
      type Constructors[F[_]] = F[Capture[F]]
    
      type Arbitrary
    
      def apply[F[_]](f: F[Arbitrary] => Arbitrary): Capture[F] = new Capture[F] {
          def continue[A](k: F[A]): A = f(k.asInstanceOf[F[Arbitrary]]).asInstanceOf[A]
        }
    }
    

    you would have some issues with e.g. passing Capture.apply as a normal function. You would have to do things like otherFunction(Capture[F](_)). By creating Apply "factory" we can split type parameter application and passing the F[Arbitrary] => Arbitrary function.

    Long story short, it is all about letting you just write: