loading...
Cover image for "Type Disjunctions" in Scala

"Type Disjunctions" in Scala

geirolz profile image David Geirola Updated on ・3 min read

Hi guys,

Today i'd like to talk about Type Disjunctions (Union Type) in Scala.

As we all know this feature is not available in Scala yet but i'd like to share with you my solution about a case i've had this week working on my library.

I'm writing a FP library to edit and work easily with XML in Scala, it's based on standard scala-xml library and Cats. You can check this library on github at: https://github.com/geirolz/advxml

Case

I've defined a new type alias named ValidatedEx

    type ValidatedEx[+T] = ValidatedNel[Throwable, T] // = Validated[NonEmptyList[E], A]

I wanted to add new methods on this type using extension methods, one of these was a method named "transform" with the aim to convert ValidatedEx instance into F[_].

Must have:

  • One single method, the user should not have the burden to choose the right method depending on the case.
  • Use Cats instances, i don't want to reinventing the wheel.

First solution [Failed]

Using an implicit monad error in order to convert ValidatedEx to F[_], pattern matching over the instance, invoking pure or raiseError depending on the case and the game is done!

    implicit class ValidatedExOps[A](validated: ValidatedEx[A]) {
        def transform[F[_]](implicit F: MonadError[F, NonEmptyList[Throwable]]): F[A] =
          validated match {
            case Valid(a)   => F.pure(a)
            case Invalid(e) => F.raiseError(e)
          }
      }

Yea! It works! But...i can't use standard monads error provided by cats...

This wont compile because in the scope is not present an implicit instance of MonadError[Try, NonEmptyList[Throwable]] and cats provides only MonadError[Try, Throwable].

      import cats.instances.try_._
      val value: ValidatedEx[String] = Valid("TEST")
      //No implicit found for parameter F:MonadError[Try, NonEmptyList[Throwable]] 
      val tryValue: Try[String] = value.transform[Try]

I don't want to create a new monad error for each type, my library must work with cats!

Second solution [Failed]

Using natural transformation!

      implicit class ValidatedExOps[A](validated: ValidatedEx[A]) {
        def transform[F[_]](implicit F: ValidatedEx ~> F): F[A] = F(validated)
      }

      import cats.instances.try_._
      val value: ValidatedEx[String] = Valid("TEST")
      //No implicit found for parameter F:ValidatedEx ~> F
      val tryValue: Try[String] = value.transform[Try]

Here we go again, this solution require ad-hoc implementations of FunctionK for each type and i don't want to reinventing the wheel.

Third solution [Success]

I want have just one method, keep all possibilities open and substantially have two MonadError in the signature but only one is required. This smell like an union type!

Let's define "union types" using Either with a very easy solution.

      type \/[+A, +B]           = Either[A, B]
      type MonadEx[F[_]]        = MonadError[F, Throwable]
      type MonadNelEx[F[_]]     = MonadError[F, NonEmptyList[Throwable]]

      implicit class ValidatedExOps[A](validated: ValidatedEx[A]) {
        def transform[F[_]](implicit F: MonadEx[F] \/ MonadNelEx[F]): F[A] =
          F match {
            case Left(m) =>
              validated match {
                case Valid(value) => m.pure(value)
                //AggregatedException is a my own class that just collapse multiple exceptions into one 
                case Invalid(exs) => m.raiseError(new AggregatedException(exs.toList))
              }
            case Right(m) =>
              validated match {
                case Valid(value) => m.pure(value)
                case Invalid(exs) => m.raiseError(exs)
              }
          }
      }

But...still fail, obviously there isn't an implicit instance for Either[MonadEx[F], MonadNelEx[F]]

      import cats.instances.try_._
      val value: ValidatedEx[String] = Valid("TEST")
      //No implicit found for parameter F:Either[MonadEx[F], MonadNelEx[F]]
      val tryValue: Try[String] = value.transform[Try]

Let's wrap what cats provides, let's "intercept" monads error provided in the scope and wrap them into Either.

      implicit def monadExLeftDsj[F[_]]
        (implicit F: MonadEx[F]): MonadEx[F] \/ MonadNelEx[F] = Left(F)
      implicit def monadNelExRightDsj[F[_]]
        (implicit F: MonadNelEx[F]): MonadEx[F] \/ MonadNelEx[F] = Right(F)

And for a more generic solution we can write:

      //for explicit values
      implicit def aLeftDsj[A](v: A): A \/ * = Left(v)
      implicit def bRightDsj[B](v: B): * \/ B = Right(v)

      //for implicit values
      implicit def aLeftDsj[A](implicit v: A): A \/ * = Left(v)
      implicit def bRightDsj[B](implicit v: B): * \/ B = Right(v)

Done! It works!

I know that this solution isn't perfect and beautiful, i hate implicit conversions, and i also know that union types are coming but in the meantime this is a simple solution to partially bypass this scala loss for a specific case like mine.

Tell me what do you think about this solution :)

Posted on by:

geirolz profile

David Geirola

@geirolz

Scala and Java developer for YOOX NET-A-PORTER GROUP

Discussion

pic
Editor guide
 

You could rely on the Curry-Howard isomorphism, like shown in this article (I did that, and it works pretty).

If you’re daring, you can use Dotty.

Or else you may just deal with every type apart (it isn’t that bad approach).