typelevel / typelevel/cats-mtl

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

Open
#644 0 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
307
Forks
66
Avg merge
6h 23m
Merged PRs (30d)
2

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the MTL 1.6 error-channel announcement and the Submarine error propagation changes in PR 619, then compare them with the proposed ErrS and HandleS aliases in this issue. The scope and name still need maintainer agreement; done would be an accepted public alias, with any required documentation or tests identified by that decision.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.