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? #554

Closed
rtzui opened this issue Mar 9, 2012 · 26 comments
Closed

Currying? #554

rtzui opened this issue Mar 9, 2012 · 26 comments
Labels
speculative Whether the change will be implemented is speculative

Comments

@rtzui
Copy link

rtzui commented Mar 9, 2012

I idea is relative simple, most languages lack it, but it's awesome and quite easy to be integrated I guess...
This example is obvious: ((x,y) -> x + y)(1,2)
When calling with only one parameter as ((x,y) -> x + y)(1) it raises a wrong number of arguments error. When a function is called with too few arguments it should instead return a anonymous function expecting the a missing one and executing it. So ((x,y) -> x +y)(1) should return (a -> ((x,y) -> x +y)(1,a)) ) (x,y) -> x +y is simply +(x,y). So +(1) would become (a -> +(1,a)). This function increments. Incrementing all fields of an array would be map(+(1),[1,2,3,4]). Of course it would be possible to simply create an anonymous function that increments, but that it would be cool if that happed automagical. Sometimes this aids a strange but useful programming style.

# could become a library function.
function >>=(a,b)
    if a
        b(a)
    end
end

function reportError(category::String, message::String)
   # write errors to different files, maybe send emails.
end 

# so some stuff that might fail
data=f(data)

>>=(checkData(data),reportError("serve"))

That a way more beautiful than
>>=(checkData(data),(message -> reportError("serve", message)))

as a consequence (if f is a regular function expecting 3 parameters) f(1,2,3) would be as valid as f(1,2)(3) or f(1)(2)(3)

@pao
Copy link
Member

pao commented Mar 9, 2012

This would conflict with multiple dispatch:

f(x, y) = x + y
f(x) = sin(x)

f(pi) # this must be sin(pi), not (y) -> (pi + y)

You might be able to do this sort of partial evaluation with a macro. That macro may already exist.

@choffstein
Copy link

I think the macro may look something like this:

macro defcurried(name, args, body)
    :($name = $reduce((body, arg) -> :($arg -> $body),
                      :($body),
                      reverse($args))))
end

function uncurry(fn, args)
    reduce((acc, a) -> acc(a), fn, args)
end

@defcurried plus, [x,y], x+y
uncurry(plus, [1, 2])

But I don't think that reverse($args) works...

@rtzui
Copy link
Author

rtzui commented Mar 9, 2012

My suggestion was to return this function just if otherwise there would be a wrong number of arguments error.
If

 f(x, y) = x + y
 f(x) = sin(x)
 f(x,y,z,a,b,c)=sqrt((x-a)^2+(y-b)^2+(z-c)^2)

Then:

 f(1) = sin(1)
 f(2,3) = 2+3
 f(1,2,3) = (a -> f(1,2,3,a))

 f(1,2,3)(4)= f(1,2,3,4) = (a -> f(1,2,3,4,a))

and so on.
I'm not sure if that's clever, but i see no conflict with multiple dispatch.

arrgh... This $%@! markdown ...

@StefanKarpinski
Copy link
Member

This definitely isn't going to happen. Even if this could somehow be made to work with multiple dispatch (and I really don't see how it could), it would be incredibly confusing. In a language like Julia, when I call a function with the wrong number of arguments, I want to know that I called a function with the wrong number of arguments. I don't want a function object where I expected a plain old result. This would also immensely complicate method dispatch, which is not acceptable from the implementation end. If you want to curry f(x,y), just write y->f(x,y) and it's very clear what's going on; it also doesn't privilege the arguments based on their ordering — you can just as easily write x->f(x,y).

@StefanKarpinski
Copy link
Member

For the markdown, you need to leave a blank line before and after your code blocks even when they're in triple backticks.

@JeffBezanson
Copy link
Member

Yes, no disrespect to currying, but this isn't julia's style.

@samuela
Copy link
Contributor

samuela commented Jun 6, 2015

On the other hand, Julia could adopt an explicit currying mechanism as in Pyret, Scala?, etc. This would circumvent issues with multiple dispatch and still provide an elegant currying experience. For example, if

f(x, y) = ...
f(x) = ...

then f(_, y) would simply desugar to a -> f(a, y) and f(x, _) would desugar to a -> f(x, a). Personally, I prefer this currying notation to implicit partial application stuff, since this notation makes everything explicitly clear. Furthermore, there is no ambiguity here as to how dispatch should work.

Currying is extremely helpful when done properly. I think Julia could benefit significantly from it.

@StefanKarpinski
Copy link
Member

The issue with using _ like that is the question of how much of the surrounding expression it consumes. For example, does f(_+2) mean x->f(x+2) or f(x->(x+2)) or f((x->x)+2)? Similarly, why doesn't f(_, y) mean a->f(a, y) instead of f(a->a, y)?

@kmsquire
Copy link
Member

kmsquire commented Jun 6, 2015

@StefanKarpinski, isn't that just a matter of choosing a convention?

Also, just curious: what would be the benefit of having f(_, y) map to f(a->a, y)?

@fcard
Copy link
Contributor

fcard commented Jun 6, 2015

From all the other usages I've seen using this type of currying, I'd say definitely f(x->(x+2)), so that it can be used like map(2*_, [1,2,3,4]) or filter(_>3, [1,2,3,4,5,6]). If you think that infix operations may cause confusion* then you can always encourage users to keep it obvious by writing them like
f(+(x,_)).

I think this is a nice and handy feature to have. It doesn't really scale to complex expressions but I think the point of the underscore is to keep simple things simple and avoid boiler-plate looking anonymous functions where the argument+arrow takes 50%+ of the definition. The do-blocks already solve the multiline anonymous function problem, so it would be a nice complement.

Edit: Also, it would make |> expressions a lot better looking:
data |> x->map(f, x) |> x->reduce(g, x) |> println vs
data |> map(f, _) |> reduce(g, _) |> println

Edit2: Just realized my previous pipelining example wasn't very good because you don't need to wrap one argument functions with anonymous ones. Way to make a fool of yourself! haha

*The most confusing example I can think of would be something like _+_+_ where it translates to
x,y,z -> +(x,y,z) vs _+_-_ where it would translate to z->x,y -> -(+(x,y),z). (assuming it would work like Scala's)

@samuela
Copy link
Contributor

samuela commented Jun 7, 2015

Right, I think the standard convention is to simply capture the most immediate parent expression. So, f(2 * _) desugars to f(x -> 2 * x), f(g(h(a, _))) desugars to f(g(x -> h(a, x))), etc. More precisely, the rule would simply identify function application expressions, check if any of the arguments are _, and then replace the function application expression with a lambda expression.

As @fcard points out, using multiple _s can be trickier. Scala manages it somehow but I don't think we would lose all that much by only supporting single parameter currying for now. Even just currying one parameter can clean up a lot of code.

@timholy
Copy link
Member

timholy commented Jun 7, 2015

I think it should be

     data
       v
map(_, _) |> sum(_) |> println(_)
    ^
    f

We could lead the ASCII-art renaissance!

@fcard
Copy link
Contributor

fcard commented Jun 7, 2015

Both Scala's and Pyret's way of dealing with multiple _s is to, for an function call with n underscores, make an anonymous function with n arguments in the order they appear in the function call.

I like it for some situations (like for reduce), but yes, we could live without it.

@timholy What we need is a digital-circuits-style mini-language:

data 
  |
  --------------v
map------->(_,_,_)--------v
f-------------^        (_,_)-----------v
sum---------------------^           (_,_)-------------->EXIT
println------------------------------^

But yeah. Are you saying that because you find that strange looking? I mean, I do too, I just like it better than the alternative :P We could always combine it with the 'pipelining' style proposed elsewhere, so that the _ becomes optional and is only used to position arguments:

data |> map(f) |> sum |> g(x,_,z) |> println

That could be implemented by simply adding the _ syntax and then having it expand before |> so that a |> f(x,_,z) becomes a |> y->(f(x,_,z)) becomes (y->(f(x,y,z)))(a).

@timholy
Copy link
Member

timholy commented Jun 7, 2015

That's pretty awe-inspiring. Maybe we should allow one to write julia code using SVG!

More seriously, yes, I personally find even basic pipelining somewhat dubious, but that's probably just because I've never written code in a language which supports it (other than bash, which is not exactly a great model for convincing me that it adds elegance 😄). I guess I see why

data |> map(f, _) |> sum |> g(x,_,z) |> println

is slightly easier to read than

println(g(x, sum(map(f, data)), z))

but one can write the latter as

println(g(x,
          sum(map(f, data)),
          z))

in which case I think the advantage disappears. The number of keystrokes is not dramatically different either way.

Anyway, while there's a part of me that's genuinely reluctant to embrace having too many ways of doing things, I don't care deeply about this issue, I was just having fun with it.

@fcard
Copy link
Contributor

fcard commented Jun 7, 2015

I am not super-fond of pipelining in Julia either, my argument was more like, "hey, given that we already decided to have pipelining, why not have this feature that makes it better?". I find pipelining more useful in other languages that already have a lot of nesting so that you can use it to differentiate 'statement-like' series of transformations. I think that this is a discussion for #5571, though.

Also, its good to have fun, and you did bring up a good point, so its all good in my book. :)

And related, _ does add complexity to the language and encourages a style that is not as prevalent in Julia as in other languages that employ it. I still like and several times wished it was there, but I guess this is the biggest argument against it.

@oxinabox
Copy link
Contributor

oxinabox commented Jun 7, 2015

@fcard You have seen Pipe.jl
It does something fairly similar to what you are talking about with _

I'm very keen on pipelines myself. (Though there are definately times where it hinders vs enhances readability)

@fcard
Copy link
Contributor

fcard commented Jun 7, 2015

@oxinabox Yes, Pipe.jl solves the case for pipelines, but it's not a complete implementation of _, and using macros for the other cases e.g. map(@pipe(2*_), [1,2,3]), makes it even more verbose than the anonymous function form, so I don't find much use in having a package for this. It seems like a nice package though, I think I will use it for any pipelining needs I may have.
Edit: I particularly liked your specialized use of _, it seems more useful than the one we would get naturally by having a general _.

It seems I touched a sensitive subject with the pipeline thing. It's one of the lesser uses I was thinking of for _, I just thought it was a cool bonus :P

Ah, and for anyone interested in the style of currying proposed originally, due to the call overloading in 0.4, it's possible to have a nice syntax for it. A simple (but not the most efficient)* implementation: https://gist.github.com/fcard/a37683ab25561953138a

*Updated with much faster version. I will probably reorganize it into a package or somethin', with maybe some other curry-based functionalities.

@samuela
Copy link
Contributor

samuela commented Jun 8, 2015

As far as I can tell, Pipe.jl is basically just a simple form of currying hacked together. Pipe.jl definitely doesn't fill this hole. If anything, I think its existence is a sign that incorporating currying into Julia should be seriously considered, since people are going through all the trouble of hacking some likeness of it together, even in such a restricted form.

@oxinabox
Copy link
Contributor

oxinabox commented Jun 8, 2015

No currying is different from what Pipe.jl is trying to achieve.
Pipe.jl is about redirecting where piped arguments go.

Currying is the replacement of a function that takes multiple arguments, with a function (functor?) that takes 1 argument, and returns a function which takes the next argument etc.

If one wanted to hack in currying, they would be hacking in partial application macros. They wouldn't be thinking about pipe redirection at all.

What you are calling "Explict Currying" is kind of similar, but it creates functions.
Pipe.jl, like the basic pipe operator just rearranged argument position.

I've not used a language with this "explicit currying", though I used F# a lot, which has normal currying -- that is to say all multiple argument functions were curried and so could be partially applied.

I think this normal implicit currying is antithetical (on a philosphical basis at least) to multiple dispatch.


I'm fairly certain what you want right now can be implemented in macros already.
And as such, would be better off in a package (like Pipe.jl) than in the main language.
Its good to keep stuff out of the main language.

(I was stuck awake last night thinking how fcard's joke could actually be implemented in macros.

@fcard
Copy link
Contributor

fcard commented Jun 8, 2015

@oxinabox
Explicit _ currying can be implemented as macros (as I did with 'implicit', and as I probably will do with this one as well if this flops). I just think that it would be much more useful as a language feature.

Like, you could implement infix operations with macros, but who would want to write @infix x + y every time? Anonymous functions themselves could have been macros, for/while loops, short function definition syntax, pairs, collections, the list goes on.

Some functionalities are indeed just as useful when implemented as macros, others not so much. I would certainly not use _ currying very often if it was one.

I think this normal implicit currying is antithetical (on a philosphical basis at least) to multiple dispatch.

That's true of anonymous functions in general, I suppose. But I don't think it matters that much. This, like anonymous functions are, would be just a convenience feature that doesn't interfere with the usage of multiple dispatch (unlike implicit currying, as discussed above).
Misread that as "explicit currying is antithetical..." :P

I was stuck awake last night thinking how fcard's joke could actually be implemented in macros.

Last year I actually implemented something similar, but with trees instead of circuits. Experiments like these are always fun!

@samuela
Its true that piping can benefit from currying, but they are separate. I personally find Pipe.jl to be an advanced version of piping, rather than a limited version of currying.

Hopefully I am not sounding pushy about this. I really like this type of currying partial application* but I leave entirely to the developers the decision of whether this is useful enough, and fits Julia's style enough, to warrant it being a feature. A very breaking feature at that.

*Got bothered by a functional programmer :P

@samuela
Copy link
Contributor

samuela commented Jun 8, 2015

I'm aware that currying and piping are two very different issues. More precisely, what I meant to convey is that Pipe.jl exists primarily because currying is not provided by Julia. If it were, the usual piping functionality would suffice. In other words, the essence of Pipe.jl is to introduce a currying-like mechanism for the sole use in piping expressions. (Feel free to correct me here if my understanding is still not correct!)

I agree with @fcard here that macros won't really do. Writing @curry f(_, x) everywhere is a real pain.

@johnmyleswhite
Copy link
Member

I think the real question for this thread should be: is this a conversation worth having now?

Aren't the function type system changes required to make higher-order functions fast vastly more important? Aren't the array changes for 0.5 vastly more important? The question shouldn't be: is this feature worth having? The question should be: is this the specific change that someone should be working on implementing today to achieve the greatest marginal change in Julia's quality? If not, can't we have this conversation in the future, when we actually know more about how Julia's functions will interact with the type system?

@fcard
Copy link
Contributor

fcard commented Jun 8, 2015

@johnmyleswhite That's a good point. I never really thought of this as a priority issue, though, I just wanted to discuss the merits of the feature for later reference, seeing that some interest was sparked. It was certainly not my intention to diverge attention to this, specially so close to the feature freeze. This being a closed issue, I didn't think I was doing that, but if this is the case, I apologize.
(Also, this being just simple syntactic sugar, I don't think that such deep issues such as Julia's type system are involved in this. But I guess that is not very important.)

@samuela How about we continue this conversation somewhere else? :P

@fcard
Copy link
Contributor

fcard commented Jun 8, 2015

@samuela After thinking about it for a bit (or after someone yelling PARTIAL APPLICATION in my ear), this is probably an issue separated from currying anyway. The best thing to do I think would be to come up with a bunch of use cases, a list of merits vs cons, etc, and maybe some time later post an issue here/start a thread in julia-dev about a possible syntax construct for partial application (PARTIA- ahem alright). If you want to discuss, feel free to email me. (we could probably do a few test implementations as well)

@samuela
Copy link
Contributor

samuela commented Jun 8, 2015

@fcard Sounds like a plan! If I get some free time I may take a stab at implementing partial application. I'm not familiar with Julia's internals so it may be quite an adventure.

@ysimonson
Copy link

This isn't as elegant as the original proposal, but you can do partials with something like this, right?

partial = (f, a...) -> (b...) -> f(a..., b...)

(So kind of like python's functools.partial.)

This should allow for, e.g:

adder = (x, y, z) -> x + y + z
partial(adder, 3)(2, 1)

LilithHafner pushed a commit to LilithHafner/julia that referenced this issue Oct 11, 2021
* bump version to ~0.33.0~ 0.32.1

* build docs on 1.3; test on (pre-)release (1.3 and 1.4), drop 1.2

* fix doctests

* release is non-breaking, using 0.24 documenter exactly
Keno pushed a commit that referenced this issue Oct 9, 2023
ViralBShah pushed a commit that referenced this issue Aug 12, 2024
Stdlib: SparseArrays
URL: https://github.com/JuliaSparse/SparseArrays.jl.git
Stdlib branch: main
Julia branch: master
Old commit: e61663a
New commit: 55976a6
Julia version: 1.12.0-DEV
SparseArrays version: 1.12.0
Bump invoked by: @ViralBShah
Powered by:
[BumpStdlibs.jl](https://github.com/JuliaLang/BumpStdlibs.jl)

Diff:
JuliaSparse/SparseArrays.jl@e61663a...55976a6

```
$ git log --oneline e61663a..55976a6
55976a6 Keep sparse solvers docs as before (#552)
95fd7ff Missing space in error message (#554)
b8a13ef implement in-place `ldiv!` for Cholesky factorization (#547)
1527014 Do not use nested dissection by default. (#550)
```

Co-authored-by: Dilum Aluthge <[email protected]>
lazarusA pushed a commit to lazarusA/julia that referenced this issue Aug 17, 2024
…aLang#55469)

Stdlib: SparseArrays
URL: https://github.com/JuliaSparse/SparseArrays.jl.git
Stdlib branch: main
Julia branch: master
Old commit: e61663a
New commit: 55976a6
Julia version: 1.12.0-DEV
SparseArrays version: 1.12.0
Bump invoked by: @ViralBShah
Powered by:
[BumpStdlibs.jl](https://github.com/JuliaLang/BumpStdlibs.jl)

Diff:
JuliaSparse/SparseArrays.jl@e61663a...55976a6

```
$ git log --oneline e61663a..55976a6
55976a6 Keep sparse solvers docs as before (JuliaLang#552)
95fd7ff Missing space in error message (JuliaLang#554)
b8a13ef implement in-place `ldiv!` for Cholesky factorization (JuliaLang#547)
1527014 Do not use nested dissection by default. (JuliaLang#550)
```

Co-authored-by: Dilum Aluthge <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
speculative Whether the change will be implemented is speculative
Projects
None yet
Development

No branches or pull requests