dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Lazy evaluation of function arguments #194

Open roman-vanesyan opened 5 years ago

roman-vanesyan commented 5 years ago

The primary use case of lazy evaluation of function arguments is to postpone initialization of arguments until the use of them in the function's body. e.g.

void foo(Bar bar) {
  // stuff...
  bar.baz(); // bar is initialized here...
}

foo(new Bar()); // ...not here

Another use case is logging. It's common to print debug info in development and disable it in production.

// stuffs..

// Logger prints data base on severity level
class Logger {
  void debug(String message) { /* ... */ }
  bool isEnabledFor(Level level) { /* ... */ }
}

// `largeDataChunk` produces a large data, that involves heavy computation.
logger.debug('Data ${largeDataChunk()}');

We can set logger's level to upper severity level and no output will be provided, but the string still is processed and largeDataChunk call occurs.

To prevent it, we can wrap the logger.debug call into the if statement, like:

// stuffs..

if (logger.isEnabledFor(Level.debug) logger.debug('Data {${largeDataChunk()}');

Until that it will give a huge number of additional boilerplate locs and breaks DRY idiom.

We even make the message argument of the debug dynamically typed and accept lambda function as argument.

class Logger {
  void debug(dynamic message) {
    if (isEnabledFor(Level.debug)) {
      String result;

      if (message is Function) {
        result = message();
      } else {
        result = message;
      }

      // stuff...
    }
  }
}

In this case it breaks static typing.

I propose to add lazy evaluation of function arguments, something like in D: https://dlang.org/articles/lazy-evaluation.html

lrhn commented 5 years ago

This would be an example of either "call-by-name" or actual lazy-evaluation of arguments (the difference is that the former evaluates the argument expression every time it's referenced, the latter only evaluates it the first time, then caches the value). Obviously, if you only reference a parameter once, there is no difference.

I think call-by-name is the more useful version because it allows the called function to evaluate the argument more than once for side-effects, if that's what it wants to do. Otherwise it can easily cache the value internally if it wants that.

Proper call-by-name, perhaps including passing statement blocks, would be a powerful feature that allows user-written control structures without introducing extra closures, like:

  void myWhile(lazy bool test, lazy void increment, void action()) {
     for (; test; increment) {
       action();
     }
  }
  myWhile(x < 10, x++, () { print(x); });
roman-vanesyan commented 5 years ago

What can I do to push toward this topic discussion?

lrhn commented 5 years ago

You can try to write a more precise definition of what the language feature should do, so there are details for people to discuss. Then hope other people are interested and want to discuss it.

You could focus either entirely on the debugging use-case, and see if there are ways to design a simpler function just for that, or go for the more general feature that can be used in multiple ways. The former is harder to argue for, but possibly easier to add to the language.

You can look at other languages and see what they have done for the same use-cases, and compare how those features would work with Dart.

In the end, all you can do is try to drive a discussion, and then hope that other people are interested in discussing the feature, and perhaps even lobbying for it. If there is no interest, and no pressing use-cases where the feature would help, it's very unlikely that anything will happen.

He-Pin commented 4 years ago

Scala's by name parameter