WU0007: Partial Application operator
Concept
It'd be nice for jinko
to have an operator specific to the partial application of a function. Let's look at a few examples
Why not reuse the call operator?
Reusing the call operator (f(arg1, arg2)
) causes more issues than it solves in my opinion. It murks up the reading of code and makes reasoning about jinko
semantics harder. Furthermore, typechecking errors now become confusing, and often, not errors on the call site!
Let's take the following example:
// TODO: Should be named `add3` really, but isn't...
// I hope this does not confuse users!
func add(x: int, y: int, z: int) -> int { x + y + z }
where x = add(1, 2);
println("{x}")
would produce something similar to the following output:
error: source.jk:5:11: cannot format expression of type `func(int) -> int`
5 | println("{x}")
| ^
hint: no specialization exists for function `fmt` for type `func(int) -> int`
-> stdlib/fmt.jk:
15 | func fmt[T](value: T) -> string; /* builtin */
hint: consider specializing the function
5 | func fmt[T: func(int) -> int](value: func(int) -> int) -> string { /* TODO */ }
As a side note, we could also consider using a similar syntax for generic specialization.
Design considerations
Delimiting partial arguments?
Application of multiple arguments at once
Chaining applications
Alternatives
Let's keep going with our weirdly named add
function.
| arg0, arg1, arg2... |
syntax
where add1 = add |1|;
where res = add(2, 3); // typeck error
where res = add1(2); // typeck error
where res = add1(2, 3); // 1 + 2 + 3, fully applied
where add1_2 = add |1, 2|;
where res = add1_2 |3|; // same result, partial application but "complete"
<- (arg0, arg1, arg2...)
syntax
where add1 = add <- 1;
where add1_2 = add <- (1, 2); // how to work with tuples?
where add1_2 = add1 <- 2;
<- arg0 <- arg1 <- arg2
syntax
where add1 = add <- 1;
where add1_2 = add <- 1 <- 2; // no problem with tuple arguments here
where add1_2 = add1 <- 2;
curry
function?
One could think about having a curry
function turning a given function into its partial application
// simple case
func curry[T, Z](f: func(T) -> Z) -> func(T) -> Z {
|t| f(t)
}
// two arguments
func curry[T, U, Z](f: func(T, U) -> Z) -> func(T) -> func(U) -> Z {
|u| curry(|t| f(t))(u) // uuuuh, that's not valid
// how?
}
Or have curry
as an interpreter builtin? But then it contradicts with the goal of having the interpreter be as small
and simple as possible.