Skip to content

More idiomatic Result API - #42

Merged
bishabosha merged 3 commits into
mainfrom
more-idiomatic
Aug 13, 2026
Merged

bishabosha merged 3 commits into
mainfrom
more-idiomatic

Conversation

@bishabosha

Copy link
Copy Markdown
Member

In preparation to make Result stable for standard library:

  • follow standard library naming conventions more
  • tap methods are inline
  • use fewer extension methods (avoid risk of collisions with third party libraries)
  • add a method to catch NonFatal.

Extension methods are still potentially causing colisions, so lets avoid in most cases.

- Move all methods from companion extension blocks into the enum body,
  using stdlib-style lower bounds (getOrElse[T1 >: T],
  flatMap[U, E1 >: E], etc.) and evidence-based flatten/toTry like
  Either's. The sources move from package.scala to Result.scala.
- Rename `or` to `orElse` and `getNullable` to `orNull` (with Option.orNull's
  signature); tap/tapErr now take f: T => U per the existing TODOs.
- Keep extensions only where required: Result.eval (unchanged), the
  right-associative *: operator, and ScalaConverters on stdlib types
  (which would become real methods if merged into the stdlib).
- Remove the unidiomatic ResultIsErrException case class; get now
  throws a plain NoSuchElementException like Option.get.
- Add Result.catching to construct a Result from a NonFatal-throwing
  body, with tests covering success, caught, and fatal rethrow cases.
@bishabosha
bishabosha requested a review from natsukagami July 17, 2026 16:32
* [[Err]].
* @group access
*/
def orNull[T1 >: T](using ev: Null <:< T1): T1 = this match

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would inference deal with this? 🤔 it looks complicated, most of the time I just want T | Null, but is it not possible because of variance?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lifted the signature from Option, which infers like this:

scala> Some(1).orNull
val res0: Int | Null = 1

@bishabosha bishabosha Jul 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a type variable does give the option to infer a nicer type as well - e.g. for getOrElse it could infer the sealed trait type rather than a straight union of value | default

scala> def getOrElse2[A, B](a: Option[A])(b: => B): A | B = a match
         case Some(a1) => a1
         case _ => b

def getOrElse2[A, B](a: Option[A])(b: => B): A | B

scala> sealed trait Bar; case class X() extends Bar; case class Y() extends Bar
// defined trait Bar
// defined case class X
// defined case class Y

scala> Some(X()).getOrElse(Y())
val res4: Bar = X() // type variable picks a nicer target

scala> getOrElse2(Some(X()))(Y())
val res5: X | Y = X() // union is too precise

@bishabosha bishabosha Aug 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just seen that Option adopted a new signature, so lets follow that:

final def orNull[A1 >: A | Null]: A1 = ???

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good :) I think we should also compile with strict nulls

@bishabosha
bishabosha merged commit 136dd9e into main Aug 13, 2026
4 checks passed
@bishabosha
bishabosha deleted the more-idiomatic branch August 13, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants