Hacker News new | ask | show | jobs
by me_vinayakakv 651 days ago
I was looking into the pattern matching example in the article with `Either` type. If we need to unwrap and check for all the cases one by one would it become a callback hell?

I was going through a Scala codebase at work that uses `Future`s and `map`ing and `flatMap`ing them. Sometimes the callbacks went 5-6 levels deep. Is there a way to "linearlize" such code?

I come from JS/TS background and have not much experience with pufe functional languages. But I love how TS handles discriminated unions - if we handle a branch and `return` early, that branch is removed from the union for the subsequent scope, and I was wondering if something of that sort can be achieved in Haskell/Scala.

3 comments

In Haskell, that's usually that's done using `do` syntax.

    do
      a <- somePartialResult
      b <- partialFunction1 a
      c <- partialFunction2 a b
      return c
where we assume signatures like

    somePartialResult : Either<A, Error>
    partialFunction1 : A -> Either<B, Error>
    partialFunction2 : A -> B -> Either<C, Error>
this overall computation has a signature Either<C, Error>. The way it works is that the first failing computation (the first Either that's actually Left-y) will short-circuit and become the final result value. Only if all of the partial computations succeed (are Right-y) will the final result by Right(c).

In Haskell we don't have an early return syntax like `return` and function scope. Instead, we construct something equivalent using `do` syntax. This can be a little weightier than `return`, but the upside is that you can construct other variants of things like early returns that can be more flexible.

Nice! Would it be possible to transform an error to something else using this syntax?

Or, should we resort to a method of `Either` that transforms its `Left` in that case?

Unfortunately, no. Or, rather, I'm sure there's a way to make it happen although that's not typical practice. Typically you'd resort to mapping the left sides of your eithers so that the error types match.

Rust offers a similar facility (though specialized to just handle a couple kinds of error handling) using its `?` syntax. This works essentially identically to the do syntax above, but also includes a call to transform whatever error type is provided into the error type of the function return.

Note that in Rust (a) this technique only, today, works at function boundaries and (b) will always be explicitly annotated since all functions require an explicit type. This helps a bit over Haskell's more general approach as it provides some additional data to help type inference along.

That said, if you were interested, it's likely possible to emulate something very similar to Rust's technique in Haskell, too.

But I don't think I've ever seen that. It just doesn't feel as stylish in Haskell. The From/Into traits define a behavior that's much more pervasive than most type classes in Haskell. It works well for Rust, but is I think less compelling to the Haskell community.

I've been out of the Scala game for a few years, but I would use a for comprehension with the cats EitherT monad transformer.

https://typelevel.org/cats/datatypes/eithert.html

    def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] =
      for {
        a <- EitherT(parseDoubleAsync(inputA))
        b <- EitherT(parseDoubleAsync(inputB))
        result <- EitherT(divideAsync(a, b))
      } yield result
Yeah, but Usually we just use something like ZIO nowadays. So the code becomes:

    def divisionProgramAsync(inputA: String, inputB: String): IO[String, Double] =
      for {
        a      <- parseDoubleAsync(inputA)
        b      <- parseDoubleAsync(inputB)
        result <- divideAsync(a, b)
      } yield result
(the annoying wrapping/unwrapping isn't necessary with ZIO here)

You can also write this shorter if you want:

    def divisionProgramAsync(inputA: String, inputB: String): IO[String, Double] =
      for {
        (a, b) <- parseDoubleAsync(inputA) <*> parseDoubleAsync(inputB)
        result <- divideAsync(a, b)
      } yield result
Yes, it is possible to linearize it. You can, for example use do notation:

    result <- do
        a <- someEitherValue
        b <- anotherEitherValue
        return (doStuff a b)
In the above example the do notation will unwrap the values as an and b, but if one of the results is Left, the computation is aborted, returning the Left value.

This is one just of the many techniques available to make error checking linear.