Skip to content

Commit 79e992e

Browse files
MilesCranmerDilumAluthgeLilithHafneraplavinnsajko
authored andcommitted
Create Base.Fix as general Fix1/Fix2 for partially-applied functions (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 <[email protected]> Co-authored-by: Lilith Orion Hafner <[email protected]> Co-authored-by: Alexander Plavin <[email protected]> Co-authored-by: Neven Sajko <[email protected]>
1 parent 86d13ab commit 79e992e

File tree

6 files changed

+167
-23
lines changed

6 files changed

+167
-23
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ New library functions
7373
* `waitany(tasks; throw=false)` and `waitall(tasks; failfast=false, throw=false)` which wait multiple tasks at once ([#53341]).
7474
* `uuid7()` creates an RFC 9652 compliant UUID with version 7 ([#54834]).
7575
* `insertdims(array; dims)` allows to insert singleton dimensions into an array which is the inverse operation to `dropdims`
76+
* The new `Fix` type is a generalization of `Fix1/Fix2` for fixing a single argument ([#54653]).
7677

7778
New library features
7879
--------------------

base/operators.jl

+36-21
Original file line numberDiff line numberDiff line change
@@ -1154,40 +1154,55 @@ julia> filter(!isletter, str)
11541154
!(f::ComposedFunction{typeof(!)}) = f.inner #allows !!f === f
11551155

11561156
"""
1157-
Fix1(f, x)
1157+
Fix{N}(f, x)
11581158
1159-
A type representing a partially-applied version of the two-argument function
1160-
`f`, with the first argument fixed to the value "x". In other words,
1161-
`Fix1(f, x)` behaves similarly to `y->f(x, y)`.
1159+
A type representing a partially-applied version of a function `f`, with the argument
1160+
`x` fixed at position `N::Int`. In other words, `Fix{3}(f, x)` behaves similarly to
1161+
`(y1, y2, y3...; kws...) -> f(y1, y2, x, y3...; kws...)`.
11621162
1163-
See also [`Fix2`](@ref Base.Fix2).
1163+
!!! compat "Julia 1.12"
1164+
This general functionality requires at least Julia 1.12, while `Fix1` and `Fix2`
1165+
are available earlier.
1166+
1167+
!!! note
1168+
When nesting multiple `Fix`, note that the `N` in `Fix{N}` is _relative_ to the current
1169+
available arguments, rather than an absolute ordering on the target function. For example,
1170+
`Fix{1}(Fix{2}(f, 4), 4)` fixes the first and second arg, while `Fix{2}(Fix{1}(f, 4), 4)`
1171+
fixes the first and third arg.
11641172
"""
1165-
struct Fix1{F,T} <: Function
1173+
struct Fix{N,F,T} <: Function
11661174
f::F
11671175
x::T
11681176

1169-
Fix1(f::F, x) where {F} = new{F,_stable_typeof(x)}(f, x)
1170-
Fix1(f::Type{F}, x) where {F} = new{Type{F},_stable_typeof(x)}(f, x)
1177+
function Fix{N}(f::F, x) where {N,F}
1178+
if !(N isa Int)
1179+
throw(ArgumentError(LazyString("expected type parameter in `Fix` to be `Int`, but got `", N, "::", typeof(N), "`")))
1180+
elseif N < 1
1181+
throw(ArgumentError(LazyString("expected `N` in `Fix{N}` to be integer greater than 0, but got ", N)))
1182+
end
1183+
new{N,_stable_typeof(f),_stable_typeof(x)}(f, x)
1184+
end
11711185
end
11721186

1173-
(f::Fix1)(y) = f.f(f.x, y)
1187+
function (f::Fix{N})(args::Vararg{Any,M}; kws...) where {N,M}
1188+
M < N-1 && throw(ArgumentError(LazyString("expected at least ", N-1, " arguments to `Fix{", N, "}`, but got ", M)))
1189+
return f.f(args[begin:begin+(N-2)]..., f.x, args[begin+(N-1):end]...; kws...)
1190+
end
11741191

1175-
"""
1176-
Fix2(f, x)
1192+
# Special cases for improved constant propagation
1193+
(f::Fix{1})(arg; kws...) = f.f(f.x, arg; kws...)
1194+
(f::Fix{2})(arg; kws...) = f.f(arg, f.x; kws...)
11771195

1178-
A type representing a partially-applied version of the two-argument function
1179-
`f`, with the second argument fixed to the value "x". In other words,
1180-
`Fix2(f, x)` behaves similarly to `y->f(y, x)`.
11811196
"""
1182-
struct Fix2{F,T} <: Function
1183-
f::F
1184-
x::T
1197+
Alias for `Fix{1}`. See [`Fix`](@ref Base.Fix).
1198+
"""
1199+
const Fix1{F,T} = Fix{1,F,T}
11851200

1186-
Fix2(f::F, x) where {F} = new{F,_stable_typeof(x)}(f, x)
1187-
Fix2(f::Type{F}, x) where {F} = new{Type{F},_stable_typeof(x)}(f, x)
1188-
end
1201+
"""
1202+
Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).
1203+
"""
1204+
const Fix2{F,T} = Fix{2,F,T}
11891205

1190-
(f::Fix2)(y) = f.f(y, f.x)
11911206

11921207
"""
11931208
isequal(x)

base/public.jl

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public
1414
AsyncCondition,
1515
CodeUnits,
1616
Event,
17+
Fix,
1718
Fix1,
1819
Fix2,
1920
Generator,

doc/src/base/base.md

+1
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ Base.:(|>)
281281
Base.:(∘)
282282
Base.ComposedFunction
283283
Base.splat
284+
Base.Fix
284285
Base.Fix1
285286
Base.Fix2
286287
```

stdlib/REPL/test/repl.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -1216,9 +1216,9 @@ global some_undef_global
12161216
@test occursin("does not exist", sprint(show, help_result("..")))
12171217
# test that helpmode is sensitive to contextual module
12181218
@test occursin("No documentation found", sprint(show, help_result("Fix2", Main)))
1219-
@test occursin("A type representing a partially-applied version", # exact string may change
1219+
@test occursin("Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).", # exact string may change
12201220
sprint(show, help_result("Base.Fix2", Main)))
1221-
@test occursin("A type representing a partially-applied version", # exact string may change
1221+
@test occursin("Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).", # exact string may change
12221222
sprint(show, help_result("Fix2", Base)))
12231223

12241224

test/functional.jl

+126
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,129 @@ end
235235
let (:)(a,b) = (i for i in Base.:(:)(1,10) if i%2==0)
236236
@test Int8[ i for i = 1:2 ] == [2,4,6,8,10]
237237
end
238+
239+
@testset "Basic tests of Fix1, Fix2, and Fix" begin
240+
function test_fix1(Fix1=Base.Fix1)
241+
increment = Fix1(+, 1)
242+
@test increment(5) == 6
243+
@test increment(-1) == 0
244+
@test increment(0) == 1
245+
@test map(increment, [1, 2, 3]) == [2, 3, 4]
246+
247+
concat_with_hello = Fix1(*, "Hello ")
248+
@test concat_with_hello("World!") == "Hello World!"
249+
# Make sure inference is good:
250+
@inferred concat_with_hello("World!")
251+
252+
one_divided_by = Fix1(/, 1)
253+
@test one_divided_by(10) == 1/10.0
254+
@test one_divided_by(-5) == 1/-5.0
255+
256+
return nothing
257+
end
258+
259+
function test_fix2(Fix2=Base.Fix2)
260+
return_second = Fix2((x, y) -> y, 999)
261+
@test return_second(10) == 999
262+
@inferred return_second(10)
263+
@test return_second(-5) == 999
264+
265+
divide_by_two = Fix2(/, 2)
266+
@test map(divide_by_two, (2, 4, 6)) == (1.0, 2.0, 3.0)
267+
@inferred map(divide_by_two, (2, 4, 6))
268+
269+
concat_with_world = Fix2(*, " World!")
270+
@test concat_with_world("Hello") == "Hello World!"
271+
@inferred concat_with_world("Hello World!")
272+
273+
return nothing
274+
end
275+
276+
# Test with normal Base.Fix1 and Base.Fix2
277+
test_fix1()
278+
test_fix2()
279+
280+
# Now, repeat the Fix1 and Fix2 tests, but
281+
# with a Fix lambda function used in their place
282+
test_fix1((op, arg) -> Base.Fix{1}(op, arg))
283+
test_fix2((op, arg) -> Base.Fix{2}(op, arg))
284+
285+
# Now, we do more complex tests of Fix:
286+
let Fix=Base.Fix
287+
@testset "Argument Fixation" begin
288+
let f = (x, y, z) -> x + y * z
289+
fixed_f1 = Fix{1}(f, 10)
290+
@test fixed_f1(2, 3) == 10 + 2 * 3
291+
292+
fixed_f2 = Fix{2}(f, 5)
293+
@test fixed_f2(1, 4) == 1 + 5 * 4
294+
295+
fixed_f3 = Fix{3}(f, 3)
296+
@test fixed_f3(1, 2) == 1 + 2 * 3
297+
end
298+
end
299+
@testset "Helpful errors" begin
300+
let g = (x, y) -> x - y
301+
# Test minimum N
302+
fixed_g1 = Fix{1}(g, 100)
303+
@test fixed_g1(40) == 100 - 40
304+
305+
# Test maximum N
306+
fixed_g2 = Fix{2}(g, 100)
307+
@test fixed_g2(150) == 150 - 100
308+
309+
# One over
310+
fixed_g3 = Fix{3}(g, 100)
311+
@test_throws ArgumentError("expected at least 2 arguments to `Fix{3}`, but got 1") fixed_g3(1)
312+
end
313+
end
314+
@testset "Type Stability and Inference" begin
315+
let h = (x, y) -> x / y
316+
fixed_h = Fix{2}(h, 2.0)
317+
@test @inferred(fixed_h(4.0)) == 2.0
318+
end
319+
end
320+
@testset "Interaction with varargs" begin
321+
vararg_f = (x, y, z...) -> x + 10 * y + sum(z; init=zero(x))
322+
fixed_vararg_f = Fix{2}(vararg_f, 6)
323+
324+
# Can call with variable number of arguments:
325+
@test fixed_vararg_f(1, 2, 3, 4) == 1 + 10 * 6 + sum((2, 3, 4))
326+
@inferred fixed_vararg_f(1, 2, 3, 4)
327+
@test fixed_vararg_f(5) == 5 + 10 * 6
328+
@inferred fixed_vararg_f(5)
329+
end
330+
@testset "Errors should propagate normally" begin
331+
error_f = (x, y) -> sin(x * y)
332+
fixed_error_f = Fix{2}(error_f, Inf)
333+
@test_throws DomainError fixed_error_f(10)
334+
end
335+
@testset "Chaining Fix together" begin
336+
f1 = Fix{1}(*, "1")
337+
f2 = Fix{1}(f1, "2")
338+
f3 = Fix{1}(f2, "3")
339+
@test f3() == "123"
340+
341+
g1 = Fix{2}(*, "1")
342+
g2 = Fix{2}(g1, "2")
343+
g3 = Fix{2}(g2, "3")
344+
@test g3("") == "123"
345+
end
346+
@testset "Zero arguments" begin
347+
f = Fix{1}(x -> x, 'a')
348+
@test f() == 'a'
349+
end
350+
@testset "Dummy-proofing" begin
351+
@test_throws ArgumentError("expected `N` in `Fix{N}` to be integer greater than 0, but got 0") Fix{0}(>, 1)
352+
@test_throws ArgumentError("expected type parameter in `Fix` to be `Int`, but got `0.5::Float64`") Fix{0.5}(>, 1)
353+
@test_throws ArgumentError("expected type parameter in `Fix` to be `Int`, but got `1::UInt64`") Fix{UInt64(1)}(>, 1)
354+
end
355+
@testset "Specialize to structs not in `Base`" begin
356+
struct MyStruct
357+
x::Int
358+
end
359+
f = Fix{1}(MyStruct, 1)
360+
@test f isa Fix{1,Type{MyStruct},Int}
361+
end
362+
end
363+
end

0 commit comments

Comments
 (0)