#2921 Proposed tweak to how () binds to no-arg methods that return function

brian Wed 19 Jun

I'd like to propose what would be a breaking change to make things work more as expected.

Consider this class where baz is a field that stores a function and bar returns the same function:

class Foo
  |->Str| baz := |->Str| { "result" }
  |->Str| bar() { |->Str| { "result" } }

The general philosophy of Fantom is that a field and a method without args is called without parenthesis basically look exactly the same. But in this case the behavior when you use the () operator on the results is inconsistent:

foo.baz()    // calls the func
foo.bar()    // gets the func
foo.bar()()  // gets the func and then calls it

I propose change the precedence of a method with no params that returns a function to bind the first () to to call the function, not invoke the getter method such that:

foo.baz()    // calls the func
foo.bar()    // calls the func

I think in most cases is that is what is probably expected. Any objections to that change?

Henry Thu 20 Jun

Hi Brian,

It's possible I don't fully understand the reasoning, but to me this seems like the change would actually make it less consistent, logically speaking.

I understand that foo.bar()() is a bit ugly/confusing, syntactically speaking, but shortcutting the call operator for this one specific instance I feel only confuses things further, and could be potentially limiting in cases that you might want to call foo.bar(), and then execute the returned method at a later point in code. (I'm thinking here specifically that could happen if the returned method is performing some kind of asynchronous task)

From a personal preference standpoint, It's also the case that I tend to try and add the empty parenthesis to any kind of method call - to differentiate it in code from invoking a field, since it makes it clear when viewing the code without the context of a syntax highlighter or something like that, for instance if viewing code on GitHub.

brian Thu 20 Jun

The reason it is important is to let Fantom provide a level of flexibility that dynamically languages can do with functions. Consider JavaScript where an object is just a map of name/values. If the value is a function, then its a method. So the dot operator looks up the function value, and the paren operator calls it.

You can already do this pattern in Fantom if you use fields. But you can't do it if you want to use methods. So it makes the pattern much less useful. Because in many situations you want the function to be computed or stored outside of a field.

So to me the benefit of the new design patterns it unlocks vastly outweigh leaving the design as it is.

The other thing I'm thinking about is letting you register callbacks on a function to listen when its called. This would provide a really powerful technique to simplify the boiler plate of callbacks.

Consider the moving parts of a callback function in domkit:

class SomeWidget 
  Void onEvent(|Event| f) { this.cbEvent = f }

  private Void fireEvent(Event e) { cbEvent?.call(e) }

  private Func? cbEvent  := null

You have three sides to this:

  • mechanism to register a callback(s) in a typesafe way
  • mechanism to store the callback(s)
  • mechanism to invoke the callback

But we could do this all in a typesafe manner with the Func API itself, something like this:

class Func 
  ** Create a version of this function that can register listeners
  This listenable()

  ** Register a listener callback each time this function is called.
  ** Raise exception is this function is not listenable.
  Void listen(|A, B, C, D, E, F, G, H->R| callback)

  ** Unregister a listener callback
  Void unlisten(Func callback)

Then in a type safe manner we can replace the domkit pattern:

class SomeWidget 
  |Event| onAction() 

// fire the action

// register callback on action where e is typed as Event
x.onAction.listen |e| { ... }

I'm not really sure about the API, but seems pretty powerful (thoughts welcome)

SlimerDude Sun 23 Jun

I am not in favour of this proposal.

It adds inconsistency and potentially severe breaking changes (that would only manifest themselves at runtime) to an otherwise very healthy and structured language.

I note that this functionality can already be mimicked in Fantom by using a field with an accessor.

|Str->Str| hello {
    get { |Str name->Str| { "Hello ${name}!" } }
    set { /* disable storage */ }
this.hello("Bob")    // <-- "Hello Bob!"

The traditional constructs of "fields" and "methods" are not Javascript "objects" - which is why there is the mis-match of behaviour and syntax. I am comfortable with the difference.

Being able to drop () in Fantom when calling a no-args method is subtle syntax sugar which merely helps blur a boundary in a situation where it need not exist.

To then repurpose () to execute other code flows seems, reckless.

If the aim is to embrace more dynamic behaviour, then Fantom already has syntax for this, the -> trap operator.

Moreover, the trap operator can be used today to achieve exactly the same result.

|Str->Str| hello() {
    |Str name->Str| { "Hello $name" }
override Obj? trap(Str name, Obj?[]? args := null) {
    method := typeof.method(name, false)
    if (method.returns.fits(Func#) && method.params.isEmpty)
        return (method.callOn(this, null) as Func).callList(args)
    return super.trap(name, args)

this->hello("Bob")    // <-- "Hello Bob!"

I tend to try and add the empty parenthesis to any kind of method call - to differentiate it in code from invoking a field

This is a very valid point. And the proposed inconsistent behaviour would be very frustrating to any new programmer coming from a traditional background.

Login or Signup to reply.