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

Create Base.Fix as general Fix1/Fix2 for partially-applied functions #54653

Merged
merged 83 commits into from
Aug 3, 2024

Conversation

MilesCranmer
Copy link
Member

@MilesCranmer MilesCranmer commented Jun 2, 2024

This PR generalises Base.Fix1 and Base.Fix2 to Base.Fix{n}, to allow fixing a single positional argument of a function.

With this change, the implementation of these is simply

const Fix1{F,T} = Fix{1,F,T}
const Fix2{F,T} = Fix{2,F,T}

Along with the PR I also add a larger suite of unittests for all three of these functions to complement the existing tests for Fix1/Fix2.

Context

There are multiple motivations for this generalization.
By creating a more general Fix{N} type, there is no preferential treatment of certain types of functions:

  • (i) No limitation that you can only fix positions 1-2. You can now fix any position n.
  • (ii) No asymmetry between 2-argument and n-argument functions. You can now fix an argument for functions with any number of arguments.
  • (iii) No asymmetry between positional arguments and keyword arguments. You can now fix a keyword argument.

Think of this like if Base only had Vector{T} and Matrix{T}, and you wished to generalise it to Array{T,N}.
It is an analogous situation here: Fix1 and Fix2 are now aliases of Fix{N}.

  • Convenience:
  • Less Compilation:
    • Using Fix* reduces the need for compilation of repeatedly-used anonymous functions (which can often trigger compilation of new functions).
  • Type Stability:
    • Fix, like Fix1 and Fix2, captures variables in a struct, encouraging users to use a functional paradigm for closures, preventing any potential type instabilities from boxed variables within an anonymous function.
  • Easier Functional Programming:
    • Allows for a stronger functional programming paradigm by supporting partial functions with any number of arguments.

Note that this refactors Fix1 and Fix2 to be equal to Fix{1} and Fix{2} respectively, rather than separate structs. This is backwards compatible.

Also note that this does not constrain future generalisations of Fix{n} for multiple arguments. Fix{1,F,T} is the clear generalisation of Fix1{F,T}, so this isn't major new syntax choices. But in a future PR you could have, e.g., Fix{(n1,n2)} for multiple arguments, and it would still be backwards-compatible with this.

Details

As the names suggest, Fix1 and Fix2, they can only inject arguments at the first and second index. Furthermore, they are also constrained to work on 2-argument functions exclusively. It seems at various points there had been interest in extending this (see links below) but nobody had gotten around to it so far.

This implementation of Base.Fix generalises the form as follows:

Fix{n}(f, x)

A type representing a partially-applied version of a function f, with the argument
"x" fixed at argument n::Int or keyword kw::Symbol.
In other words, Fix{3}(f, x) behaves similarly to
(y1, y2, y3) -> f(y1, y2, x, y3) for the 4-argument function f.

With this more general type, I also rewrite Fix1 and Fix2 in this PR.

const Fix1{F,T} = Fix{1,F,T}
const Fix2{F,T} = Fix{2,F,T}

With Fix{n}, the code which is executed is roughly as follows:

function (f::Fix{N})(args...; kws...) where {N}
    return f.f(args[begin:begin+(N-2)]..., f.x, args[begin+(N-1):end]...; kws...)
end

This means that the captured variable x is inserted at the N-th position. Keywords are also captured and inserted at the end.

This adds several unittests, a docstring, as well as type stability checks, for which it seems to succeed.

I also run the new Fix1 and Fix2 test suites added by this PR on the Fix version of each struct.

Examples

Simple examples:

To fix the argument f with an anonymous function:

with_f = (a, b, c, d, e, g, h, i, j, k) -> my_func(a, b, c, d, e, f, g, h, i, j, k)

whereas now it becomes:

with_f = Base.Fix{6}(my_func, f)

I have some usecases like this in SymbolicRegression.jl. I want to fix a single option in a function, and then repeatedly call that function throughout some loop with different input. Fix1 and Fix2 are not general enough for this as they only allow 2-argument functions.

A more common use-case I have is to set MIME"text/plain" in print for tests, which can now be done as Fix is no longer limited to 2 arguments:

s = sprint(Fix{2}(print, MIME"text/plain"()), my_object)

without needing to re-compile at each instance.

In a reduction:

Fix1 and Fix2 are useful for short lambda functions like

sum(Base.Fix1(*, 2), [1, 2, 3, 4, 5])

to reduce compilation and often improve type stability.

With this new change, you aren't limited to only 2-arg functions, so you can use things like fma in this context:

sum(Base.Fix{2}(Base.Fix{3}(fma, 2.0), 0.5), [1, 2, 3, 4, 5])

where this will evaluate as x -> affine(x, 0.5, 2.0).

Another example is a mapreduce, where you would typically want to fix the map and reduction in applying:

sum(
    Base.Fix{1}(Base.Fix{1}(mapreduce, abs), *),
    [[1, -1], [2, -3, 4], [5]]
)

In data processing pipelines:

Fix can be used to set any number of arguments, and of course also be chained together repeatedly as there is no restriction on 2-arg functions. It makes functional programming easier than with only Fix1 and Fix2.

For example, in a processing pipeline:

using CSV, DataFrames

affine(a, b, c) = a .+ b .* c

affine_transform_df(path) = (
    CSV.read(path, DataFrame)
        |> dropmissing
        |> Fix{1}(filter, :id => ==(7)) # Use like Fix2
        |> Fix{2}(Fix{3}(affine, 2.0), 0.5)    # Multiple args
        |> Fix{2}(getproperty, :a)
)

affine_transform_df("data.csv")

For dispatching on keyword-fixed functions:

Say that I would like to dispatch on
sum for some type, if dims is set to an integer. You can do this as follows:

struct MyType
    x::Float64
end

function (f::Base.Fix{:dims,typeof(sum),Int64})(ar::AbstractArray{MyType})
    return sum(ar; dims=f.k.dims)
end

which would result in any use of Fix(sum; dims=1) operating on
Vector{MyType} to call this special function.

Real-world examples

I would like to use this in my code. Here are some examples:

For example,

function michaelis_menten(X::AbstractMatrix, p, t::AbstractVector)
    reduce(hcat, map((x,ti)->michaelis_menten(x, p, ti), eachcol(X), t))
end

which could now be done with map(Fix{2}(michaelis_menten, p), eachcol(X), t), reducing compilation costs, and avoiding any potential issues with capturing variables in the closure.

Another one would be this:

    candidate_info_data = DataFrame((
        name = string.(candidates),
        score = (x -> round(x, digits = 3)).(summary_scores.mean),
        uncertainty = (x -> round(x, digits = 2)).(summary_scores.std),
        q25 = (x -> round(x, digits = 3)).(summary_scores_q[!, "25.0%"]),
        q75 = (x -> round(x, digits = 3)).(summary_scores_q[!, "75.0%"]),
    ))

with this you could write Fix(round; digits=3) and not need to re-compile an anonymous function for each new outer method.

Features in other languages

Here are some of the most related features in other languages (all that I could find; there's probably more)

Groovy's .ncurry

(Expand)

In Apache Groovy there is the <function>.ncurry(index, args...) to insert arguments at a given index. This syntax is semantically identical to Base.Fix.

From the documentation

In case a closure accepts more than 2 parameters, it is possible to set an arbitrary parameter using ncurry:

def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d)
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)
  1. the volume function defines 3 parameters
  2. ncurry will set the second parameter (index = 1) to 2d, creating a new volume function which accepts length and height
  3. that function is equivalent to calling volume omitting the width
  4. it is also possible to set multiple parameters, starting from the specified index
  5. the resulting function accepts as many parameters as the initial one minus the number of parameters set by ncurry

Python's functools.partial

(Expand)

In Python, there is no differentiating between args and kwargs – every function can be passed kwargs. Therefore, functools.partial is semantically similar to Fix:

def f(a, b, c, d):
    return a + b * c - d

f_with_b = functools.partial(f, b=2.0)

f_with_b(1.0, d=3.0)

which would be equivalent to Fix{2}(f, 2.0).

C++'s std::bind

(Expand)

In modern C++, one can use std::bind to insert placeholders. This is semantically closer to an anonymous function in Julia, though it binds the arguments in a way similar to Fix1 and Fix2 do:

void f(int n1, int n2, int n3, const int& n4, int n5);
 
int main() {
    using namespace std::placeholders

    auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
    f2(10, 11, 12)  // f(12, g(12), 12, 4, 5)
}

The C++ approach was also briefly mentioned on discourse back in 2018.


Closes:

Related issues:

Other approaches:

  • FixArgs.jl which stemmed out of Generalize Base.Fix1 and Base.Fix2 #36181
    • Note that this package takes a much different and more extensive approach to this problem using macros (see docs), so is likely not in-scope for merging to Base. I have written the Base.Fix in this PR from scratch based on the same patterns as Fix1 and Fix2 but with varargs.
  • AccessorsExtra.jl
    • The FixArgs implementation in this package is much more closely related to this PR. It works in a similar way although though stores the full signature with a Placeholder() set to the replaced arg.
  • FastBroadcast.jl defines an identical (aside from keywords) struct here for internal use

Semi-related issues:


  • Edit 1: Made Fix work for Vararg so that you can insert multiple arguments at the specified index.
  • Edit 2: Added keywords to Fix.
  • Edit 3: Switched from Fix(f, Val(1), arg) syntax to Fix{1}(f, arg).
  • Edit 4: After triage, added back the keyword arguments, and rewrote Fix1 and Fix2 in terms of Fix.
  • Edit 5: Added some validation checks for repeated keywords and non-Int64 n
  • Edit 6: Restricted the number of keyword arguments OR arguments to 1, and made struct more minimal.
  • Edit 7: After second triage, various cleanup and simplification of code
  • Edit 8: Removed the keyword argument. Now Fix{n} is exclusively for a single positional keyword argument.

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 2, 2024

Questions:

  1. Should this be called Base.FixN or Base.Fix? I moved to Base.Fix which seems to make the aesthetics much better. But I can change it back if you want. Changed to Base.Fix{n} which reads nicer.
  2. Do you want me to add keyword arguments too? I think it should be fairly easy. It would just store a NamedTuple in addition to the Tuple args. Then we could have things like Added this as well. Let me know if you'd rather it be removed.

@MilesCranmer MilesCranmer changed the title Create Base.FixN for general partially-applied functions Create Base.Fix for general partially-applied functions Jun 2, 2024
@MilesCranmer MilesCranmer changed the title Create Base.Fix for general partially-applied functions Create Base.Fix as general Fix1/Fix2 for partially-applied functions Jun 2, 2024
@admsev
Copy link

admsev commented Jun 2, 2024

@gooroodev can you review this?

@gooroodev

This comment was marked as spam.

@jakobjpeters
Copy link

+1 for Base.Fix and keyword parameters!

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 3, 2024

The implementation turns out to be pretty simple and everything is type stable and compiled. I do like it, it feels like this is what a general Fix ought to be.

julia> sum_1 = Base.Fix(sum; dims=(1,))
(::Fix{typeof(sum), 0, Tuple{}, @NamedTuple{dims::Tuple{Int64}}}) (generic function with 1 method)

julia> sum_1(ones(3, 2))
1×2 Matrix{Float64}:
 3.0  3.0

Just for now I've left it so you can fix both the args and the kwargs:

Should that be removed (forcing the user to specify args OR kwargs; not both), or is this actually better?

@LilithHafner LilithHafner added the triage This should be discussed on a triage call label Jun 3, 2024
@aplavin
Copy link
Contributor

aplavin commented Jun 3, 2024

More general fixargs would indeed be nice to have!

Are Fix1 and Fix2 also getting propagation of kwargs and additional args in this PR? Like Fix1(f, 1)(2, 3; x=4) === f(1, 2, 3; x=4).

And just a minor comment regarding usecase examples: the closure stability example is easily resolved in current Julia as well, without any additions. Simply replace function g() with function g(x=x, y=y), and f becomes type stable.

Other approaches <...>

Also see a short & performant implementation in https://github.com/JuliaAPlavin/AccessorsExtra.jl/blob/master/src/fixargs.jl. It takes a somewhat different approach compared to this PR: basically, stores a tuple of fixed args = (1, 2, Placeholder(), 4), and when applying fixed_f(3) puts the 3 in place of Placeholder, resulting in f(1, 2, 3, 4).

That approach is more flexible in allowing to fix any arguments, eg 1, 2, and 4 – not just consecutive args, as this PR does. Basically the conceptual difference is between "fix this arg(s) and propagate others" (this PR) vs "propagate this arg and fix others" (AccessorsExtra.jl). Though the latter can be generalized to allow Placeholder{1}, Placeholder{2}, ... as well, to support multiple non-fixed arguments.

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 3, 2024

Are Fix1 and Fix2 also getting propagation of kwargs and additional args in this PR? Like Fix1(f, 1)(2, 3; x=4) === f(1, 2, 3; x=4).

No, Fix1 and Fix2 are exclusively for 2-argument functions without kwargs. I would be open to extending that of course if triage asks for it.

Re: closure example, yes I agree there are many ways to solve it. I just wanted to give it as an example because Base.Fix1 and Base.Fix2 are convenient shorthands for solving this problem, and Fix would be another more general one.

Also see a short & performant implementation in https://github.com/JuliaAPlavin/AccessorsExtra.jl/blob/master/src/fixargs.jl. It takes a somewhat different approach compared to this PR: basically, stores a tuple of fixed args = (1, 2, Placeholder(), 4), and when applying fixed_f(3) puts the 3 in place of Placeholder, resulting in f(1, 2, 3, 4).

Thanks!

To summarize, that linked package uses:

function (fa::FixArgs)(arg)
    args = map(x -> x isa Placeholder ? arg : x, fa.args)
    fa.f(args...; fa.kwargs...)
end

The implementation on this PR roughly does, for N>1:

function (f::Fix{F,N,T,K})(args::Vararg{Any,M}; kws...) where {F,N,T,K,M}
    return f.f(args[begin:begin+(N-2)]..., f.x..., args[begin+(N-1):end]...; f.kws..., kws...)
end

I should note I don't feel particularly attached to either. For the same scenario, which one is easier on the compiler? I would expect they compile to the exact same thing in either case, so does it even matter? And if the Placeholder approach, how would it look like if you had multiple args?

With the Placeholder I do like how you could put potentially put args at different locations. Although to actually use that feature it would require a different syntax closer to C++'s bind or #24990.

I guess you could potentially do some syntax like Fix(f, (1, 3, 5), (x, y, z)) but it might be more readable to use an anonymous function at that point. I think I prefer limiting to Fix1(f, x) -> Fix(f, 1, x).

@aplavin
Copy link
Contributor

aplavin commented Jun 3, 2024

No, Fix1 and Fix2 are exclusively for 2-argument functions without kwargs. I would be open to extending that of course if triage asks for it.

I see... It just seems to be the most minimal straightforward extension of the current state.

I guess you could potentially do some syntax like Fix(f, (1, 3, 5), (x, y, z))

I had in mind more like Fix(f, (x, Placeholder(), y))(z) == f(x, z, y) (and Fix(f, (x, Placeholder{1}(), y, Placeholder{2}()))(z, w) == f(x, z, y, w) if needed).
The main benefit of this approach is being able to cleanly fix any arguments: for example, get(collection, Placeholder(), default) and get(f, Placeholder(), key) aren't possible with this PR.

@MilesCranmer
Copy link
Member Author

I see, thanks. I guess what you had in mind is semantically similar to #24990.

Let's just wait for triage to give an opinion on all of this as I want to hear initial thoughts from them.

@mikmoore
Copy link
Contributor

mikmoore commented Jun 3, 2024

I would opt for one of the proposals (of which there are several) that goes all the way and form a general closure type here. Further, it's not much more work to allow inputs to be re-ordered into arbitrary positions.

We should use the name Fix if we specify the position of the closed arguments (which would require the "open" arguments to be applied in-order) and something else if we specify the positions of the "open" arguments directly.

In terms of specifying the "open" positions, something like this (the name needs work):

struct Closure{P, F, T} # require T<:Tuple?
  f::F # function to be called
  fixed::T # fixed arguments, to be inserted in positions not indicated in P
end

# pass positional arguments to positions 3 and 1
# fill the rest from the closed values
Closure{(3,1)}(foo, (12, 14))(13, 11) == foo(11, 12, 13, 14)

# still need a syntax to indicate where Vararg
# inputs would go, if we want that
# would they be passed as a tuple or splatted?

# backwards compatibility
Base.Fix1(f, arg1) = Closure{(2,)}(f, (arg1,))
Base.Fix2(f, arg2) = Closure{(1,)}(f, (arg2,))

# provide a macro syntax
Closure{(3,1)}(foo, (2, )) == @closure (x, y) -> foo(y, 2, x)
# the difference between a @closure anonymous function and
# a true anonymous function is that the @closure version would
# build an explicit struct
# this would block constant propagation
# but might avoid recompilation, just like Fix1/Fix2

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 3, 2024

I will state up front that I really really want to avoid making a macro for general anonymous functions, and spurring yet another 500-comment thread on currying syntax like #24990. I would like to keep this PR relegated to implementing a Fix that acts as a general version of Fix1 and Fix2.

Above anything else, all I want is for

Base.Fix(f, n, arg)

to work. This is something I see as a much-needed feature. With that syntax alone you could already chain together a sequence of Fix to fix any arguments – something you can't do with Fix1/Fix2.

All of the other features I put into this PR are not necessary. Even the fixed vararg and kwargs are optional from my point of view (but still nice if people are on board). If triage only wants Fix(f, n, arg) I would still be very happy with that. And it would be easy to extend Fix if we want more features later, without needing a breaking change.

For more general closure types I think it's worth having a longer discussion elsewhere.

@mikmoore
Copy link
Contributor

mikmoore commented Jun 3, 2024

Above anything else, all I want is for Base.Fix(f, n, arg) to work.

I hate to be the bikeshedder, but part of my post was protesting this name. The indicated proposal means that Base.Fix(f, 1, args...) == Base.Fix2(f, only(args)) (note n=1 vs Fix2). I strongly dislike the switch from indicating the position of the "fixed" argument to the position of the "open" argument. I think this an unreasonable mental burden for how avoidable it is, hence the vote for a different name.

Also, the indicated syntax cannot be type stable without Val so I would go ahead and just make that TypeVar a la Base.Fix{POSITION}(f, args...). If one passes a non-Val for your n, it's already unstable so might as well just have them set the TypeVar directly (stable or not). But once one is already passing one position, I don't see the issue with setting multiple.

I don't actually care whether there's a macro or not. That said, I don't think I think that the macro I proposed runs up against many of the ambiguities that tanked the curry proposals because the macro I proposed does not curry. It simply uses anonymous function syntax with the goal to create a Closure object that behaves exactly like the anonymous function would have if every capture was localized via let. It also adopts the same compilation implications as Fix1/Fix2, but that's not semantically visible.

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 3, 2024

I hate to be the bikeshedder, but part of my post was protesting this name.

Oh I see, sorry, I misinterpreted. If it's just the name I'd certainly be happy to rename it. Closure or Bind sound fine to me.

The indicated proposal means that Base.Fix(f, 1, args...) == Base.Fix2(f, only(args)) (note n=1 vs Fix2). I strongly dislike the switch from indicating the position of the "fixed" argument to the position of the "open" argument. I think this an unreasonable mental burden for how avoidable it is, hence the vote for a different name.

I see, what about if if n came first? Then it would be Base.Fix(1, f, arg) == Base.Fix1(f, arg).

Edit: Oops, I realized you said Fix(f, 1, args...) == Base.Fix2(f, only(args)) – that is incorrect.

Also, the indicated syntax cannot be type stable without Val so I would go ahead and just make that TypeVar a la Base.Fix{POSITION}(f, args...). If one passes a non-Val for your n, it's already unstable so might as well just have them set the TypeVar directly (stable or not). But once one is already passing one position, I don't see the issue with setting multiple.

I wondered this too but it turns out it's not an issue because the constructor is so simple! If n is known at compile time, the compiler will just do constant propagation, and it's the same as if you had written Val(n). You can check this in the LLVM.

It's the same reason writing out Val(1) itself isn't unstable – compiler will just do constant propagation to Val{1}().

@DilumAluthge
Copy link
Member

Hi @admsev, please do not use the JuliaLang/julia repo as a testing ground for developing your AI tool.

@mikmoore
Copy link
Contributor

mikmoore commented Jun 3, 2024

I see, what about if if n came first?

Actually I slightly preferred the function as the first argument. While there isn't much reason to use this particular constructor with do, it still matches the Julia convention of functions first. It really was simply that Fix(... 1 ...) actually was the analog of Fix2 and Fix(... 2 ...) was the analog to Fix1. A non-Fix-related name would remove it enough that this difference would not impose a mental burden (compare to in/contains).

It's purely my aesthetic preference, but I still like the typevar syntax (a la Bind{1}(foo, 3) for x->foo(x,3)). I agree there's no performance difference because of constant propagation (in either case), I just think it's a cleaner syntax and also makes it very clear that the value should be compiler-known or else instability will ensue. Your current constructor simply puts the value in the typevar anyway. If we used a typevar syntax, then I would say that the position should be the first typevar (since it must always be specified while the remainder can be inferred from field inputs).

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Jun 3, 2024

Wait, I don't understand why you say that Fix(f, 1, arg) is equivalent to Fix2(f, arg)? Shouldn't it be the opposite?

For example:

x = 0.5
f = Fix(/, 1, x)
g = Fix1(/, x)

@test f(2.0) == g(2.0)

@LilithHafner LilithHafner removed the needs news A NEWS entry is required for this change label Jul 27, 2024
@LilithHafner
Copy link
Member

Triage is happy with merging Fix{N}(f, x) to replace Fix1 and Fix2 (but keep Fix1 and Fix2 for compat)

Some folks want more, some want to stop at Fix{N}, but everyone is happy with merging support for one positional argument and no keyword arguments.

The justifications based on "makes compilation faster" are sound for now, but it's not good to work around compiler limitations.

Some folks (including Jeff and I) really prefer the minimal design because there are fewer arbitrary choices.

@LilithHafner
Copy link
Member

Clarification:

  • Remove the Fix{:kw} functionality from this PR
  • Do continue to pass through keywords in the context of Fix{1}(sort!, [1,2,3])(rev=true)

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Aug 1, 2024

All suggestions implemented.


(Edit: ignore the other comments I posted, I temporarily forgot how _stable_typeof specialised)

@LilithHafner LilithHafner merged commit 3d99c24 into JuliaLang:master Aug 3, 2024
7 checks passed
@LilithHafner LilithHafner removed the triage This should be discussed on a triage call label Aug 3, 2024
@LilithHafner
Copy link
Member

Thanks for accommodating all the design changes and shifting the implementation as needed! I'm excited to have this in 1.12 :)

@MilesCranmer
Copy link
Member Author

Awesome, thanks!

@MilesCranmer
Copy link
Member Author

MilesCranmer commented Aug 7, 2024

Fix{N} is available in Compat.jl v4.16.0 for anybody interested in using this feature early.

lazarusA pushed a commit to lazarusA/julia that referenced this pull request Aug 17, 2024
…tions (JuliaLang#54653)

This PR generalises `Base.Fix1` and `Base.Fix2` to `Base.Fix{N}`, to
allow fixing a single positional argument of a function.

With this change, the implementation of these is simply

```julia
const Fix1{F,T} = Fix{1,F,T}
const Fix2{F,T} = Fix{2,F,T}
```

Along with the PR I also add a larger suite of unittests for all three
of these functions to complement the existing tests for `Fix1`/`Fix2`.

### Context

There are multiple motivations for this generalization.
**By creating a more general `Fix{N}` type, there is no preferential
treatment of certain types of functions:**

- (i) No limitation that you can only fix positions 1-2. You can now fix
any position `n`.
- (ii) No asymmetry between 2-argument and n-argument functions. You can
now fix an argument for functions with any number of arguments.

Think of this like if `Base` only had `Vector{T}` and `Matrix{T}`, and
you wished to generalise it to `Array{T,N}`.
It is an analogous situation here: `Fix1` and `Fix2` are now *aliases*
of `Fix{N}`.

- **Convenience**:
- `Base.Fix1` and `Base.Fix2` are useful shorthands for creating simple
anonymous functions without compiling new functions.
- They are common throughout the Julia ecosystem as a shorthand for
filling arguments:
- `Fix1`
https://github.com/search?q=Base.Fix1+language%3Ajulia&type=code
- `Fix2`
https://github.com/search?q=Base.Fix2+language%3Ajulia&type=code
- **Less Compilation**:
- Using `Fix*` reduces the need for compilation of repeatedly-used
anonymous functions (which can often trigger compilation of new
functions).
- **Type Stability**:
- `Fix`, like `Fix1` and `Fix2`, captures variables in a struct,
encouraging users to use a functional paradigm for closures, preventing
any potential type instabilities from boxed variables within an
anonymous function.
- **Easier Functional Programming**:
- Allows for a stronger functional programming paradigm by supporting
partial functions with _any number of arguments_.

Note that this refactors `Fix1` and `Fix2` to be equal to `Fix{1}` and
`Fix{2}` respectively, rather than separate structs. This is backwards
compatible.

Also note that this does not constrain future generalisations of
`Fix{n}` for multiple arguments. `Fix{1,F,T}` is the clear
generalisation of `Fix1{F,T}`, so this isn't major new syntax choices.
But in a future PR you could have, e.g., `Fix{(n1,n2)}` for multiple
arguments, and it would still be backwards-compatible with this.

---------

Co-authored-by: Dilum Aluthge <dilum@aluthge.com>
Co-authored-by: Lilith Orion Hafner <lilithhafner@gmail.com>
Co-authored-by: Alexander Plavin <alexander@plav.in>
Co-authored-by: Neven Sajko <s@purelymail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Design of APIs or of the language itself feature Indicates new feature / enhancement requests
Projects
None yet
Development

Successfully merging this pull request may close these issues.