#892 Proposal: using ?: operator with throw

brian Sat 2 Jan 2010

Fantom follows the traditional C/Java model where not all statements are expressions (something I would do differently if I went back in time). But there is one case I keep finding annoying, where I want to write this:

map[key] ?: throw KeyNotFound(key)

That code won't compile today because throw isn't an expression. But I think it would be very handy to make that code work because I keep running across issues where it requires three lines:

v := {some expr which might eval to null}
if (v != null) return v
throw Err

In a perfect world we would make throw a general purpose expression. However that gets into all sorts of ugly type system issues like Nothing/bottom type.

So I am proposing instead just a simple change to the grammar of ?: that allows either an expression or a throw statement.

Comments?

DanielFath Sat 2 Jan 2010

Seems like a nice addition despite the limitation. But overall seems like a small improvement. Oh and Happy New Year.

tactics Sat 2 Jan 2010

I'd be for it. Fan just keeps nipping away at those keystrokes, and it's great.

DanielFath Sat 2 Jan 2010

So am I. But personally, I think self documentation (as in letting Fandoc find and register what exceptions each method throws) of unchecked exceptions would be more awesome.

tactics Sat 2 Jan 2010

letting Fandoc find and register what exceptions each method throws

This is an interesting idea. Don't know if it belongs in Fandoc, though. But it would sure make a cool IDE tool!

DanielFath Sat 2 Jan 2010

Thanks. The way I see it checked exceptions have one major advantage over unchecked. They are self documenting.

With Fandoc collecting and presenting (per method) a brief summary why is exception thrown (or just stating in Java-like manner which Errs are thrown) would make unchecked easy to spot and document and even easier to deal with.

Idea is that it works only for those exceptions that are thrown in the source. IDEs could detect even those exceptions that are thrown in the layers below the method invocation.

andy Sat 2 Jan 2010

Not sure I'm sold on that. The type system should be handling alot of cases where you would do that. So I don't think its worth adding that wrinkle.

casperbang Sun 3 Jan 2010

The thing is, this "self documentation" of checked exceptions looks nice on paper, but really doesn't help much in practice - you can not enumerate all possible exit conditions and it just ends up polluting the signature.

I have yet to understand the paranoid rules around checked exceptions in Java (and for fun, have modified javac to get some more relaxed semantics).

helium Sun 3 Jan 2010

However that gets into all sorts of ugly type system issues like Nothing/bottom type.

Is this concept really too hard to understand? If something has type bottom it never returns. It's an infinite loop, throws an exception, terminates the process or whatever else you can think of that never returns normally.

Bottom can be used everywhere an expression of any type can be used. That makes sense as it won't ever be used anyway as something of type bottom never returns.

? : currently returns the common super type of its two possible result types. The common super type of any type and bottom is that type.

That's all there is to it. Bottom means "does not return", common super-type of anything and bottom is this anything.

foo := condition ? "some string" : Sys.exit(1)

foo has type String. (Assuming Sys.exit would be changed to have type bottom.)


Treat it generally just like Void (except that it also indicates that the function never returns). So don't allow something like lists of bottom or whatever and everything should be fine.

One additional rule: Methods of this type must not contain a return statement. But that's a pretty obvious rule.

Than you perhaps should find a different name for bottom in Fantom. All I currently come up with is something like NoReturn, witch I don't really like.

katox Sun 3 Jan 2010

Why did you narrow Statements as Expressions only to this? Rereading the original discussion there are only a few objections against the idea. And it doesn't really mean that we have to discard return everywhere, as JohnDG mentioned. I can't find the followup discussion maybe the reasoning against is there...

Sometimes, you can easily write the code in an alternative syntax - like if else converted into ? : but if you can't things get messy or tedious.

Noone is forced to actually use the values of "former-statements" so I'd say there is no harm unless there is some kind of an implicit rule which might bite you.

Ad Bottom type, I agree with helium.

brian Sun 3 Jan 2010

Why did you narrow Statements as Expressions only to this?

Well during that discussion the feature to omit return on a single statement solved my original problem. Also when I got further into thinking about the grammar and compiler I realized it something that was going to be very difficult to to.

For this case, I'm thinking just making throw an expression instead of a statement in the general case is the right solution. That would simplify the language and give us the ability to use throw with ?: expressions.

The question is does making throw an expression instead of a statement require an official Nothing type be introduced into the language? Helium's right it isn't that difficult a concept. But I don't think we really need to introduce the type formally - I think it is can an aspect of the type system internal to the compiler.

Or put another way, is there really any good reason to allow a programmer to annotate a method as Nothing? There are a couple boundary cases like methods which throw exception, go into an infinite loop, or something like Sys.exit. But Void works good enough for those. So my leaning is to not introduce the bottom/nothing type.

KevinKelley Sun 3 Jan 2010

map[key] ?: throw KeyNotFound(key)

has some utility, but seems like adding a special case when the real issue is tied up in the Statements as Expressions thread.

andy Sun 3 Jan 2010

has some utility, but seems like adding a special case when the real issue is tied up in the Statements as Expressions thread.

I agree - I think there are probably more common patterns that could be simplified if the root issue is solved (and then of course this one too). I know I've run across cases where this would help, I'll try to find some examples.

brian Tue 5 Jan 2010

Promoted to ticket #892 and assigned to brian

I will look at making throw a general purpose expression. I will not be enhancing the Fantom type system (it will be handled internal to compiler).

jodastephen Wed 6 Jan 2010

While considering this change, I'd suggest looking whether break, continue and retun can be handled in the same way, as they are in a similar vein (jumping control flow).

map[key] ?: return "Not found"
map[key] ?: throw Err()
map[key] ?: continue
map[key] ?: break

I can see all of these being useful, and they seem like a logical subset of statements.

helium Wed 6 Jan 2010

The bottom type would have some further advantages.

static Int method(Int x)
{
   if (x < 0)
      Sys.exit(1)
   else
      return x
}

Currently the compiler will not compile this completely valid method: "Must return a value from non-Void method".

tompalmer Wed 6 Jan 2010

Currently the compiler will not compile this completely valid method

That's a great argument for "does not return". I dislike exit, but I've made convenience methods for throwing exceptions before, and maybe other cases exist.

DanielFath Wed 6 Jan 2010

A quick question; Assuming Nothing is implemented how would that reflect on Void when it comes to Func? Would |->| become a |Nothing->Nothing| or some other combination of Nothing and Void?

tactics Wed 6 Jan 2010

Fantom isn't the right language to worry about the nuances of flow control. I don't really like the idea of a Nothing or allowing arbitrary statements to be used as expressions.

helium's exit example is a case where you just need a little coercion to compile your code.

static Int method(Int x)
{
   if (x < 0)
      Sys.exit(1)
      return 0
   else
      return x
}

That doesn't muddy up the code. The mere existence of Sys.exit is a far worse crime than adding a dummy return value. I use PHP at work, and seeing die statements haphazardly littered about makes me nauseous (among everything else in the language).

tcolar Wed 6 Jan 2010

I don't care for Nothing either.

To be that's mostly an academic concept that's really not needed in day 2 day code.

brian Thu 7 Jan 2010

While considering this change, I'd suggest looking whether break, continue and retun can be handled in the same way

That would definitely be nice (I keep running across these cases). The problem is that throw is an exception flow control that always makes sense. But sometimes return might be really confusing:

if (foo ?: return x) ...
return foo ?: return x

So I'm thinking we should just stick with throw.

I don't care for Nothing either.

I don't think this feature requires adding the Nothing type to Fantom. It might be handy, but I don't think it is something we need to to now. We can always add it in the future without breaking anything.

jodastephen Thu 7 Jan 2010

But sometimes return might be really confusing

This is the case with expression based languages in general:

Void doSomething(Int i, Str s) { ... }

// weird method call:
Str? foo := ...
doSomething(23, foo ?: throw NullErr())

// weird return
return foo ?: throw NullErr()

I'm thinking there really isn't much difference with return/'continue'/'break'. So, we either add the feature, and people can write code thats a bit daft, or don't add the feature, and stay with safe separate statements.

brian Thu 7 Jan 2010

This is the case with expression based languages in general:

I totally agree, the problem is that Fantom isn't an expression based language, it is statement based. So things like return, break, continue would be extremely difficult features to implement. I'd probably have to use an exception under the covers.

So I don't see this feature as a full move to an expression based language. Just one small change which makes throw an expression. But return, continue, break, if, while, etc all remain statements.

helium Thu 7 Jan 2010

I'd probably have to use an exception under the covers.

Could you explain why? I thought your current idea is to transform something like

Str? foo := ...
doSomething(23, foo ?: throw NullErr())

into

Str? foo := ...
if (foo == null) throw NullErr()
doSomething(23, foo)

The same transformation could be used for continue, break and return.

I'm not sure that I'd ever want to do this, but I don't see the problem with implementing it.

brian Thu 7 Jan 2010

The transformation of "?:" into bytecode isn't statement based, it is stack based within the VM. And in general that gets into stack balancing issues (.NET especially doesn't like you leaving a method with an unbalanced stack).

Obj? method(Obj? foo)  { foo ?: "xxx"  }

Is compiled into:

0:  LoadVar             1
3:  Dup                 sys::Obj?
6:  CmpNull             sys::Obj?
9:  JumpTrue            15
12: Jump                21
15: Pop                 sys::Obj?
18: LoadStr             xxx
21: Return

jodastephen Thu 7 Jan 2010

I think that you are creating a special rule here, just for throwables. That will be a gotcha I suspect. I think it should be all jump-style statements become expressions, or none.

I'm afraid I don't understand the bytecode problem. It seems like a simple source code manipulation. In your bytecode above, you could surely add anything you like at line 18, including continue, break and return. How else would a return normally work?

andy Thu 7 Jan 2010

I think that you are creating a special rule here, just for throwables.

I think I side with Stephen - just don't think this wrinkle is worth breaking the statement semantics over.

casperbang Thu 7 Jan 2010

Just listening in here, but I agree it feels counter-intuitive to Fantom's problem statement. All or nothing.

brian Thu 7 Jan 2010

Just to keep this discussion on track. There are several conflicting things being discussed:

  1. make ?: special case for throw
  2. make ?: special case for throw, return, break, continue
  3. make throw an expression instead of an statement
  4. make throw, return, break, continue all true expressions

I am proposing something very simple, change the grammar to make throw an expression instead of a statement. It is strictly a grammar/parser change, nothing else changes in the compiler or language. Just anywhere you can use an expression, you can now use a throw.

I am not proposing to make ?: a special case. I am not sure Stephen whether you are arguing for number 1 or 4.

Turning constructs like return, break, etc into special cases or making them full expressions is way beyond the scope of how I wish to change the language. If we were starting off fresh, then I'd love to create a pure expression language. But we're several years into a statement based language. If we want to revisit turning Fantom into an expression language then lets start a new topic. But I see this particular issue specifically as a single case, like we made return optional in single statement blocks.

jodastephen Thu 7 Jan 2010

I've been arguing for #2, make ?: special case for throw, return, break, continue.

I agree that #4 is a big change, and not really suited to Fantom's style now.

Unless I'm misunderstanding, I think #3 is very messy. Don't these then compile?

doSomething(23, throw Err(), "very odd")
Obj? a := throw Err()
b := map[throw Err()]

Restricting it to ?: seems safer and more obvious, as there is always a continuation path close to the ?: with a normal type.

brian Fri 8 Jan 2010

Don't these then compile

Yes, throw would be a true expression, not a statement. But I assume for example that is how Groovy already works today right?

For example whatever we do for ?:, I'd to work with normal ternary operator:

val := foo.isOdd ? foo + 1 : throw Err("not odd!")

So while we can't follow the model of true expression languages, I think throw is a case where we should. Just limiting it to ?: operators might be ok, but is actually way more complicated from a grammar and compiler perspective.

jodastephen Sat 9 Jan 2010

Both the ternary a?b:b and defaulting a?:b have the aspect of having a normal path through that is unaffected by the addition of the throw. In my view that converts this change from weird and bug-creating, to OK.

I'm firmly against making throw just a normal expression because of the weird things you can then do with it.

brian Sun 10 Jan 2010

I agree that really throw really only makes sense for a ?: b and a ? b : c expressions. I don't really have a problem restricting it to those constructs, but that makes the grammar and compiler a little more complicated.

At that point I might consider return (I guess break, continue too but they are hardly ever used). But stack balancing that might be tricky.

brian Fri 22 Jan 2010

Ticket resolved in 1.0.49

I have updated the grammar and compiler to support using throw as an expr in either a ternary or elvis expression:

<ifExpr>         :=  <ternaryExpr> | <elvisExpr>
<ternaryExpr>    :=  <condOrExpr> ["?" <ifExprBody> ":" <ifExprBody>]
<elvisExpr>      :=  <condOrExpr> "?:" <ifExprBody>
<ifExprBody>     :=  <condOrExpr> | <ifExprThrow>
<ifExprThrow>    :=  "throw" <expr>

So now you can do things like this:

map[key] ?: throw ArgErr("bad key: $key")

I poked around what it would take to also support return but the stack balancing complexity is way more than I want to bite off right now. But the grammar is setup to allow it in the future.

Couple things to note about this grammar change...

The elvis precedence was changed to match precedence of the ternary operator (which it should have had all along). But in some rare cases if you were using elvis with conditional expressions you might need to wrap things in parens.

Also, this grammar change removes the ability to chain elvis:

a ?: b ?: c    // previously allowed
(a ?: b) ?: c  // now you need to wrap in parens

The compiler internally treats a throw expression as the Nothing type:

Int x := 5
y := x.isOdd ? x : throw Err()

In this example the inferred type of y is Int since the throw expression doesn't evaluate to an actual value.

Login or Signup to reply.