single-line function declarations #442

Closed
gavinking opened this Issue Oct 24, 2012 · 20 comments

Comments

Projects
None yet
5 participants
Owner

gavinking commented Oct 24, 2012

This is something that came up in tangential discussions in #363 and #377.

Currently we let you write:

function intRange(Integer from, Integer to) = Range;

or

value intRange = Range;

both of which define the function intRange() by reference, making intRange essentially an alias for Range. This was something I settled on a very long time ago, without it ever having been exposed to much discussion. I'm now more and more inclined to think that this is not the best way to go. I think we should instead give you the choice between:

function intRange(Integer from, Integer to) = Range(from, to);

and

value intRange = Range;

i.e. the parameters of the function would be in scope on the RHS of = and the type of the RHS expression would be expected to be the return type of the function, not the callable type of the function. This would:

  1. give you a lot more flexibility, allowing shit like:

    function intRange(Integer to) = Range(0, to);
    function sqr(Float x) = x*x;
    class IntRange(Integer to) = Range(0, to);
    

    Indeed, = something; would now just mean { return something; }.

  2. be much more consistent with the way type alias declarations work currently, where the type parameters are in scope on the RHS (but not the ordinary parameters, curiously, which would be fixed under this proposal).

  3. be consistent with the traditional notation in mathematics and, I think, usually more readable.

  4. simply look a lot more symmetric and "balanced".

The only subtle thing that I can think of here is with forward declaration and named arguments. Currently, the following things are natural:

//forward declaration
function intRange(Integer from, Integer to);
intRange = Range;

//named arg
void run(Progress p) { ... }
void repeat(void do(Progress p)) { ... }
repeat { do = run; };

Now, that seems, initially to clash with the new syntax. But not if we let you write the following as well:

//forward declaration
function intRange(Integer to);
intRange(Integer to) = Range(0, to);

//named arg
void run(Progress p) { ... }
void repeat(void do(Progress p)) { ... }
repeat { do(Progress p) = run(p); };

In fact, this seems to work out pretty neatly (assuming it can be easily parsed).

Indeed, given this new syntax repeat { do(Progress p) = run(p); };, we might wind up just simplifying the whole syntax of named argument lists!

Member

ikasiuk commented Oct 24, 2012

What about the => notation proposed in #377 (see comment #377 (comment))? This seems closely related to the syntax proposed here. Do you consider allowing both, or is the => notation off the table then?

Owner

gavinking commented Oct 24, 2012

This would be instead of introducing =>. Essentially I'm proposing that the semantics of => be used whenever we have an = after a parameter list.

There is one major advantage that => has: it lets you define lazy getters:

String name => firstName + ' ' + lastName;

But I guess we could achieve the same effect with an annotation:

lazy String name = firstName + ' ' + lastName;

(Or something.)

Do you consider allowing both, or is the => notation off the table then?

I'm trying to avoid introducing => because

  1. It's very slightly cryptic (though with its popularity in newer languages, increasingly less so)
  2. It would give us three syntaxes for defining a function instead of two.
Owner

gavinking commented Nov 2, 2012

we might wind up just simplifying the whole syntax of named argument lists!

So I want to go a little further down this path. But first I need to bring up a missing feature of named argument lists that we're going to need when we get up to using them to build user interfaces. What we need is the ability to declare a named value inside a named argument list, and access that value "further down the tree". For example:

Grid {
    value text = Text();
    Cell {
        Label("Name: "), text
    },
    Cell
        Button {
            label = "Greet";
            onClick(Click click) = sayHello(text.value);
        }
    }
}

Given the current syntax for named arg lists, the line value text = Text(); would be interpreted as an argument to a parameter named text of Grid, so we would need some special annotation or some shit to distinguish that this is just an ordinary lexically-scoped declaration.

But if we can now define functional arguments using the new syntax proposed above, i.e.

arg(Param param) = expression(param);

Then perhaps it's not as important to be able to do this anymore:

Button {
    label = "Greet";
    function onClick(Click click) { sayHello(text.value); }
}

And it would be OK to force you to use the actual annotation to distinguish "new" local declarations from argument functions:

Button {
    label = "Greet";
    actual function onClick(Click click) { sayHello(text.value); }
}

Note that this would be extremely consistent with how this stuff works for formal members and refinement! (We interpret a specification statement as refinement, or require you to annotate the declaration actual.)

Thoughts?

Owner

gavinking commented Nov 2, 2012

i.e. what I'm saying is you could write either:

Grid {
    value text = Text();
    Cell {
        Label("Name: "), text
    },
    Cell
        Button {
            label = "Greet";
            onClick(Click click) = sayHello(text.value);
        }
    }
}

or:

Grid {
    value text = Text();
    Cell {
        Label("Name: "), text
    },
    Cell
        Button {
            label = "Greet";
            actual function onClick(Click click) {
                sayHello(text.value);
            }
        }
    }
}
Owner

gavinking commented Nov 2, 2012

P.S. Of course, you would also be able to use the syntax for refining formal methods:

abstract class X() {
    shared formal Float x;
    shared formal Float y(Float z);
}

class Y() extends X() {
    x = 2.0;
    y(Float z) = z**x;
}

That's going to be really convenient.

Member

ikasiuk commented Nov 2, 2012

There's one thing that strikes me here: how increasingly similar named argument invocations and class bodies are getting. And especially when considering the planned ad-hoc refinement with named arguments (interface instantiation) it seems that a named argument invocation (of a type) basically is an inline object declaration. I think we should seriously explore if there is a way to merge the syntax of named arguments with that of object bodies.

Owner

gavinking commented Nov 2, 2012

FTR, I have always considered it a strong goal that they should be very
similar, syntactically and semantically.
On Nov 2, 2012 4:15 PM, "Ivo Kasiuk" notifications@github.com wrote:

There's one thing that strikes me here: how increasingly similar named
argument invocations and class bodies are getting. And especially when
considering the planned ad-hoc refinement with named arguments (interface
instantiation) it seems that a named argument invocation (of a type)
basically is an inline object declaration. I think we should seriously
explore if there is a way to merge the syntax of named arguments with that
of object bodies.


Reply to this email directly or view it on GitHubhttps://github.com/ceylon/ceylon-spec/issues/442#issuecomment-10017503.

Owner

FroMage commented Nov 3, 2012

function intRange(Integer from, Integer to) = Range(from, to);

I find that confusing because I get the impression that intRange is assigned the result of invoking Range(from, to). I understand that this is more in line with class aliases, but for some reason in the case of class aliases I don't have the same impression that the RHS constructor is invoked when defining the alias.

Owner

FroMage commented Nov 3, 2012

Regarding allowing local variables in named-param calls, I find it pretty confusing to read. Not only that but if we suddenly require actual annotations in there for regular params, it gets heavy.

I'd rather have a let expression, but even that I'm not really fond of adding at this point.

I think we should wait until the need for something like that arises. I haven't had it yet.

Owner

gavinking commented Nov 3, 2012

I think we should wait until the need for something like that arises. I haven't had it yet.

You will need it the very second you first try to write a user interface in Ceylon.

Owner

FroMage commented Nov 3, 2012

I never got that idea (yet). I am on the other hand pretty sure that I'd do this sort of trick in an HTML template:

html{
 body{
  a{
   if(userIsAdmin){
    id="mainLink";
   }
   href = "...";
  }
  ...
  a{
   if(!userIsAdmin){
    id="mainLink";
   }
   href = "...";
  }
 }
}
Owner

gavinking commented Nov 5, 2012

I have implemented the first step of this: making stuff like the following work:

function sqrt(Float x) = x**0.5;

And wow, updating the tests, I realize I like this syntax soooo much better!

Owner

gavinking commented Nov 6, 2012

The following is now essentially working:

function sqrt(Float x);
sqrt(Float x) = x**0.5;

Pretty cool :-)

Member

ikasiuk commented Nov 6, 2012

There's something I don't like about this:

Float f() { return x*x; }
Float f() = x*x;
Float f = x*x;
Float f { return x*x; }

One of these behaves different than the others, and I find that not very obvious. That was the big advantage of =>: it makes the difference immediately clear.

Also, I could imagine that getters would be a typical case where the one-line syntax would be useful because getters are often very simple. But as far as I can tell there's currently no way to apply that syntax to getters, isn't it?

Owner

gavinking commented Nov 6, 2012

@ikasiuk I agree, it's the one downside of this, and as mentioned above, it's likely we'll want to add some kind of lazy annotation or something...

Member

tombentley commented Nov 8, 2012

I used to be able to write

Callable<Integer, <Boolean>> f { throw;}
Integer foo(Boolean b) = f;

but this is no longer considered well-typed. I guess a lazy annotation might let me write this, but what I find really surprising is that by deferring the assignment

Callable<Integer, <Boolean>> f { throw;}
Integer foo(Boolean b);
foo = f;

it suddenly becomes well-typed. Is that a bug in the current implementation, or a "feature"?

Owner

gavinking commented Nov 8, 2012

Is that a bug in the current implementation, or a "feature"?

That's a feature. You're assigning a value of type Callable<Integer, <Boolean>> to a value of the same type.

Of course the following is not well-typed:

Callable<Integer, <Boolean>> f { throw;}
Integer foo(Boolean b);
foo(Boolean b) = f;
Owner

gavinking commented Nov 16, 2012

This feature has been implemented:

  • for declarations in ordinary blocks, and
  • for callable parameters.

FTR, @FroMage and @emmanuelbernard talked me into the fat arrow syntax =>. So, for example, it is:

void tenTimes(String f(Integer i) => i.string) =>
        print(", ".join(for (i in 0..10) f(i)));

To finish off this work, we need to do #466.

We also need to think some more on my idea above:

#442 (comment)

gavinking closed this Nov 16, 2012

Owner

quintesse commented Nov 16, 2012

What does it mean to have it in the parameter? Is it the default value?

Owner

gavinking commented Nov 16, 2012

Is it the default value?

Of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment