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

Proposal: Empty argument lists #129

Closed munificent closed 9 years ago

munificent commented 9 years ago

Right now, if a method takes no arguments, it has no argument list at all. () is a syntax error in Wren. That works great for "getters", things that are conceptually properties on an object:

"string".count
[1..3].min
(new Fiber).isDone

It's a little weird, but I think generally good for methods that generate something new but don't modify the underlying object:

123.toString
(-4).abs

Where it feels strange to me is methods that do modify the object:

[1, 2, 3].clear
(new Fiber).run

It's particularly odd when you call one of these methods from inside its class and use an implicit receiver. For example, here's a piece of the Set example:

class Set {
  // Other code...

  cleanup {
    // Remove duplicates in the underlying list...
  }

  count {
    cleanup // <--
    return _list.count
  }
}

Seeing a bare identifier on the // <-- line and having to realize it's there because it causes code with a side-effect feels spooky to me. Now, maybe this is just because I've inherited prejudices from other languages and I just haven't internalized Wren's way of doing things. But, given that Wren is designed to be familiar to people coming from those other languages, I feel this may be confusing.

Proposal

My proposed solution is to allow an empty argument list as part of a method's name. So just as you can overload foo and foo(arg), you could also define foo(). Note that this would not make the parentheses optional. foo and foo() would be two different methods with different signatures.

Since this is just another syntax for defining names, it adds little complexity to the language. The compiler has to take () into account when building a method name, but the rest of the VM is unaffected.

What I am concerned about is whether or not this makes it hard for library designers to know which form to choose. When should you use a getter versus an empty argument list? If the guideline isn't clear here, we'll get frustrated library designers and inconsistent libraries.

I think the reasonable rule is probably, "if it modifies any state (aside from internal-only caches, etc.), don't use a getter". So, clear and cleanup would become clear() and cleanup() (although the latter is a bit fishy since it doesn't modify anything publicly visible) while abs and toString remain getters.

Thoughts?

MarcoLizza commented 9 years ago

I second the empty arguments parentheses for the function/method calls.

Now it's a bit confusing and visually cumbersome. Moreover it's a benefit to have parentheses to spot the calls better.

I also second to preserve getters and setters (although we all now the can be implemented via method calls).

As I library/module designer I would use empty-arguments-list methods and functions for activities that (could) change the internal state of the class, possibly not returning anything. Getters are only observers for the internal state of the class, and they should not perform complex activities (but they can elaborate the result, of course).

kmarekspartz commented 9 years ago

EDSLs can be less clear. Ruby has done quite a bit with optional parentheses. Additionally, Cascading / Method chains can get lumpy.

Now, I'm assuming that this proposal is not suggesting the language forces one to use the () syntax if there is mutation, but rather that it is a convention generally used. I planned on an EDSL for Time similar to http://momentjs.com/ for zeckalpha/wren-io#6

munificent commented 9 years ago

As I library/module designer I would use empty-arguments-list methods and functions for activities that (could) change the internal state of the class, possibly not returning anything. Getters are only observers for the internal state of the class, and they should not perform complex activities (but they can elaborate the result, of course).

Yeah, this lines up with my intuition too. Defining "complex" gets pretty hand-wavey but hopefully most people know it when they see it.

EDSLs can be less clear. Ruby has done quite a bit with optional parentheses. Additionally, Cascading / Method chains can get lumpy.

Ruby also has the feature that parentheses are optional even when you have arguments, like:

foo.bar arg, arg

I like what that does for some DSLs but it makes the grammar really hairy, so I plan to stay away from that for Wren.

Now, I'm assuming that this proposal is not suggesting the language forces one to use the () syntax if there is mutation, but rather that it is a convention generally used. I planned on an EDSL for Time similar to http://momentjs.com/ for zeckalpha/wren-io#6

Right, there's nothing preventing you from doing all sorts of shenanigans in a getter. From looking at that API, I think it would be totally kosher to have fromNow and calendar be getters.

MarcoLizza commented 9 years ago

Defining "complex" gets pretty hand-wavey but hopefully most people know it when they see it.

Everytime the language does not dictates a way to accomplish something, it's always a fuzzy and questionable matter of individual style.

Over the years I stumbled upon some very "complex" setters, mostly due to inexperience of the programmer. In the majority of the cases getters have simple body structures... otherwise, they are methods.

Clear enough, uh? :)

kmarekspartz commented 9 years ago

I like what that does for some DSLs but it makes the grammar really hairy, so I plan to stay away from that for Wren.

Agreed. This is just the empty arguments case.

munificent commented 9 years ago

This is done now! A key motivation (in addition to being able to define methods with empty argument lists) is that Wren now has a well-defined concept of a "call signature":

// call                 // signature
obj.foo                 // foo
obj.foo()               // foo()  <-- note, different from getter
obj.foo(arg)            // foo(_)
obj.foo(arg, arg, arg)  // foo(_,_,_)
obj.foo = arg           // foo=(_)
-obj                    // -
obj - arg               // -(_)
obj[arg, arg]           // [_,_]
obj[arg, arg] = arg     // [_,_]=(_)

These show up in stack traces and debugger output. But, more importantly, they are used in the embedding API now. I'm hoping to use that in some changes in the embedding API I have in mind, though I'll have to see if it pans out.

I also changed some core methods to use ():

MarcoLizza commented 9 years ago

Horray! Well done! :+1:

munificent commented 9 years ago

:metal: