wren-lang / wren

The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
http://wren.io
MIT License
6.86k stars 550 forks source link

Question: Different Function Syntax for Methods and Anonymous Blocks #54

Closed JAForbes closed 9 years ago

JAForbes commented 9 years ago

I just have a few questions about the Function syntax in Wren and the reasoning behind it. I am on Windows so I couldn't get it running to find out for myself. I have never written a language so I have no idea what the internal trade offs may be, or why certain syntax variation may actually make the language simpler or faster.

For a method the arguments list is wrapped in parens, followed by the block braces.

callMe(fn) {
  // Call it...
}

An anonymous function, however, has its arguments surrounded by pipes.

{ |first, last|
  IO.print("Hi, " + first + " " + last + "!")
}

You can then pass this function immediately to another function as an argument.

callMe { |first, last|
  IO.print("Hi, " + first + " " + last + "!")
}

But if you want to pass multiple arguments to a function, including a function, you use a different syntax again. Wrapping the non function arguments in parens and leaving the anonymous function bare.

blondie.callMeAt(867, 5309) {
  IO.print("This is the body!")
}

When passing an anonymous function as a parameter to another function, it doesn't need to be instantiated. But when storing a function in a variable, it does.

var someFn = new Fn {
  IO.print("Hi!")
}

It seems to me the syntax could have a smaller footprint as follows.

Stroring anonymous function without new Fn, just like passing the function to a method.

var someFn = {
  IO.print("Hi!")
}

Calling a method with a function(s) as an argument, just like any other argument.

callMe({
  IO.print('First function!')
},{
  IO.print('Second function!')
},3,4,5)

An anonymous function's arguments stored in parens instead of pipes, just like methods.


(first, last) {
  IO.print("Hi, " + first + " " + last + "!")
}

Looking forward to trying Wren out, and hearing your thoughts.

munificent commented 9 years ago

An anonymous function, however, has its arguments surrounded by pipes.

Right. This is because using parentheses before the curly body would be ambiguous given the other syntax for passing a function to a method that also takes parameters:

someMethod (foo) { ... }

Here, is foo the first argument to someMethod, or the parameter for the function being passed to it?

When passing an anonymous function as a parameter to another function, it doesn't need to be instantiated. But when storing a function in a variable, it does.

The same amount of allocation happens either way, actually. The new Fn {} just returns the function you pass to it, so don't read too much into that new there. In both cases, you need to create an object, but only one. :)

var someFn = {
  IO.print("Hi!")
}

I really like the syntax you suggest here. I actually did pretty much this for the language Wren is based on. However, there are two things that get in the way:

  1. Wren has a "block statement" similar to C, Java, etc. Like:
var a = "outer"
{
  var a = "inner"
  var b = "second"
  IO.print(a) // inner
}

IO.print(a) // outer
IO.print(b) // ERROR: b is not in scope
  1. Sometime soon, Wren will have maps (think hashes/hashtables/objects in JS) and those will also use curly bodies:
{
  "key": "value"
}

That ends up with a lot of corners of the grammar fighting for those curlies and it gets ambiguous very easily.

Calling a method with a function(s) as an argument, just like any other argument.

Yes, we could definitely do that if there was a dedicated function syntax. Wren actually used to have that (see: https://github.com/munificent/wren/commit/d14601855917cafd9c73d8b9ecbc35a87de881aa). It looked like:

var someFn = fn() {
  IO.print("Hi!")
}

Where fn was a reserved word. In practice, though, most of the time you use functions, you are immediately passing them a method, like:

someList.map(fn() {
  ...
})

All of that ( () { }) punctuation really bothers me, which is why I added the syntactic sugar for creating a function directly by passing it to a method:

someList.map {
  ...
}

But, then, once I did that, I realized I didn't need the fn() { ... } syntax at all anymore. I could just make some built-in method that would return the block you pass to it in the rare cases where you don't need to immediately pass it to a method. The obvious name for this built-in method was... new Fn. :)

That let me tear out the old fn syntax (https://github.com/munificent/wren/commit/55c2c6ffb0bef31ce80acc3af1a4ace9b232e885) and simplify the language, which is always a good thing.

I later realized Ruby does the exact same thing.

The only part that bugs me is having two syntaxes for parameters lists, (a, b, c) for methods and |a, b, c| for functions. I'd like to make those consistent, but haven't been able to come up with a good way to do it that plays nice with:

someList.method(args, for, method) {
  ...
}
JAForbes commented 9 years ago

Thanks @munificent!

It is really interesting to hear the reasons things end up the way they do, and then to hear the ruby probably did the same thing for the same reason is just weird.

I can see how quickly the various uses of { ... } would introduce complexity. And again, I've never written a language so I don't know how hard it is to resolve that complexity.

But shouldn't the presence of the var <varname> = { ... } resolve the ambiguity?

var fn = {
  IO.print('I am a function')
}

var block = { 
  // unless you can have a named block the above shouldn't be ambiguous
}

And doesn't the : in the map resolve ambiguity too?

The only part that bugs me is having two syntaxes for parameters lists

Perhaps the argument list could always live in side the braces

var fn = new Fn { ( a,b )
  IO.print(a+b)
}

class Blondie {
  callMe { (fn)
    // Call it...
  }
}

That is a little different, but may resolve the ambiguity of

someList.method(args, for, method) {
  ...
}

someList.method(args, for, method) { (arg, def, for, anon, func )
  ...
}

That may just introduce more ambiguity in other places though ...

Maybe I'll try and implement my own language before asking anymore questions :smile:

munificent commented 9 years ago

But shouldn't the presence of the var = { ... } resolve the ambiguity?

Yup, but that means you could only define a function as the right-hand side of a variable declaration, which would be an annoying limitation. I think users expect to be able to write a function anywhere an expression is allowed.

And doesn't the : in the map resolve ambiguity too?

In general, yes. It gets a bit tricky with empty maps, blocks and functions. You basically have to just decide which one {} represents.

What gets nasty is if you allow arbitrary expressions as map keys, like:

{
  "some" + "long" + "map" + "key": "ok, finally a value"
}

If the parser doesn't know if it's parsing a map or a function until it sees that :, it may have to parse arbitrarily far ahead before it finds one. This isn't intractable, but it requires unbounded lookahead, which tends to make parsers nastier and can cause slowdowns if you aren't careful.

Perhaps the argument list could always live in side the braces

That's definitely an option. I don't know about you, but it looks a bit funny to me?

JAForbes commented 9 years ago

In general, yes. It gets a bit tricky with empty maps, blocks and functions. You basically have to just decide which one {} represents.

I hadn't thought of that problem. And I am not sure how you are going to handle empty blocks/maps now that you mention it.

I don't know about you, but it looks a bit funny to me?

Honestly, it does seem a little off, but, for me, a large component of beauty is utility.

This isn't intractable, but it requires unbounded lookahead, which tends to make parsers nastier and can cause slowdowns if you aren't careful.

Ah I see! That is exactly the kind of answer I was looking for. Thanks for taking the time.

munificent commented 9 years ago

And I am not sure how you are going to handle empty blocks/maps now that you mention it.

I believe it will work out. A {} by itself will be an empty map. A {} that appears where a function argument can appear will be parsed as an empty function. There's no use for empty blocks, so they just won't be possible.

JAForbes commented 9 years ago

A {} that appears where a function argument can appear will be parsed as an empty function

Which explains why your passed functions are outside of the arg parens (another thing I wondered). Otherwise you wouldn't know if the argument was a function or a map literal!

The only part that bugs me is having two syntaxes for parameters lists, (a, b, c) for methods and |a, b, c| for functions.

Just reflecting on this again. Could argument definitions always be in pipes, and argument passing always be in parens?

That way you can keep your parens outside the body without having ambiguity.

class Blondie {
  someFunc |a,b,c, fn| {

  }
}

...

blondie.someFunc(a,b,c) | c,d | {
  IO.print(c,d)
}
munificent commented 9 years ago

Could argument definitions always be in pipes, and argument passing always be in parens?

That's definitely possibly, but I think the unfamiliarity tax would be too high. :(

blondie.someFunc(a,b,c) | c,d | {
  IO.print(c,d)
}

Putting the | c, d | before the { is ambiguous with the infix bitwise | operator. Consider:

foo.bar | b | {}

Is that passing a one-argument function to bar, or calling the bitwise | on foo.bar, b, and an empty map? Syntax design is hard! :)

JAForbes commented 9 years ago

Syntax design is hard! :)

Indeed :)