Thursday, August 22, 2013

Function Signatures as Parameters in Dart


One of the hazards of reading through other people's code is coming across syntax that you do not recognize. OK, so maybe that is not so much a hazard as it is awesome…

One of the things that I found while reading some Dart code was a function parameter that looked very much like a function signature. I don't remember where I first saw it, but it looked something like:
doSomething([cb()]) {
  // possibly some code here
  cb();
}
As I said, this was new to me, but the intention seems pretty clear: this is a way to document that certain function parameters are themselves functions. Of course, I could be wrong. Even if this is correct, how far can I take this syntax? Can I supply a default function if none is supplied? Can I specify the arity of the callback function? Can I specify types?

It so happens that I need to modify some callbacks in the dart dirty package, so this seems an opportune time to explore some of those questions. I will concentrate on the close() method in dart-dirty, which closes the file in which the dart_dirty noSQL database is stored. Currently that methods looks like:
class Dirty implements HashMap<String, Object> {
  // ...
  void close([cb]) {
    _io.close().
      then((_) => cb());
  }
}
The square brackets around the cb parameter indicates that enclosed parameters are optional, in the order specified. In this case, there is only one optional parameter, the callback. Now that I look at that method, there is an obvious problem when no callback is supplied to the close() method. Hopefully I can address that here.

First up in my exploration of function signature parameters is to simply add the method signature to the method definition:
  void close([cb()]) {
    _io.close().
      then((_) => cb());
  }
The dartanalyzer tool, which performs type analysis on Dart code, thinks that is perfectly fine, thank you very much. If I call close() with a callback function that prints out a message:
      db.close(()=> print("close!"));
Then the code runs and print out the message when the DB is closed:
close!
Interestingly, if I call close() with a string instead of a function, I get no warnings from dartanalyzer:
      db.close(' *** this is not a function *** ');
Obviously, I get a run time error when I try to “call” the string, but there is no type analysis notifications. I would presume that, at some point in the future, there would be a dartanalyzer warning from this. For now, this seems to be just a convention built into the language.
Update: this does work if I declare my db variable with an explicit type.

As I suspected, when I call close() without a callback, it blows up on me (treating null like a function). To fix that, I could add a conditional inside the method:
  void close([cb()]) {
    _io.close().
      then((_) { if (cb != null) cb(); });
  }
But really. Ew. Dart is a beautiful language that makes me want to write beautiful code. That ain't it.

Dart supports default values for optional parameters. Perhaps I can set this to an anonymous, empty function?
  void close([cb()=(){}]) {
    /* ... */ 
  }
Well, no, that does work. Compilation fails because default values need to be compile time constants and anonymous functions do not count:
'package:dirty/dirty.dart': Error: line 91 pos 20: expression must be a compile-time constant
  void close([cb()=(){}]) {
                   ^
I rather believe that anonymous functions could be considered compile time constants. Regardless, even if they are not, regular functions are compile time constants:
default_cb(){}
class Dirty implements HashMap<String, Object> {
  // ...
  void close([cb()=default_cb]) {
    _io.close().
      then((_) => cb());
  }
}
It is important that the default_cb function be defined outside of the class so that it is a function, not a method. With that, dartanalyzer is again happy, I can now call close() without any arguments, and there are no failures. If I had a suggestion for the Dart authors, it would be that specifying a function signature parameter automatically defaults to an empty function. Still, I cannot complain too much: that code is very clean.

The last question that I have tonight is about arity and types in the function signature parameter. It turns out that I can add types to the optional function signature:
default_cb(){}
class Dirty implements HashMap<String, Object> {
  // ...
  void close([cb(String message)=default_cb]) {
    _io.close().
      then((_) => cb());
  }
}
Interestingly, both the type and the arity are enforced by dartanaylyzer in the default option, the actual callback function supplied and the invocation used. The above gives me errors because default_cb does not accept a parameter and because the cb() call does not supply a value:
[warning] A value of type 'default_cb' cannot be assigned to a variable of type '(String) -> dynamic'
[warning] 1 required argument(s) expected, but 0 found
2 warnings found.
I can fix that by updating the default callback to accept a single parameter and to supply the string to the callback as the function signature says that I will:
default_cb(String m){}
class Dirty implements HashMap<String, Object> {
  // ...
  void close([cb(String message)=default_cb]) {
    _io.close().
      then((_) => cb(' *** dart dirty *** '));
  }
}
I also have to update calls to close() so that they supply a callback that accepts a string:
db.close((message)=> print("close! $message"));
This results in the following output:
close!  *** dart dirty *** 
If the default callback is updated to accept an integer instead of a string (e.g. default_cb(int m){}) or the callback is invoked with an integer dartanalyzer complains about mismatched types.

So in the end, it seems that function signature parameters are supremely powerful. Part of the callback hell from JavaScript with which so many developers suffer is trying to remember the kind of object that is supposed to be sent at various points. If the code is self-documented, especially to the point that simple analysis can be performed, then we are significantly closer to callback paradise than callback hell.

Of course, this should be a fairly rare thing in Dart code since Futures are baked into the core language. In fact, the bulk of what I need to do with close() can be solved by returning the Future from _io.close() instead of fussing with callbacks. Still, it is great to know that these message signatures are there if I need them.


Day #851

No comments:

Post a Comment