-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Function definition, dispatch, arguments, and organisation #19
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Nick Cameron <nrc@ncameron.org>
|
||
Note that if a non-optional argument has optional type, then it must be supplied (though could of course be `None`) and cannot be elided (c.f., optional arguments). | ||
|
||
If `self` has array type and the function is called with multiple (unlabelled) values with a single super-type, they will be packed into an array. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this behaviour? Maybe we could just not allow self
to have an array type. Variadics could be tricky to handle, as you call out below
This likely makes this feature interact poorly with array self arguments, and we should probably just not check for matching argument names in that case).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useful for points, etc, e.g., pt(0, 0, 0)
to create a point at the 3d origin relies on this behaviour. Typing out pt(x = 0, y = 0, z = 0)
would be a pain
|
||
An optional argument may not have an optional type, i.e., `a?: T?` is illegal for all `T`. If `None` is passed for an optional argument, that is equivalent to not specifying the argument, e.g., `foo(a = 0)` and `foo(a = 0, b = None)` are equivalent. | ||
|
||
Note that if a non-optional argument has optional type, then it must be supplied (though could of course be `None`) and cannot be elided (c.f., optional arguments). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's a good use-case for mandatory arguments of optional type like (a: num?)
-- I suggest we just don't allow this. It seems really limited compared to using an optional argument with mandatory type like a?: num
and will be a stumbling block for users. We can always relax that restriction later.
|
||
E.g., for `fn foo(self: num)`, `42 |> foo()` is equivalent to `foo(42)`. `42 |> foo(0)` is not allowed. | ||
|
||
E.g., for `fn foo()`, `42 |> foo()` is equivalent to `foo()`, `42` is ignored (in this example, it should trigger a warning, more generally the lhs may be used elsewhere). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why should this be allowed? Seems like an error to me. Is it just to avoid breaking the pipeline?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can be used in a nested function, e.g., a |> debug(start())
would desugar to debug(start(a))
|
||
[^self2]: This may remind you of method call syntax in other languages using `.` or `->`. It sort-of is. It also follows `|>` syntax from F#. Note that we use `.` in KCL for field access, including accessing functions if functions are stored in a field (in particular this happens with imported [modules](modules.md)). Note though, that using `.` just locates the function, it does not treat the lhs as the receiver/`self` argument. | ||
|
||
[^yikes]: Yikes! This seems a bit subtle and error-prone. However, it does mean that the following examples work: `path |> line(angle = perpendicular(), len = segmentLen())` (`perpendicular` and `segmentLen` are called with `self = path`) and `path |> mirror(line(from = (0, 0), to = end()))` (the non-self version of `line` is used, `path` is passed as `self` to `end`). To get other behaviour, the user can always break the pipeline and use a variable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will make helper functions pretty annoying to work with. Any helper function you call inside a pipeline will need to take and ignore the pipeline's current self argument. E.g. in your example below:
startSketch(on = XY) // : Sketch
|> startPath(at = pt(0, 0)) // : PathBuilder
|> line(rel = vec(6, 0)) // : PathBuilder
|> arc(rel = vec(3, 3)) // : PathBuilder
|> line(rel = vec(0, 6)) // : PathBuilder
|> line(to = start()) // : PathBuilder
|> enclose() // : Sketch
the function pt
has to take a Sketch as its self
, and vec
need to take a PathBuilder as its self
. If I instead want to replace pt(0,0)
with a call like origin()
then origin
also needs to take and ignore the self
arg.
I know it's been a bit confusing but I do like that %
solves this problem, by immediately clarifying which subexpressions use the LHS of the |>
vs. which subexpressions don't need it.
Instead of automatically passing the LHS as the first arg, could we instead get users to pass self
if they want that? E.g.
startSketch(on = XY) // : Sketch
|> startPath(at = pt(0, 0)) // : PathBuilder
|> line(rel = vec(6, 0)) // : PathBuilder
|> arc(rel = vec(3, 3)) // : PathBuilder
|> line(rel = vec(0, 6)) // : PathBuilder
|> line(to = start(self)) // : PathBuilder
|> enclose() // : Sketch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the function pt has to take a Sketch as its self, and vec need to take a PathBuilder as its self. If I instead want to replace pt(0,0) with a call like origin() then origin also needs to take and ignore the self arg.
I don't think that's right. If the functions don't have a self formal argument, then the pipeline object is ignored.
self
would interfere with self
from the function, but we could use a different keyword (or use something different for self
).
fn bar(): num {...} // error: duplicate names | ||
``` | ||
|
||
Exception: functions may have the same name if they have fully distinct `self` types. A function with no self argument is considered distinct to any functions with `self`. If the `self` types are unrelated, there is no restriction on the other arguments or return type. If the `self` types are subtypes, other arguments must match exactly[^contra] and return types must be covariant. Due to the `self`'array sugar, `T` is not considered distinct from `[T]` or `[T; 1]`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the motivation for this? Seems like this would make our hover and autocomplete support very difficult.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should work with hover and autocomplete since it can all be done statically. Not sure what your referring to exactly with 'this', but the general concept is to support virtual dispatch, e.g., in the example how line
is implemented as a free function and on sketch (and we might want to implement it differently on different kinds of sketch)
|
||
### Dispatch | ||
|
||
A function name whether imported from a module or defined locally, defines *a set of functions*. Likewise, referring to a function via a module (`moduleName.fnName`) references a set of functions. Passing or storing a function may refer to a specific function (`a = fn(...) { ... }`) or a set (`fn foo(...) { ... }; a = foo`); in the former case, no dispatch rules are applied (the latter case needs some thought and I would not support it initially). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand how fn foo(...) { ... }; a = foo
is a set of functions, the way I see it there's only 1 function here, it just has two names. I'm confused!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah sorry, that's not clear, it is a set with size one. In general there might be multiple foo
s with different self
s so the set might be larger
This document proposes several changes to how function work in KCL. It supports a lot of the ergonomic changes proposed elsewhere. Although it changes the implementation a lot, I believe the 'feel' of KCL functions will not change too much, other than removing the many flavours of similar function in std (e.g.,
line
,lineTo
,angledLine
,xLine
, etc are all replaced by the singleline
).There is a lot in this design doc and it might feel like too much complexity for a small, domain-specific language. However, most of this complexity exists to facilitate very simple and clear usage patterns, and the detail in the doc is to ensure it will all work as desired. Bear in mind that this doc is aimed at implementers, not users, and documentation for users would approach these topics in a very different way. I'm pretty sure that the complexity here fits well with the desired learning curve: beginners will not have to concern themselves with much of this, more advanced users can do more as they learn more. Skip to the end to see an elaborated example of how this all fits together and how it would feel to a user of KCL.