SMLFamily / Successor-ML

A version of the 1997 SML definition with corrections and some proposed Successor ML features added.
191 stars 10 forks source link

clauses #4

Open RobertHarper opened 8 years ago

RobertHarper commented 8 years ago

I wonder whether anyone has a suggestion for how to fix the discrepancy in the syntax between fun bindings and case's. As everyone knows, it is monumentally annoying that the former use equal sign, and the latter use double-arrow; switching a fun to a case or vice-versa is painful for no good reason.

Without knowing what has already been suggested or done, let me suggest some options:

  1. Denigrate fun entirely. Except for super-small examples, I don't see a downside to this.
  2. Migrate fun to use double-arrow syntax so that the clauses look exactly like case branches.
  3. The same, but denigrate multiple clauses so that only fun f(x,y,z) => x+y+z would be permitted.

Bringing up this vexed question automatically brings up a few more. The most annoying is the needless shift-reduce conflict between two cases and between a case and a fun. Adding parens is very ugly imo, but ymmv. If we disallow clausal fun's, then one of these conflicts goes away immediately.

To manage the remaining shift-reduce conflict, why not allow curly braces as a form of parentheses for clauses? That way I could write case e of { p1 => e1 | ... | pn => en } if I'd like, but we needn't insist on it. The braces could be used more generally for bracketing any subgroup of clauses in a match, but I would guess the main use is as illustrated.

A related problem is the "fn" syntax, which also raises a problem with the scope of a match. Perhaps we can limit fn to have only one clause, so that fn (x,y,z)=>x+y+z is ok, but not a more general match? It would simply force the use of a case, which I find nicer anyway.

There is also the question of whether fn's should be name-able and self-referential, and whether one could or should admit mutually recursive fn's (which would define a labelled tuple, I suppose).

Incidentally, denigrating fun declarations entirely would have the advantage that we can use fun for lambda's, which I find nicer than the cramped "fn" (and much nicer than "function").

Has anything been done or suggested on these topics already? They seem so simple to fix, and it would certainly make things nicer syntactically.

pclayton commented 8 years ago

I've never found multiple fun clauses beneficial and consider them detrimental for large functions because:

Without multiple fun clauses, the difference between '=' and '=>' doesn't seem particularly annoying, so I suggest another option:

  1. Don't allow multiple clauses so that only fun f(x,y,z) = x+y+z would be permitted

Considering the other suggestions:

  1. If we avoid use of fun, where would we allow curried patterns? Nobody wants to write

    val f = fn x1 => ... => fn xn => e

    Maybe fun could then be a lambda function that allows curried patterns, e.g.

    val f = fun x1 ... xn => e
  2. I think fun f x => e looks weird. I always expect just a pattern on the left of '=>' but here we have the function f applied to its argument. Furthermore, when defining a value, I expect to see '='.
  3. Same as 2.

Using braces to work around the dangling-clauses problem doesn't look too bad:

case e of
  p1 =>
    case e1 of {
      p11 => e11
    | p12 => e12
    }
| p2 => e2

but I don't see it as any less ugly than the existing solution of adding parentheses:

case e of
  p1 => (
    case e1 of
      p11 => e11
    | p12 => e12
  )
| p2 => e2

Do you have an example of where braces are nicer?

Would it be useful if the existing syntax was modified to require parentheses to avoid the dangling-clauses problem? I think this would give syntax errors where type errors are currently given, which seems preferable.

olopierpa commented 8 years ago

On Sat, Apr 9, 2016 at 1:37 AM, Phil Clayton notifications@github.com wrote:

I've never found multiple fun clauses beneficial and consider them detrimental for large functions

Agreed.

Using braces to work around the dangling-clauses problem doesn't look too bad:

case e of p1 => case e1 of { p11 => e11 | p12 => e12 } | p2 => e2

but I don't see it as any less ugly than the existing solution of adding parentheses:

case e of p1 => ( case e1 of p11 => e11 | p12 => e12 ) | p2 => e2

Do you have an example of where braces are nicer?

Would it be useful if the existing syntax was modified to require parentheses to avoid the dangling-clauses problem?

IMHO, it would be much more readable, and less error prone to require a keyword for ending a case.

pclayton commented 8 years ago

IMHO, it would be much more readable, and less error prone to require a keyword for ending a case.

That's definitely worth considering. An obvious choice would be 'end', giving

case e of
  p1 =>
    case e1 of
      p11 => e11
    | p12 => e12
    end
| p2 => e2
end

If lambda functions aren't limited to a single clause, they could follow the same solution, e.g.

fn 0 => 0 | n => n - 1 end
fn n => n + 1 end

This might not be so bad given alternative syntax for a single clause lambda function, e.g.

fun n => n + 1
RobertHarper commented 8 years ago

Yes, “end” has often been suggested, and is perhaps a reasonable solution, particularly since “end” is already a reserved word.

The advantage of having parenthesization for matches is that it is useful in several contexts, not just the ambiguous case situation. One example would be exception handlers, which also create a conflict.

I would favor limiting “fun” to one-line multi-argument functions, in which case, as was suggested, the need to make “=“ into “=>” is greatly diminished.

Bob

On Apr 8, 2016, at 20:55, Phil Clayton notifications@github.com wrote:

IMHO, it would be much more readable, and less error prone to require a keyword for ending a case.

That's definitely worth considering. An obvious choice would be 'end', giving

case e of p1 => case e1 of p11 => e11 | p12 => e12 end | p2 => e2 end If lambda functions aren't limited to a single clause, they could follow the same solution, e.g.

fn 0 => 0 | n => n - 1 end fn n => n + 1 end This might not be so bad given alternative syntax for a single clause lambda function, e.g.

fun n => n + 1 — You are receiving this because you authored the thread. Reply to this email directly or view it on GitHub https://github.com/SMLFamily/Successor-ML/issues/4#issuecomment-207672312

ratmice commented 8 years ago

@RobertHarper

There is also the question of whether fn's should be name-able and self-referential, and whether one could or should admit mutually recursive fn's (which would define a labelled tuple, I suppose).

i've been dealing a fairly nasty recursive function where so far syntactically the best i've managed to come up with syntactically was the usage of a "one legged case" and a recursive fn,

in this example I appreciate both the lack of terminator for case, and the ability to define a recursive function without the need to repeat its name in every leg.

fun foo x = case x of {y, z} => let
   val rec bar =
   fn (A.Zotz(...)) => ...
    | (A.Frobnitz(...)) => ...
    | ...
   in bar end

is there something that I am missing about your question here, as 'fun' currently just being the derived form of 'val rec', seems to admit nameble/self-referential fn's

JohnReppy commented 8 years ago

I would be happy to see val rec go away. In your example, I tend to use

fun bar x = (case x
        of A.Zotz(...) => ...
         | A.Frobnitz(...)) => ...
         | ...)

to avoid repeating the function name.

ratmice commented 8 years ago

@JohnReppy

Hmm, I don't see how that deals with pattern matching x, x's contents (y, and z), A.Zotz etc are another parameter all together, so i would need nested cases which i'm not very fond of

JohnReppy commented 8 years ago

I left out the outer definition of foo, but I think that my definition of bar matches yours.

ratmice commented 8 years ago

I see, the thing that confused me was that I used 'x' in foo, you used 'x' in bar, shadowing the original x, I use this pattern when the function is recursive across currying e.g. the function can be recursive on both 'foo' and 'bar'

this was actually the impetus for using this, I don't want to give the x in "bar x" a name, its merely another variable in scope which I have to wonder if it really deserves a name, or did I have to assign it a name to apply the case expression to avoid repeating the function name.

using this I was able to have bindings at the top, and bindings in the leaves, and for things that really matter like recursion in the middle, and factor out anything which could be anonymous...

perhaps only useful as an interim step while refactoring a mess, but it has been helpful.