KCL functions should use keyword args not positional args #4600
Description
Background
We don't want to keep multiple versions of the KCL stdlib around. We don't want to do a six-week release cycle of KCL 1.1, 1.2, etc etc. Simply too much to manage as a startup.
However, we also don't want to freeze the stdlib in place. We have to keep adding options to existing stdlib functions, without breaking user code. The easiest way to do this is by adding new arguments to stdlib functions, with default values. That way users can start using the new features when they want, but old code (written before the new option was added) doesn't break.
Problem
Currently arguments are positional, i.e. when you assign a value to parameter like circle(4, [2,3], $myCircle)
you are assigning based on the position from left-to-right. This has two problems:
- Say you keep adding new optional params with default values, e.g.
fn circle(radius, center, tag, opt1, opt2, opt3, opt4, opt5)
. What if users only want to setopt5
? They'll have to call the function like `circle(4, [2,3], $myCircle, null, null, null, null, 18). This sucks! It's hard to read, hard to count, hard to write. - To fix this, we sometimes use a single object as a param, e.g.
circle({radius = 4, center = [2,3]})
. This is more readable, but it's not clear to users why these weird squiggly braces exist.
Solution
Instead we will add keyword arguments, so the circle call would look like this:
// Old
circle(4, [2,3], $myCircle, null, null, null, null, 18)
// New
circle(radius = 4, center = [2,3], tag = $myCircle, opt5 = 18)
This means we can introduce new keyword arguments as time goes on, without breaking old code. We can also get rid of all the objects that KCL functions usually take, and break them down into multiple keyword args instead.
Spec
- KCL functions are declared with 0+ keyword arguments. Optionally, the first argument may be declared unlabeled. This argument can be passed without a label and must be the first argument. For example in
rem(@numerator, divisor)
thenumerator
param can be called without a label, like this:rem(10, divisor = 4)
. A function can declare at most one unlabeled argument. - It is an error to give an unlabeled param with a label when calling it. E.g.
q = div(numerator = 10, divisor = 3)
is an error. - If a function is called in a
|>
pipeline, and the unlabeled param isn't given, it's implicitly set to the LHS of the|>
operator. - Sketch functions like
line
andarc
will use@sketch
as their first parameter. This means users won't need all those%
in long pipelines. E.g. this program:
return startSketchAt([-l + center[0], -l + center[1]])
|> line([ l, 0], %)
|> line([ 0, l], %)
|> line([-l, 0], %)
|> line([ 0, -l], %)
|> close(%)
|> extrude(l * 2, %)
becomes
startSketchAt([-l + center[0], -l + center[1]])
|> line(end = [ l, 0])
|> line(end = [ 0, l])
|> line(end = [-l, 0])
|> line(end = [ 0, -l])
|> close()
|> extrude(length = l * 2)
Note this doesn't change the meaning of %
, and users can certainly keep using it as the first argument in a pipeline if they want to. It just isn't necessary in the majority of cases anymore.
For more detail and enhancements, see https://github.com/KittyCAD/kcl-experiments/pull/19/files
Plan
We cannot feasibly convert the entire language + stdlib to use keyword args in one big PR. Instead, the plan is:
- Add support for calling functions with keyword arguments. AST node will be called CallExpressionKw.
- Add support for defining functions with keyword arguments. AST node will be called FunctionExpressionKw.
- Add support for stdlib functions that accept keyword arguments (modifying the
derive-docs::stdlib!
macro to allow this and generate good docs) - Slowly convert the stdlib to use the new syntax
- Once the entire stdlib is converted, remove the CallExpression and FunctionExpression AST nodes
- Rename CallExpressionKw and FunctionExpressionKw to CallExpression and FunctionExpression