This is part of my series on the humanist programming language I’m building called (currently) HL. Read the rest here.
In this blog series I’ve been building a humanist programming language; meaning a language where the usability of the syntax and API is far more important than the speed or implementation details. This also means that the usability of the function calls is extremely important. Function calls essentially are the UI of a programming language. When you code you are stringing lots of function calls together, so it should be as easy as possible to remember parameters and to read code written by other humans.
When first planning this language I decided that I really liked named parameters, or ‘keyword’ parameters. If you aren’t familiar with them, it’s when instead of calling a function like this:
make_circle(50, 60, 20, 0, PI*2)
you can call it like this:
make_circle(x:50, y:60, radius:20, start:0, end:PI*2)
Keyword parameters have a lot of advantages over positional parameters.
- They are easier to remember since you use the names over and over.
- The IDE can automatically complete the name, so you don’t have to remember the exact spelling or full name.
- Order no longer matters. Put them in whatever order is useful to you.
- You can start with just a required argument then put in additional ones to refine your desires. The
chartfunction only requires a list of data, but once you see the initial chart you can add labels, scale the axes, and otherwise improve the result through additional keyword parameters.
We can make the keyword parameters even more useful by including default values for when the user doesn’t supply them. And of course, as in the rest of the language, we can specify that capitalization and use of underscores doesn’t matter. One less syntax error you have to worry about. (And in case you think this is unecessary, I literally helped my 11 yr old nephew with this problem yesterday).
So keyword parameters are cool but they have a big downside: they are more verbose. It’s more typing and sometimes, for certain APIs, very annoying. If you are creating lots of point objects, for example, you don’t want to type
point(x:5, y:5) over and over. There is already a convention that the two arguments to
y and always in that order. Yes, you do have to remember it, but it’s not that hard.
The other challenge of keyword parameters is pipelining. Pipelining is when the results of one function are passed as the parameter to another. Really it’s just function composition with some syntax sugar to avoid lots of nesting:
eat(pasta) >> digest(with:’pepto’)
The second is much easier to read. However, this only makes sense with indexed parameters. The result of the first function becomes the first argument of the second. How does this play well with only keyword parameters? Which keyword does it go to?
My solution is to offer both kinds of parameters. Generally users of the language will use keywords, but indexed parameters are available for certain cases.
Now we just have to define how arguments to a function are actually resolved to parameters inside the function without making life too hard on the function implementor.
To make it easy for both sides of the function call I’ve defined parameter resolution to follow these simple steps.
To resolve a function call we must
- Look up the function definition from the relevant scope (currently there is only global scope).
- Match all named arguments to the matching parameter slots in the function definition.
- Next match all indexed arguments to any empty spots, left to right.
- Use defaults for any remaining empty slots.
- Throw an error if anything required is still empty
- Execute the function definition.
I think this is pretty straightforward but I'm open to suggestions.
To actually create a function we need a syntax that lets the developer specify the names, a particular order for indexed arguments, and default values with requirements. This is my proposed function definition syntax: (assume the inside uses JS arrays for the implementation)
function sort( data=REQUIRED,
let result = data.slice().sort(where)
if(order === ‘descending’) return result.reverse()
In the code above the function
sort provides a default for each parameter.
REQUIRED is a unique symbol (not the string
"REQUIRED") which means that data argument must be supplied by the caller. It has no default value. The
order parameter has a string value
"ascending" for its default, so this will be used if
order isn’t supplied by the caller. Finally
where uses a constant for the default value. This constant would be defined elsewhere, probably nearby the definition of
I think the above system will work pretty well. In most cases the function implementor doesn’t have to think about parameter resolution while implementing the function. They just write code against some names. The caller of the function doesn’t have to care either. They can either use just named parameters, or use indexes for a few common cases where it is convenient.
However, there are a few issues to address.
First, the ordering of the parameters in the function definition matters, so the author of this function should take special care with those first few args. It’s good to be consistent with a set of related APIs. Function signatures can be added to but never changed; once you do it’s forever. Be careful. As an example, all of the List related functions use
data as the first argument since the actual list data will be used in all of them, and it's the most common thing to want to pipeline. They all work the same way and play together nicely by design.
Next, there is no way to say a parameter can only be used by keyword, not index. Someone could try to specify 50 parameters in order, and it would be a problem if the API ever changed in the future. What if there’s a way to say which ones can be indexed and which ones can’t? This is probably just a theoretical problem, but I want to mention it for posterity.
Another issue is that the default value of a parameter might be a constant that is not accessible to the caller. Is this a problem? It’s only used when the caller doesn’t supply it, so I suppose it should work. It might make inter-module calling a problem in the future, but we don’t have modules yet. It also might make on the fly documentation a problem if the constant isn’t visible to the IDE.
Finally, something like the
range() function doesn’t work well with my system. With one arg it’s
range(max). With two args it’s
range(min,max). The meaning of the first argument actually changes! This is bad design in the general case, but it seems to work okay in the specific case of
range. Python and other languages use this form. The system I’ve designed doesn’t handle this well. Instead you have to do
range(max) or go to keywords:
range(min:0, max:10). Is this a long term issue?
I’ve started implemented the parameter system in the new HL parser. The unit tests are mostly passing. Hopefully in the next week I’ll have all of the standard library converted over and available in the live editor. Until then, stay safe and stay warm.