Pre-SIP: Proposal of introducing a Rust-like type "Result"

#1

Suppose we want to implement a function that checks whether the length of a string is within certain limits. If the check succeeds the function shall return the provided value, if it fails some information about the reason shall be returned.

We can meet these requirements by using Either as the return type of the function:

import scala.util.{Either, Left, Right}

def checkString(value: String): Either[String, String] = {
  val len = value match {
    case s: String => s.length
    case _         => 0
  }

  len match {
    case _ if (len < 4)  => Left("too short")
    case _ if (len > 10) => Left("too long")
    case _               => Right(value)
  }
}

checkString("user") match {
  case Right(value) => println(s"valid value: $value")
  case Left(reason) => println(s"invalid value: $reason")
}
n

This, of course, works fine but the naming of Left and Right has not much semantic to it and a caller of checkString always has to consult the documentation to know what kind of result Left and Right represent.

To make things clearer I propose to add a type Result to the Scala Standard Library which has concrete implementations named Ok and Err. Since Success and Failure are aready used by scala.util.Try I suggest to just take the names of the corresponding Rust enum values.

2 Likes
#2

They’re not part of the standard library, but Cats has great tooling around Validated (which has the implementations Valid and Invalid), which sounds like it provides what you’re looking for.

2 Likes
#3

The terms Valid and Invalid are in validation tasks useful but do not have a greater scope than that, at least in my opinion.

1 Like
#4

Result, like this?

https://github.com/vpatryshev/ScalaKittens/blob/master/scala212/core/src/main/scala/scalakittens/Result.scala

I got a feeling it’s time is a little bit past, with ZIO around, which is way deeper in this relation.

1 Like
#5

At first glance, this Result does not allow us to return two different types. So that is not what I am looking for - and it is not part of the Standard Library.

2 Likes
#6

I like Result, the naming works much better than for Either. I slightly dislike having two different “result” types, Result, and Try with different constructors. One alternative would be to make Try[T] a type alias for Result[T, Throwable], then we could continue to use Failure and Success for both.

11 Likes
#7

One alternative would be to make Try[T] a type alias for Result[T, Throwable] , then we could continue to use Failure and Success for both.

Great idea! This makes Try more useful without proliferating the number of data types isomorphic to Either.

In Scala 3, Try could be an opaque type for Either, enabling zero-cost conversion between Try and Either.

2 Likes
#8

Surely the whole point of Try is that it traps exceptions thrown during map, etc. I’m not sure how that behaviour could be preserved if it were simply an alias. What would we do if an exception were thrown when mapping a Result[String, String]?

The main problem to my thinking is that Either carries little semantic indication as to its intent, being used as both a disjoint union and as success/failure when failure isn’t a subclass of Throwable

If anything, we want to deprecate Either and then introduce Result with subtypes of Good/Bad, Ok/Err, or whatever else the bike-shedding exercise comes up with. Either/Left/Right could potentially be aliased to this, and conversions between Try and Result would be needed, much as we already have between Try and Either.

It would be easy enough to implement a naive scalafix rewrite too, though it would arguably be doing the wrong thing in cases where Either is used as a disjoint union.

For the disjoint union use-case, we already have much more capable intersection types in Dotty.

4 Likes
#9

Surely the whole point of Try is that it traps exceptions thrown during map , etc. I’m not sure how that behaviour could be preserved if it were simply an alias.

It can’t be, not in a parametrically sound fashion. I suppose that argues for a clean separation (although I personally do not rely on this characteristic of Try).

Good point. :+1:

2 Likes
#10

scalactic’s Or was just like Result, including the success type on the left. We’ve moved away from it to ZIO as a much richer error model

#11

Yes, good point. We can’t change Try's map and flatMap behavior, and that makes it indeed incompatible with a Result type.

3 Likes
#12

This doesn’t really add anything. First off, Either is not ambiguous. Right is the right answer, and Left is an invalid result. There’s a bias by convention from Haskell, and, I believe, Scala 2.13 made it right-biased by API as well. I’ll give you that the clue relies on a word play, but, still, you are not adding anything but a name.

Second, I believe Rust’s result is much closer to Try than Either, since a common operator on it, for which there’s even a syntactic shortcut, is get-or-raise-exception. And, yeah, Try is Success or Failure, so naming is covered by that.

But third and most importantly, Rust’s Result is supported in multiple ways in Rust: library conventions, the afore-mentioned syntactic shortcut, etc. Simply adding a Result will not help and, if anything, it will muddle things even more by adding yet another result class, which will leave people wondering what’s supposed to be used when. And if you think such confusion will not arise, then consider the fact that you were either (pun intended) unaware that Either is right-biased by years of convention and by API, or could not memorize which was supposed to be used when – consider people trying to memorize when to use Either, Try or Results…

And if you think the right-bias is not well documented, here’s the second paragraph of Either’s scaladoc:

A common use of Either is as an alternative to scala.Option for dealing with possibly missing values. In this usage, scala.None is replaced with a scala.util.Leftwhich can contain useful information. scala.util.Right takes the place of scala.Some. Convention dictates that Left is used for failure and Right is used for success.

There. Anyway, fourth, that simple proposal does not address the host of issues people have with Either that lead to the creation of more powerful types. For example, can errors be chained, like exceptions? Should errors me accumulated list-like, or should only one be kept? What’s the relationship between failure results and exceptions? How can you use this type when doing monadic comprehensions?

Fifth and finally, there’s a bunch of better “result” classes out there. Let’s not add another to the mix, which doesn’t add anything but a better name.

9 Likes
#13

Never under-estimate the importance of just a name in making logic self-documenting

My proposal was that either be deprecated in favour of Result and dotty’s disjoint unions. Try/Result is not hard to remember, given that one of those options (no pun intended) shares a name with the try keyword. By clearly establishing that the intent of Result is to be used in a pass/fail scenario and not as a union, this becomes an enabling change for the sort of library conventions and syntactic support that Rust offers.

1 Like
#14

It adds semantic, something very important when it comes to the understanding and conciseness source code.

That I know, I was refering to the clear naming of the enum entities of Rust’s Result.

In fact what I probose is a renaming of Either and its concrete implementations. It is just about semantics. The implementation of Either is completely fine.

1 Like
#15

Either is just data and operations on it, without having anything to say about the meaning of the data it contains. On the one hand, I understand that making it more discoverable that this datatype is useful for encoding a result that can be either a success or a failure through its name has advantages in discoverability. But that comes at the cost of implying that there is somehow something about the datatype that makes it unsuitable for things that are not results, but some other form of data that can be either one thing, or another. While you can reasonably argue that’s less important, it’s very much a matter of opinion, and one I don’t agree with.

Can smart aliasing accomplish the same thing that renaming would?

3 Likes
#16

Either is not applicative, and that’s a problem. Result is. That includes accumulating errors (in a monoid).

1 Like
#17

I do not really want to rename Either. I only wanted to underline that I am unhappy with Either's implementation but with its naming.

#18

Did you mean union types?

In any case, Dotty union types are absolutely not a replacement for Either, since they can only act as disjoint unions if it is statically known that the two alternatives can be distinguished at runtime. That rules out types like List[Int] | A (see https://github.com/lampepfl/dotty/issues/5826); in the general case you need to wrap the alternatives in proper constructors, at which point it’s basically the same as Either, just more ad-hoc and with worse library support.

6 Likes
#19

I like having a Rust like Result type in the standard library. @dcsobral mentioned that Either is right biased. So Right is interpreted as Ok and Left as Err. And this is something that is clearly mentioned in the docs. But I didn’t know this because I did not care to go through the doc for Either. The reason for that was not carelessness but rather the naming itself. The name Either doesn’t suggest that it is something that is designed to specifically handle Ok and Err. Using Either for Ok and Err sounded like a hacky way doing things, at that time.
Hence, I have been using Try. But it has it’s short coming given that your errors can only be Throwable

1 Like
#20

And in Rust people know what Result is just from the name?

2 Likes