Skip to content

Would writing type ErrS[F[_]] = Handle[F, String] inside Cats MTL be doing the ecosystem a favour?Β #644

@benhutchison

Description

@benhutchison

Now that, as of MTL 1.6 series, we have a error channel type decoupled from the awful relic Throwable via Submarine error propagation, it might be worth thinking about what error channel types users actually want.

Speaking strictly for myself, I rarely indulge in custom error ADTs, rather I typically use simple Strings for my errors. This style emerged from two realisations:

  • Typed errors drop off sharply in value as the handling code site "moves away" from the raising site. In the small, typed error ADTs are useful close to the site where they're thrown. But at scale, errors become intermingled with other unrelated causes, and one typically finds oneself dealing with "errors in general" and wanting to forget the specific varieties.

  • Who consumes the specific information in errors? It's typically humans. And so human-readability is the number one criterion for a good error, ie a well-constructed String msg.

The simplicity of Strings also means they naturally interop with other ecosystem libraries. I hit a specific example today, the excellent new Refined Type library Iron. Iron already works in terms of a String type on the left for failed refinements. However, it currently limits itself to concrete effect types (Either, Validation) in the Cats integrations.

Combining Iron & Cats MTL we can support refinement using any effects that support String-typed errors. The demo shows refinement to Either, and then to IO:

//> using scala 3.7.1
//> using dep org.typelevel::cats-effect:3.5.4
//> using dep org.typelevel::cats-mtl:1.4.0
//> using dep io.github.iltotore::iron:2.6.0

import cats.mtl.Handle
import cats.mtl.syntax.raise.*

import io.github.iltotore.iron.{given, *}

type ErrS[F[_]] = Handle[F, String]

object IronExt:
  extension [A](value: A)
     //Curried types!? Scala 3 syntax is cool 😎  https://docs.scala-lang.org/sips/clause-interleaving.html 
    def refineRaise[C](using RuntimeConstraint[A, C])[F[_]: ErrS as F]: F[A :| C] =   
      value.refineEither[C] match
        case Right(t) => F.applicative.pure(t)
        case Left(msg) => msg.raise

  extension [A, C](rt: RefinedType[A, C])
    def raise[F[_]: ErrS as F](value: A): F[rt.T] = 
      rt.either(value) match
        case Right(t) => F.applicative.pure(t)
        case Left(msg) => msg.raise

import cats.effect.IO
import io.github.iltotore.iron.constraint.string.*
import IronExt.*
import cats.effect.unsafe.implicits.global
@main
def demo = 
    println("***".refineRaise[Alphanumeric]) //Left(Should be alphanumeric)
    Handle.allow[String]("***".refineRaise[Alphanumeric][IO]).rescue(msg => IO.raiseError(new Exception(msg))).unsafeRunSync()

Abstraction over effect types may seem esoteric at first glance, but once APIs harden around a concrete effect type, it's nigh impossible to retrofit. I encountered this limitation with Circe some years ago because it uses a concrete error effect, and it proved insoluble.

Anyway, that's a long build-up to my main point. A type alias for Handle[F, String], like ErrS above, is likely to be widely useful and enable integrations between libraries that can use Strings as a common denominator error type.

Without it, I expect many downstream libraries will end up duplicating that alias in their own code. I propose doing the ecosystem a favor and putting it into MTL. I like ErrS because it's mnemonic yet short, and I expect I'm going to write it a lot. But HandleS is another candidate name.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions