Previous Entry Share Next Entry
Local functions in QL
jducoeur wrote in querki_project
As mentioned yesterday, the current release of Querki includes a concept of "local functions". That's worth a bit of discussion unto itself. (This is only interesting for experienced programmers.)

QL is a pure functional programming language, stripped down to the bare bones -- to date, it's been arguably simpler than Scheme. That's intentional: simplicity is the name of the game here, because I want non-programmers to be able to do useful things in it, with the barest minimum of syntax.

But it's also intended to provide enough capability so that power users (that is, professional programmers) can quickly create fairly complex custom displays that fit their needs. And I've been starting to chafe at factoring problems when I try to do that.

When I try to build something really interesting (such as the new Documentation by Category page), I would wind up with duplicate "code". Or at least, duplicate text expressions, which are basically code. One of my mottos of programming is, "The Root of all Evil is Duplicate Data" -- duplication tends to make things much more difficult to maintain, and leads to errors. Indeed, that was one of the origins of Querki -- trying to do things in traditional wikis wound up requiring a lot of duplication, and that was causing me problems.

That's the motivation behind Local Functions. We've had a formal concept of Functions for a long time, but they're pretty heavyweight: you need to create a Function Property, and fill that in with the desired code. (Or alternatively, create a Thing and set its _apply Property to the desired code.) Either way, that winds up creating a *global* function -- useful for many cases, but it feels like an awful lot of work when you just want to be able to do the same thing several times in one expression.

So I've added local functions, which are a lot like functions in a typical pure-functional programming language. Let's look in more detail at the example I mentioned yesterday (whose full text can be found in the unit tests):
[[Album._instances -> _groupBy(Artists) -> +$byArtist
     _def $From First Album($artist, $field) = $byArtist -> _groupGet($artist) -> _first -> $field
     $From First Album(Blackmores Night, Link Name)
     $From First Album(Eurythmics, Artists -> Genres)
This is based on top of a small CD Space that is defined in the Querki test suite -- several Artists and Albums, and Genres that they fit into. Let's deconstruct this code, and understand what's going on.

The first line groups all of the Albums by Artist, and assigns that grouping to $byArtist:
Album._instances -> _groupBy(Artists) -> +$byArtist
Remember that +$byArtist is the new syntax to assign the received value to $byArtist.

The second line is the interesting bit, a function definition:
_def $From First Album($artist, $field) = $byArtist -> _groupGet($artist) -> _first -> $field
_def is something Querki hasn't had before, an actual keyword. I thought about other syntaxes, but decided that they were too clever and idiosyncratic to live -- let's just make a function definition look like a function definition.

The _def keyword must be followed by the name of the function; as with all local names, this must start with $. Note that, like all Querki names, it may have spaces in it. I'm still of mixed minds whether I like that style for function names: you're certainly welcome to use camel-case or underscores or dashes if you prefer, but Querki allows spaces as a general rule, and there was no reason to change that here.

The function name may (optionally) be followed by a parameter list. If you omit the parameter list, the function will still have access to the calling context, as well as to any bound names that are in scope. The parameters, again, must start with $. There is no concept of optional parameters yet, nor of calling parameters by name, but both might come in time.

Then comes = , to signify the function definition. This syntax may look odd to Java programmers, but it's common among functional languages, and gets the point across. Indeed, it's kind of literally true, in ways that we'll discuss below. Note that the equals sign must be surrounded by spaces.

Finally, we get the meat of the function. This is specifically a phrase -- a one-liner -- because that's what you most often want. I believe you can put in a full multi-line expression by surrounding it with parentheses, but that hasn't been tested yet. This phrase starts out by receiving the context that was passed into the function call, and may use any names that are in scope, as well as the parameters.

So in this case, we're operating entirely on the parameters, which should be an Artist, and a "field" -- really, in this case, an expression that we're going to apply to the first album for that artist. We go back to the $byArtist groupings that we computed in the first line; get the Albums for the specified $artist using _groupGet(); grab the first Album in the list; and apply $field to that.

The last two lines are function calls:
     $From First Album(Blackmores Night, Link Name)
     $From First Album(Eurythmics, Artists -> Genres)
The first one produces the name of the first album by Blackmore's Night; the second takes the first album by the Eurythmics, looks up its Artists (which is, of course, the Eurythmics), and produces their Genres. In practice, this winds up printing "Fires at Midnight\n[Rock](Rock)".

Hopefully that's mostly pretty intuitive, but I'd like to call your attention to one important nuance. Keep in mind that QL is, essentially, a macro language; that shows up in the way parameters are handled.

Think about how parameters work in a typical programming language. When you call a function with some parameters, those parameters are evaluated *first*, in the *calling* context; the results are then passed in to the function. You're probably so used to that that you don't even think about it most of the time.

That is *not* how parameters work in QL, intentionally. Instead, the expression itself is passed as the parameter. On the last line of the above example, we don't start by computing Artists -> Genres locally and pass the result into $From First Album -- instead, we pass the expression Artists -> Genres as the $field parameter, and we evaluate that expression when the parameter gets used.

The result is that you can kind of think of QL local functions as if they were macros. (They aren't actually, but the effect is similar.) That last line can be thought of as literally saying:
$byArtist -> _groupGet(Eurythmics) -> _first -> Artists -> Genres
This approach has its advantages and disadvantages. I find that it does occasionally bite me (and it's going to add special challenges to the long-term plan to make QL strongly-typed), but most of the time it's deliciously powerful. It allows you to easily do things that many languages find quite challenging -- even my much-loved Scala would have to resort to nuisance-to-define macros in order to do some of this stuff. And I'm finding that, in principle, I *usually* need that power to do what I need in Querki: the most common use for parameters is passing a Property or Expression to be evaluated in the right place.

One final note: as mentioned above, functions don't require parameters, and receive the context at the point of call. Here's another unit test, that illustrates this:
[[Album._instances -> _groupBy(Artists) -> +$byArtist
   _def $Show Artist = _first -> Link Name
   $byArtist -> _groupGet(Blackmores Night) -> $Show Artist
This variant calls the simpler $Show Artist function, which just receives the list of Albums passed into it. Note that the body of the function can be a text literal; this is how you can use the same text expression in several places.

I hope you find this useful. Local functions are really only aimed at the 1% of Querki users who are trying to do really serious stuff in it, but if you fall into that category, I believe they'll make your life easier...


Log in

No account? Create an account