Improve currying in Julia Base

Before opening an issue on Julia GitHub I would like to ask two things (as I might have missed the relevant discussions - I could find some issues that are related but not something directly the same).

Question 1.

Many functions allow an optional function as a fists argument, e.g. sum, any, maximum, etc.
The question is if it would be acceptable to add the following definitions in Julia Base (I am giving one example):

sum(f::Function) = Base.Fix1(sum, f)

Question 2

Are there any plans for exposing Base.Fix1 to users (possibly with some nicer syntax/name). If this were provided then Question 1 would be obsolete as one could just use the syntax to get what is intended (assuming the syntax would be easy enough).

2 Likes

In almost all cases it’s fine to write

f(x) = y -> f(x, y)

There’s very little reason to export Base.Fix1 / Fix2 when anonymous functions exist. There are a couple of packages out there like FixArgs.jl and ChainedFixes that generalize Fix1 and Fix2 to the Nth argument when such a thing is needed.

If something like RFC: curry underscore arguments to create anonymous functions by stevengj · Pull Request #24990 · JuliaLang/julia · GitHub were ever accepted, and if it was the version that only ever consumes one function call, then perhaps it should produce a FixN like type.

I think this is kinda a bad idea because it makes it so that sum(x) has a very different semantic meaning depending on whether x::Function or not, plus not all callable objects are <: Function. However, it’s not a terrible thing.

3 Likes

For question 2, they were added to the manual recently (Essentials · The Julia Language) in https://github.com/JuliaLang/julia/pull/36094, so they are at least part of the public API now, but they don’t have a nicer name/syntax and are not exported.

4 Likes

Thank you for sharing these points. Let me give some more perspective of my question:

AFACIT Fix1 and Fix2 exist exactly because anonymous functions are not a good solution in many cases. You can dispatch on Fix1 and Fix2, but you cannot on anonymous function which is a crucial problem (and the reason of my question). If you write a package (like DataFrames.jl) that wants to be efficient you often want to have a special code for common operations. Essentially what I need is to be able to intercept signature like:

Base.Fix1{typeof(sum), T}) where {T}

and run a fast code on it. While with using an anonymous function it is not possible.

For this reason RFC: curry underscore arguments to create anonymous functions by stevengj · Pull Request #24990 · JuliaLang/julia · GitHub in its current state is not good for me (but if it produced FixN it would be OK).

I was aware of the packages, but I would prefer not to add more dependencies to DataFrames.jl if I could avioid this.

Additionally apart from dispatch benefits Fix1 and Fix2 reduce compilation latency, as most DataFrames.jl usage by regular is happening in global scope and defining anonymous functions causes recompilation on each call even if the operation is the same as already executed in the same session.

that sum(x) has a very different semantic meaning depending on whether x::Function or not

Good point. It is clearly bad.

5 Likes

This seems very significant to me. Regardless of whether this change is implemented, I would think the right way to do it is using a Callable trait instead of specializing on ::Function.

1 Like

Not all callable objects are <:Callable. So this will not resolve this. I used Function as in DataFrames.jl I prefer to stick to this as functions are not specialized on Function if it is passed through them, while if you allow e.g. Callable some specialization happens even with @nospecialize and it leads to excessive compilation latency unfortunately.

1 Like

Right, what I meant was that for most people in most circumstances, Fix1 and Fix2 do not confer any benefit over anonymous functions. The internals of Julia itself and DataFrames.jl however are somewhat unusual circumstances where these objects can be of benefit.

I didn’t mean to say that these are useless, just that they’re not necessarily the sort of thing Base should be exporting.

I think @jzr is talking about a hypothetical Callable trait that can detect if an object has methods, not the current flimsy thing sitting in Base as Base.Callable.

4 Likes

Yep.

2 Likes

Sure, I just wanted to give more explanation of the reason why I asked. In short:

  1. people want DataFrames.jl to be fast
  2. people are using DataFrames.jl functions in global scope passing custom functions as transformations.
  3. people care a lot about compilation latency (as now in most cases we are at the level of speed where most of the time is spent in compilation for MB sized data and not on doing actual computation)

I think @jzr is talking about a hypothetical Callable trait that can detect if an object has methods

Ah - then it would cover it, but probably still it would not be handled by @nospecialize as I would want it to be.

6 Likes