Skip to content
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

Currying #818

Open
unlessgames opened this issue Nov 23, 2023 · 10 comments
Open

Currying #818

unlessgames opened this issue Nov 23, 2023 · 10 comments
Labels
proposal Proposal or discussion about a significant language feature

Comments

@unlessgames
Copy link

So I know there's an issue #75 from the past but currying got a bit lost in there so I just wanted to make a fresh single-purpose feature request for it.

add := (a, b) =>>
  a + b

add2 := (a, b) ->>
  a + b

would become

const add = (a) => {
  return (b) => {
    return a + b
  }
}

function add2(a){
  return function(b){
    return a + b;
  }
}

I vote =>>/>=> over ==>/~=> because

  • multiple arrow-heads represent the concept of multiple arrows better I think (a => b =>)
  • ==> is easier to confuse at a glance with => especially when using ligatures
  • there are ligatures for =>>/->>/>=>/ >-> while none for ~=>, ~->
  • if the tilde is placed higher in a font ~=> can look bad like ˜=>

I'd pick =>> over >=> because

  • it's less jarring to the eye while still being noticably different from =>
  • it's more convenient to type thanks to the repeating >
  • no misleading association with the fish operator (>=>) from Haskell
@bbrk24
Copy link
Contributor

bbrk24 commented Nov 24, 2023

In terms of practical use, I've less commonly needed that and more often something like (x) -> () -> .... Would there also be a shorthand for that?

@edemaine edemaine added the proposal Proposal or discussion about a significant language feature label Nov 24, 2023
@unlessgames
Copy link
Author

Not a specific shorthand but that could be done with the same general syntax using a placeholder argument at the end like

f := (x, _) =>> ...

The underscore here is nothing special of course, just an argument with name _ which you simply wouldn't make use of.

const f = (x) => {
  return (_) => {
    ...
  }
}

If you wanted to have typing support for this you'd need to do

f := (x : any, _ : void) =>> ...

Otherwise it would be nice to have a Unit type-like notation as an addition to this so that you could write (x, ()) =>> which would become to (x) => () => without the need for the placeholder.

@edemaine
Copy link
Collaborator

edemaine commented Nov 24, 2023

Could you give some (ideally real-world) examples where such currying is helpful/convenient? I think that would help the conversation.

I worry that curried functions are annoying to apply because f x y means f(x(y)) in Civet, not (f(x))(y) as in ML/Haskell etc. Of course, we could still write f(x)(y), but it's less nice... On the other hand, I use the current expansion order a lot (e.g. Math.sin Math.abs theta).

@bbrk24
Copy link
Contributor

bbrk24 commented Nov 24, 2023

Here's a couple examples 1 2 of where I'm using the (args) -> () -> ... pattern. They both take some information, and return an action to be executed at a later point.

@edemaine
Copy link
Collaborator

Ah yes, I've used that pattern before too. Honestly => => looks pretty good for this use case...

Similarly, note that the original examples can already be written as

add := (a) => (b) =>
  a + b

add2 := (a) -> (b) ->
  a + b

That is a lot of parentheses, though, so I can see why some shorthand might be nice, especially for a bunch of parameters... but I'd like to see an example where it's more than two. 🙂

@bbrk24
Copy link
Contributor

bbrk24 commented Nov 25, 2023

It turns out the space in the double arrow isn't even necessary; both Civet and CoffeeScript accept (x) =>=> x and (x) ->-> x.

@unlessgames
Copy link
Author

unlessgames commented Nov 28, 2023

Sorry in advance for explaining stuff which you already know, it felt better to put it all together like this...

So I think the main use of currying within civet would be to use alongside the pipe operator and when working with higher order functions in general. Additionaly a somewhat niche case would be writing FFI for or working with functions from purescript modules.

Of course most things you could do via currying can be solved through other means and I agree that since function application/calling is different from Haskell, using partial application with more than one argument is somewhat inconvenient and using a curried function with more than 2 arguments where only the first was partially applied would also be inconvenient.

A classic simple example would be printing with units. (all the examples assume we have currying function declarations)

print_as := (unit, value) =>>
  `${value}${unit}`

1.4
|> print_as "m"
|> console.log 

142
|> print_as "cm"
|> console.log 

Here currying is a convenient way to reuse a function without typing out lambdas everywhere.

But when applying multiple arguments it becomes increasingly more inconvenient because of the parens needed as @edemaine said.

print_as := (unit, precision, value) =>>
  `${value.toFixed(precision)}${unit}`
 
1.4142135
|> print_as("m") 4
|> console.log 

6.48074069
|> print_as("cm") 2
|> console.log 

I still think it's nice to have currying even in this manner but I agree that it's not ideal. The case where it becomes even less ideal is where we want to use the curried function that is left with more than 1 arguments not applied at the final destination.

For example passing a partially applied function to Array.map where we want to use both the element and the index. If currying was everywhere this would work nicely:

print_unit_indexed := (unit, value, index) =>>
  `${index} : ${value}${unit}`

[1, 41, 42].map print_unit_indexed "cm"
|> console.log

But instead we'd actually need to write

[1, 41, 42]
  .map (v, i) => print_unit_indexed("cm")(v) i

Which makes currying the opposite of handy.

Overall I think I rest this proposal for currying.


Now if I allowed myself to dream further I could envision a few helpers that could make currying more convenient. Take the following with a grain of salt, I haven't thought it through that much (similar to suggesting currying I suppose lol).

Curry powder

So Haskell has the $ operator to combat the inverse case of function application problems that would otherwise introduce parens. When you want to call a function with an argument while applying another function to the argument beforehand you can put $ to escape having to include the parens (. also exists to compose functions).

f ( g x ) (which would be just f g x in civet) can be written as f $ g x

Inversely in civet we could have something that does kind of the opposite like f . g . x would mean f(g)(x). This would make the example from before into

print_as := (unit, precision, value) =>>
  `${value.toFixed(precision)}${unit}`

1.4142135
|> print_as . "m" . 4
|> console.log 

Lambda pipe

Alternatively, there could be a pipe-like symbol that switched to curried call mode for the scope afterwards:

|\ f g x would transpile to f(g)(x)

Partial currying

The other glaring issue is with interfacing with the rest of javascript where expecting curried functions is not common, like with the Array.map example above. So what if we could define arbitrary groupings inside a curry definition. Of course this makes currying less modular but it might be more useful in the context of js.

fn := (a, b, (c, d)) =>>

would be the same as

fn := (a) => (b) => (c, d) =>

@edemaine
Copy link
Collaborator

edemaine commented Nov 28, 2023

To me, these examples seem to point to "explicit currying" (which is already possible with multiple arrows) as the obvious solution; it lets you control what gets specified when, and with single or multiple arguments, which lets you integrate with the rest of JavaScript. Here's how we'd write your examples:

print_as := (unit) => (value) =>
  `${value}${unit}`

1.4
|> print_as "m"
|> console.log 

142
|> print_as "cm"
|> console.log 

print_as := (unit, precision) => (value) =>
  `${value.toFixed(precision)}${unit}`
 
1.4142135
|> print_as "m", 4
|> console.log 

6.48074069
|> print_as "cm", 2
|> console.log 

print_unit_indexed := (unit) => (value, index) =>
  `${index} : ${value}${unit}`

[1, 41, 42].map print_unit_indexed "cm"
|> console.log

Personally I find (unit) => (value, index) => clearer than (unit, (value, index)) =>> but you might disagree there. (unit) => (value) => is a little verbose to write, but it also seems clearer to me than (unit, value) =>>. Are there scenarios where you want more than two or three "phases" of function evaluation? (I'm arguing that (unit, precision) => (value) => is more "useful" than (unit) => (precision) => (value) => in this setting. Maybe it requires more forethought, but argument ordering for currying always does I guess.)

I think where this approach falls apart some is when you have a function that's used in multiple different ways. For example, consider sub := (a, b) => a - b and you want to represent ($) => sub $, x or ($) => sub x, $. But this is a problem not fixed by currying; it's more of a placeholder issue. For example, #480 would write these as sub &, x and sub x, &.

@JUSTIVE
Copy link

JUSTIVE commented Nov 29, 2023

as a pipe & auto-currying lover, the proposal looks great and feels very natural to me. however, I get why explicit currying is better to be. but writing explicit curried functions is very painful when there are so many curried variants used in the codebase. So I wonder what if the compiler generates a curried function on-demand when the curried scenario occurs so that programmers don't care whether it's curried or not.

mad := (a, x, b) => a * x + b
---
const mad = (a, x, b) => a * x + b
mad := (a, x, b) => a * x + b

3
|> mad 1 2

3
|> mad 1
---
const mad = (a, x, b) => a * x + b
const mad_2_1 = (a, x) => (b) => a * x + b // note that _2_1 thing means the arity of curried function
const mad_1_1_1 = (a) => (x) => (b) => a * x + b // would be co-located by the original function so that it can be reused

mad_2_1(1,2)(3)
mad_1_1_1(1)(2)
function mad(a, x, b)
  a * x + b

3
|> mad 1 2

3
|> mad 1
---
function mad(a, x, b){ return a * x + b }
function mad(a, x){ return function (b){ return a * x + b }}
function mad(a){ return function (x){ return function (b) { return a * x + b }}}

mad(1,2)(3)
mad(1)(2)

or... just create whole permutations of each function's arguments, and let the bundlers do the tree shake...

not sure about optional arguments btw..

@edemaine
Copy link
Collaborator

A neat idea! But optional arguments are problematic, and in JavaScript, all arguments are optional. So I think you'd need to be explicit about which arguments should remain, which seems like we're back to placeholders again instead of currying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal Proposal or discussion about a significant language feature
Projects
None yet
Development

No branches or pull requests

4 participants