grain
grain copied to clipboard
Lang: `partial` operator
The partial
operator allows you to partially apply a function.
For example, look at this implementation of an add function:
let add = (a, b) => a + b
In let add2 = partial add(2)
, add2
is a function that accepts a single argument and adds two to it.
Arguments may also be skipped and provided later:
let map123 = partial List.map(_, [1, 2, 3])
map123(a => a * 4) // [4, 8, 12]
Do you need the partial
keyword?
@cullophid There's no technical reason why the keyword is necessary. We feel that when reading code in languages that allow partial application, it's not at all obvious that a function is being partially applied. You only really know that a partial application is happening if you know the type of the function being called.
We wanted something more explicit in Grain, and the partial
keyword is what we came up with. If we find that it's burdensome, we'll add syntax for it. This isn't at all the proposed syntax, but it could look similar to invoking macros in Rust: add!(1)
I'm curious about this; while I agree that reading through code and coming across a partially-applied function can be a bit of a brain-bender the first time you see it, I do wonder if having a partial
keyword is perhaps not quite the right solution. In JavaScript (to use a common example) it can be weird to see something like onClick: handleClick
when you know that the handleClick
function actually takes arguments. You could envision someone asking, "Shouldn't this be onClick: (event) => handleClick(event)
?"
Partial application feels similar to me: if a beginner came across code like
"foo bar baz"
|> String.split(" ")
|> Array.join(",")
That beginner could reasonably ask questions like, "Wait, doesn't String.split take two arguments? Which string is being split?" That's actually a good question, and beginners should ask it, but I don't think it implies that languages should be more verbose. For example, The following code would still provoke questions, I imagine:
"foo bar baz"
|> partial String.split(" ")
|> partial Array.join(",")
The beginner might now ask, "What does partial
mean? And doesn't String.split take two arguments? Which string is being split?" In other words, I don't think the keyword actually solves education problem. What if instead we tried to solve that problem with a two-pronged approach of helpful compiler errors and good dev tooling? TypeScript does a really good job with things like this: if you try to call a function that returns a promise, tsc
will ask, "Hey did you mean to put an await
here? Your code doesn't make sense without it." If someone is trying to use a function that isn't fully applied yet, you could raise an error like
Hey, you wrote
let x = 3 / add(1)
but `add(1)` is of type (number) => number while the expression is expecting a value of type Number.
Did you forget to fully apply the `add(1)` function? If you're unfamiliar with the concept of partial
application, read more here <link to helpful guide in documentation about partial application in Grain>
Anyway, just my two cents. 😄
Thanks for the feedback! 😄
In the cases where you're familiar with the functions being partially applied, I completely agree with you. If you're not familiar with the functions, say, in user code rather than standard library functions, it's really hard to tell that partial application is happening. A benefit of the partial
keyword is that it gives you something to search for—you're not sure what it means, but you can now look up partial
in the Grain docs, or with programming in general.
I'm all for helpful errors and good tooling though! OCaml has a form of this, where they warn if you have a statement that doesn't return unit. That works pretty well, but you miss out on the warning if you ignore
a function call—if parameters are added, your function call just doesn't happen. I haven't seen a solution for this, but I suppose we could look out for that exact use case.
We really like how explicit partial
is. We get that it's more to type, but we think the explicitness of it outweighs the extra characters.
I should have a draft PR up pretty soon so we can all play around with it and see how it feels. If we hate it, we can remove the keyword!
Sounds good. A draft PR with the ability to play with the syntax would be a good idea, and I definitely agree that the "google-ability" factor is higher with a keyword rather than the implicit approach!
I actually like the idea of a partial keyword, as you can easily have a "education" mode where by hovering the keyword you get a link on your IDE to the documentation.
And an interesting thing to think about always requiring the partial keyword, is that it makes clear where all the missing parameters will be collected.
let f = List.map(x => add(x, _), _)
/* probably means */
let f = z => List.map(x => y => add(x, y), z)
/* but you likely want want */
let f = y => z => List.map(x => add(x, y), z)
/* by having the explicit partial there is no ambiguity */
let f = partial List.map(x => add(x, _), _)
/* means */
let f = y => z => List.map(x => add(x, y), z)
/* and to achieve the naive transformation */
let f = partial List.map(x => partial add(x, _), _)
/* means */
let f = z => List.map(x => y => add(x, y), z)
I'd have to dig up the conversation in discord, but I believe @peblair wanted to always have the _
placeholders.