Idea: throw try away and unify match-catch

Yes it is, but it is still not equivalent to the match-with-catch construct that I propose.
It is not trivial to mimic match-with-catch with current Scala constructs. Yet the byte code generation would be easy.

It’d be problematic because …

I don’t understand what you mean.
Could you illustrate this, with traditional code and with the match-with-catch alternative?

Honestly, I find that very unintuitive and unclear. I’d probably veto it if I ever saw it in a code review, whether it was legal syntax or not…

2 Likes

I’ll cop to using “brainwash” in an ungenerous way.

For me, the term was hot-button because of Patty Hearst but today I understand the term as such to be discredited, so in fact I used it jocularly, though not unseriously.

I did intend to say that we can’t think rationally about the try construct. An expression that throws is simply too special in the Java ecosystem. (More later.)

I did not intend to compare Scala syntax to various forms of coercion that are applied cruelly and unusually in this world.

Since I am liable to express myself in terms that might reasonably be construed to be over-the-top, I’m willing to justify this over-the-top term in this context.

I think there is a distinction between language policing which says “You can’t say that” and a conversation which says “You have to justify your words, or at least use more words.”

In the spirit of using more words, my Java friend insists that Javadoc list all throwables, and also that throws clauses list unchecked throwables. This is so that the IDE will always tell you what throws where. I prefer the Scala solution, that everything throws anything, and you catch where it is meaningful to do so. I just remembered the phrase, “catch as catch can”.

So I do think there is a “throws” culture who buy into throwing as a way of passing control up the stack, as opposed to a functional or value culture who would prefer to pass a value up the stack, perhaps an Either.

I just experienced this distinction recently in a Java project where I had introduced a Try. I added some API and realized I could now just return a Try. I felt like such a fool.

So I do insist that our ability to conceptualize is limited by previous thinking. Maybe someone could remind me of the term for that, something more precise than “brainwashing”. It seems to me that Althusser had a word for it, but after so many years, the word escapes me.

Edit: sorry to derail the conversation, and also thanks for the responses attempting to parse what I might have said, etc.

2 Likes

I think this is called “Exceptions as flow control” (?). For what it’s worth, I don’t see try as part of this as much as a shim for interacting with Java libraries ¯\_(ツ)_/¯

Possibly, “stuck in the throw paradigm”? I also sort of like, “hanging onto their Java accent”

Took a couple hours to sink in, and rereading it a few times, but I think I understand what you’re trying to say. I still disagree, but at least I think I grok it a bit better.

The inverted version I posted has exactly these semantics:

This becomes easier to see when you inline the blocks:

try e1 catch {
  case failedResult => e3
} match {
  case successResult => e2
}

Incidentally, this effectively Try.fold, but blessed into the language.

Introducing new syntax would help differentiate this from the existing try, but an automatic rewrite would cause the problems I mentioned. Specifically, it’s this bit that causes the trouble:

You’d create a situation where an error handling construct couldn’t be scoped to handle errors which trigger before the Future takes over, because it would automatically lift the block into the context of the returned Future. This only works if there’s no chance exceptions can be thrown outside of the Future, which is (unfortunately) not the case.

I’m also really unsure of the wisdom of overloading the meaning of yield. Embedding this in a for comprehension would look a bit off:

for {
  a <- f1
  b <- f2
} yield a + b yield match {
  case r => 
} with catch {
  case e => 
}
1 Like

That is not true. A concrete example:

{throw new Exception} 
match {case _ => print("Success"}
with 
catch {case _ => print("Failure"}

prints: “Success

Edit: Oops. that should be “Failure”!

Your interpretation, with an extra pair of braces to get it compiled:

{ try {throw new Exception} catch {
  case failedResult => print("Failure")
} } match {
  case successResult => print("Success")
}

prints: “FailureSuccess”.

With respect to the version for futures and possibly similar constructs:

f yield match e1 
   with catch e2

would behave exactly the same as

f.onSuccess{e1}
f.onFailure{e2}

unless I am overlooking something.
AFAICS there is no strange behaviour when an Error is thrown.

Edit: I see now that onSuccess and onFailure are deprecated. Instead of the fragment just above, it should be

f.       foreach{e1}
f.failed.foreach{e2}

Your counter example would typically not compile, because the second yield would require that the preceding expression is of a type Yield_match_catch_handler.

@AndreVanDelft Your match .. catch does not seem to solve a common problem to me. There should be a very high bar to put something in the standard library, but an even higher bar to put it in the language.

If this is a common problem in your codebase, you can implement it in user space:

object Foo {
  implicit def MatchCatchOps[A](self: => A): MatchCatchOps[A] = new MatchCatchOps(() => self)
  class MatchCatchOps[A](val self: () => A) extends AnyVal {
    def matchC[B](pf: PartialFunction[A, B]): MatchC[A, B] =
      new MatchC(self, pf)
  }
  class MatchC[A, B](self: () => A, pf: PartialFunction[A, B]) {
    def catchM(pfCatch: PartialFunction[Throwable, B]): B = {
      val e = try Right(self()) catch { case th: Throwable => Left(pfCatch(th)) };
      e.map(pf).fold(identity(_), identity(_))
    }
  }
}

then:

scala> import Foo._
import Foo._

scala> { 5: Int } matchC { case 5 => throw new IllegalArgumentException; case _ => "Success" } catchM { case _ => "Failure" }
java.lang.IllegalArgumentException
  at $anonfun$1.applyOrElse(<console>:16)
  at $anonfun$1.applyOrElse(<console>:16)
  at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:38)
  at scala.util.Either.map(Either.scala:353)
  at Foo$MatchC.catchM(<console>:11)
  ... 28 elided

scala> { 6: Int } matchC { case 5 => throw new IllegalArgumentException; case _ => "Success" } catchM { case _ => "Failure" }
res5: String = Success

scala> { (throw new Exception): Int } matchC { case _ => "Success" } catchM { case _ => "Failure" }
res6: String = Failure
4 Likes

If this is to be universally available syntax, it certainly should.

If it isn’t, then there’s no real basis to put it into the standard library - just use Future.transform

1 Like

While that’s a fair point, I think that example is backwards, as throwing an Exception resulting in printing "Success" would be deeply counterintuitive.

To get exactly the semantics you seem to want, you would need to use Try:

Try(throw new Exception)
  .fold(_ => print("Success"), _ => print("Failure"))

With the automatic lift, what would you expect your proposed syntax to do in this case:

{ 
  throw new IllegalStateException("Seems like this would be uncatchable, due to the transformation to Future.yieldMatchWithCatch")
  Future.successful("Foo")
} yield match { case success => print(s"Returned $success (inside Future)") }
  with
  catch { case failure => print(s"Return $failure (inside Future)" }

Yes that would be so…I made an error; I should have written:

prints: “Failure”

I edited that in my previous post.

1 Like

Not sure whether I should answer this after discovering my “Success”/“Failure” mistake.

I assume you meant yield match rather than match, in your last code fragment. Then the IllegalStateException would indeed not be caught.

Yep, I meant yield match, good catch.

It seems like not catching the IllegalStateException would be a big problem.

Currently, this can be handled by wrapping the main block in a try, and handling the error inside the Future elsewhere, which seems much easier to follow:

val future = try {
  throw new IllegalStateException
  Future.failed(new Exception)
} catch {
  case e: IllegalStateException => print(s"Failure to launch: $e")
}

future.transform(
  r => print(s"Success: $r"),
  e => print(s"Failed: $e")
)

The proposed syntax would require nesting inside an additional try, which would be a bit of a mess:

try { 
  {
    throw new IllegalStateException
    Future.successful("Foo")
  } yield match { case success => print(s"Returned $success (inside Future)") }
    with
    catch { case failure => print(s"Return $failure (inside Future)" }
} catch {
  case e: IllegalStateException => print(s"Failure to launch: $e (outside Future)"}
}

Thank you; that is a neat piece of code!
It makes me almost wonder why there is a match construct.

For the yield variants a similar approach would apply.

I don’t really think this needs improving!

try readList() match
  case Nil => 1
  case x :: xs => 2
catch
  case IOException => 3
  case Error => 4

and yet it does need improving; let’s try :slight_smile:

try readList() match
  Nil => 1
  x :: xs => 2
catch
  IOException => 3
  Error => 4

With whitespace significant syntax there should be no need for case in match clauses (or perhaps anywhere in the language outside of case class definitions).

This would align Scala more closely syntactically to the ML family of languages, which are IMO rather pleasing to the eye, both terse and readable.

With whitespace significant syntax there should be no need for case in match clauses (or perhaps anywhere in the language outside of case class definitions).

Will dotty really become whitespace sensitive?

Remember that Dotty is basically the experimental testbed. At the moment, it is whitespace sensitive under certain circumstances, but that’s controversial – we’ll see whether that makes it into Scala 3.

3 Likes