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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
a2b0d84
Create Base.FixN for complex functional workflows
MilesCranmer Jun 2, 2024
402ed6c
add test for chaining FixN together
MilesCranmer Jun 2, 2024
2c0d91c
allow varargs inside FixN
MilesCranmer Jun 2, 2024
b25bb9e
remove +0 in FixN
MilesCranmer Jun 2, 2024
c4c9b33
rename to `Fix` function
MilesCranmer Jun 2, 2024
3cb4133
ensure Val constructors always inlined
MilesCranmer Jun 2, 2024
9867242
test for number of arguments edgecase
MilesCranmer Jun 2, 2024
c52779b
fix bound
MilesCranmer Jun 2, 2024
7cf7203
allow fixing kwargs in `Base.Fix` as well
MilesCranmer Jun 3, 2024
325621a
Merge branch 'master' into fixN
DilumAluthge Jun 3, 2024
e377640
switch to `Fix{n}` syntax
MilesCranmer Jun 3, 2024
a2d8605
back to minimal `Fix` implementation
MilesCranmer Jun 4, 2024
2491c62
clean up docstring with keyword-less version
MilesCranmer Jun 4, 2024
439c5b6
allow keywords in Fix and rewrite Fix1/Fix2
MilesCranmer Jun 6, 2024
4a0e778
prevent keywords from being passed to Fix1/Fix2
MilesCranmer Jun 6, 2024
a473b5e
use nothing instead of Tuple{}
MilesCranmer Jun 6, 2024
758bbcc
update Fix1/Fix2 docstrings
MilesCranmer Jun 7, 2024
e2fc740
fix undefined `@NamedTuple`
MilesCranmer Jun 7, 2024
9aa7c52
constrain `K` to `NamedTuple`
MilesCranmer Jun 7, 2024
b886dd0
extend docstring for Base.Fix
MilesCranmer Jun 7, 2024
c32c65a
update tests for Base.Fix
MilesCranmer Jun 7, 2024
866c7c7
add Base.Fix to docs
MilesCranmer Jun 7, 2024
a9674c0
add dummy-proofing to `Base.Fix{n}`
MilesCranmer Jun 8, 2024
1825f76
simplify validation check
MilesCranmer Jun 8, 2024
5604ffe
unify `Fix` constructors with `Union{F,Type{F}}`
MilesCranmer Jun 8, 2024
74dcfb1
improve readability of validation code
MilesCranmer Jun 8, 2024
10b8320
update tests with allowed behavior
MilesCranmer Jun 8, 2024
73dd2cf
make constructors more readable
MilesCranmer Jun 8, 2024
fb8ad78
tweak docstring
MilesCranmer Jun 9, 2024
de330be
tweak docstring
MilesCranmer Jun 9, 2024
a24f389
`Fix` syntax takes symbol as parameter
MilesCranmer Jun 20, 2024
1b5ffc1
remove redundant `Fix1/Fix2` methods
MilesCranmer Jun 20, 2024
6d92c1a
add note about nested `Fix`
MilesCranmer Jul 4, 2024
bc053cf
amend `Fix` validation to not convert to `Int64`
MilesCranmer Jul 4, 2024
3c19ecf
improve `Fix` readability
MilesCranmer Jul 4, 2024
3374b8f
rearrange branches in `Fix` for robust errors
MilesCranmer Jul 4, 2024
8d9c690
rewrite `Fix1`/`Fix2` docstrings to be aliases
MilesCranmer Jul 4, 2024
9a4926e
Apply suggestions from code review
MilesCranmer Jul 4, 2024
aa08daf
single branch for `Fix{n::Int}`
MilesCranmer Jul 4, 2024
e541a51
Apply suggestions from code review
MilesCranmer Jul 4, 2024
751e964
one-line validations
MilesCranmer Jul 4, 2024
669f753
remove `@inline` as not needed
MilesCranmer Jul 4, 2024
5694840
add `Fix` to public
MilesCranmer Jul 4, 2024
87f5575
update comments
MilesCranmer Jul 4, 2024
79dcd6d
avoid redundant `Val`
MilesCranmer Jul 4, 2024
f3d5c5c
reduce lines of `_validate_fix_param`
MilesCranmer Jul 4, 2024
c8bc1ab
remove old empty `Fix` test
MilesCranmer Jul 4, 2024
e9c566b
update `Fix` tests with latest changes
MilesCranmer Jul 4, 2024
9b1d749
update `Fix` test with new error message
MilesCranmer Jul 4, 2024
47a49bf
reduce branches
MilesCranmer Jul 4, 2024
06aab6f
inline `_validate_fix_param`
MilesCranmer Jul 4, 2024
54150eb
Merge branch 'master' into fixN
MilesCranmer Jul 4, 2024
6028074
Apply suggestions from code review
MilesCranmer Jul 4, 2024
f2980d4
Update base/operators.jl
MilesCranmer Jul 4, 2024
5146e1e
clean up more redundancies in tests
MilesCranmer Jul 4, 2024
cec1675
clean up more redundancies in tests
MilesCranmer Jul 4, 2024
09ee6ec
modify REPL test with new Fix docstring
MilesCranmer Jul 4, 2024
42a1e3e
add special cases for constant propagation
MilesCranmer Jul 5, 2024
bd51d2e
soften wording of nested `Fix` note
MilesCranmer Jul 5, 2024
9d8e9ab
Apply suggestions from code review
MilesCranmer Jul 5, 2024
717f39b
Apply suggestions from code review
MilesCranmer Jul 6, 2024
3f2b116
test for Fix: update tests to new error message; add zero-arg test
MilesCranmer Jul 6, 2024
316db38
grammar in error message
MilesCranmer Jul 6, 2024
04c247c
more grammar tweaks in error message
MilesCranmer Jul 6, 2024
1908c1f
test multiple kw error message
MilesCranmer Jul 6, 2024
3fbdb91
Merge branch 'master' into fixN
MilesCranmer Jul 6, 2024
d514e46
Update base/operators.jl
MilesCranmer Jul 25, 2024
ded1bfb
docs: space out compat statement
MilesCranmer Jul 25, 2024
2e9a91c
docs: describe `Fix` in NEWS
MilesCranmer Jul 26, 2024
08d774b
docs: tweak docstring for Fix
MilesCranmer Jul 26, 2024
1a107b0
docs: formatting docstring for Fix
MilesCranmer Jul 26, 2024
6bc91a2
nitpick docstring grammar
MilesCranmer Jul 26, 2024
3e19e97
normalize `compat` statement to other docstrings
MilesCranmer Jul 26, 2024
ff197b8
feat: remove single keyword version of `Fix`
MilesCranmer Aug 1, 2024
9e813fa
docs: back to old compat statement
MilesCranmer Aug 1, 2024
04dfe09
refactor: `LazyString` versions of `Fix`
MilesCranmer Aug 1, 2024
6cac583
docs: tweak compat statement
MilesCranmer Aug 1, 2024
0271f75
Merge branch 'master' into fixN
MilesCranmer Aug 1, 2024
ed64473
docs: update NEWS.md with removed keyword arg functionality
MilesCranmer Aug 1, 2024
4999c2f
fix: change in specialization from `_stable_typeof`
MilesCranmer Aug 1, 2024
c7aebb3
Revert "fix: change in specialization from `_stable_typeof`"
MilesCranmer Aug 1, 2024
8c9f62f
test: add back in test
MilesCranmer Aug 1, 2024
f065c33
Merge branch 'master' into fixN
LilithHafner Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ New library functions
* `waitany(tasks; throw=false)` and `waitall(tasks; failfast=false, throw=false)` which wait multiple tasks at once ([#53341]).
* `uuid7()` creates an RFC 9652 compliant UUID with version 7 ([#54834]).
* `insertdims(array; dims)` allows to insert singleton dimensions into an array which is the inverse operation to `dropdims`
* The new `Fix` type is a generalization of `Fix1/Fix2` for fixing a single argument ([#54653]).

New library features
--------------------
Expand Down
57 changes: 36 additions & 21 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1154,40 +1154,55 @@ julia> filter(!isletter, str)
!(f::ComposedFunction{typeof(!)}) = f.inner #allows !!f === f

"""
Fix1(f, x)
Fix{N}(f, x)

A type representing a partially-applied version of the two-argument function
`f`, with the first argument fixed to the value "x". In other words,
`Fix1(f, x)` behaves similarly to `y->f(x, y)`.
A type representing a partially-applied version of a function `f`, with the argument
`x` fixed at position `N::Int`. In other words, `Fix{3}(f, x)` behaves similarly to
`(y1, y2, y3...; kws...) -> f(y1, y2, x, y3...; kws...)`.

See also [`Fix2`](@ref Base.Fix2).
!!! compat "Julia 1.12"
This general functionality requires at least Julia 1.12, while `Fix1` and `Fix2`
are available earlier.

MilesCranmer marked this conversation as resolved.
Show resolved Hide resolved
!!! note
When nesting multiple `Fix`, note that the `N` in `Fix{N}` is _relative_ to the current
available arguments, rather than an absolute ordering on the target function. For example,
`Fix{1}(Fix{2}(f, 4), 4)` fixes the first and second arg, while `Fix{2}(Fix{1}(f, 4), 4)`
fixes the first and third arg.
"""
struct Fix1{F,T} <: Function
struct Fix{N,F,T} <: Function
f::F
x::T

Fix1(f::F, x) where {F} = new{F,_stable_typeof(x)}(f, x)
Fix1(f::Type{F}, x) where {F} = new{Type{F},_stable_typeof(x)}(f, x)
function Fix{N}(f::F, x) where {N,F}
if !(N isa Int)
throw(ArgumentError(LazyString("expected type parameter in `Fix` to be `Int`, but got `", N, "::", typeof(N), "`")))
elseif N < 1
throw(ArgumentError(LazyString("expected `N` in `Fix{N}` to be integer greater than 0, but got ", N)))
end
new{N,_stable_typeof(f),_stable_typeof(x)}(f, x)
end
end

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

"""
Fix2(f, x)
# Special cases for improved constant propagation
(f::Fix{1})(arg; kws...) = f.f(f.x, arg; kws...)
(f::Fix{2})(arg; kws...) = f.f(arg, f.x; kws...)

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

Fix2(f::F, x) where {F} = new{F,_stable_typeof(x)}(f, x)
Fix2(f::Type{F}, x) where {F} = new{Type{F},_stable_typeof(x)}(f, x)
end
"""
Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).
"""
const Fix2{F,T} = Fix{2,F,T}

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

"""
isequal(x)
Expand Down
1 change: 1 addition & 0 deletions base/public.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public
AsyncCondition,
CodeUnits,
Event,
Fix,
Fix1,
Fix2,
Generator,
Expand Down
1 change: 1 addition & 0 deletions doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ Base.:(|>)
Base.:(∘)
Base.ComposedFunction
Base.splat
Base.Fix
Base.Fix1
Base.Fix2
```
Expand Down
4 changes: 2 additions & 2 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1216,9 +1216,9 @@ global some_undef_global
@test occursin("does not exist", sprint(show, help_result("..")))
# test that helpmode is sensitive to contextual module
@test occursin("No documentation found", sprint(show, help_result("Fix2", Main)))
@test occursin("A type representing a partially-applied version", # exact string may change
@test occursin("Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).", # exact string may change
sprint(show, help_result("Base.Fix2", Main)))
@test occursin("A type representing a partially-applied version", # exact string may change
@test occursin("Alias for `Fix{2}`. See [`Fix`](@ref Base.Fix).", # exact string may change
sprint(show, help_result("Fix2", Base)))


Expand Down
126 changes: 126 additions & 0 deletions test/functional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,129 @@ end
let (:)(a,b) = (i for i in Base.:(:)(1,10) if i%2==0)
@test Int8[ i for i = 1:2 ] == [2,4,6,8,10]
end

@testset "Basic tests of Fix1, Fix2, and Fix" begin
function test_fix1(Fix1=Base.Fix1)
increment = Fix1(+, 1)
@test increment(5) == 6
@test increment(-1) == 0
@test increment(0) == 1
@test map(increment, [1, 2, 3]) == [2, 3, 4]

concat_with_hello = Fix1(*, "Hello ")
@test concat_with_hello("World!") == "Hello World!"
# Make sure inference is good:
@inferred concat_with_hello("World!")

one_divided_by = Fix1(/, 1)
@test one_divided_by(10) == 1/10.0
@test one_divided_by(-5) == 1/-5.0

return nothing
end

function test_fix2(Fix2=Base.Fix2)
return_second = Fix2((x, y) -> y, 999)
@test return_second(10) == 999
@inferred return_second(10)
@test return_second(-5) == 999

divide_by_two = Fix2(/, 2)
@test map(divide_by_two, (2, 4, 6)) == (1.0, 2.0, 3.0)
@inferred map(divide_by_two, (2, 4, 6))

concat_with_world = Fix2(*, " World!")
@test concat_with_world("Hello") == "Hello World!"
@inferred concat_with_world("Hello World!")

return nothing
end

# Test with normal Base.Fix1 and Base.Fix2
test_fix1()
test_fix2()

# Now, repeat the Fix1 and Fix2 tests, but
# with a Fix lambda function used in their place
test_fix1((op, arg) -> Base.Fix{1}(op, arg))
test_fix2((op, arg) -> Base.Fix{2}(op, arg))

# Now, we do more complex tests of Fix:
let Fix=Base.Fix
@testset "Argument Fixation" begin
let f = (x, y, z) -> x + y * z
fixed_f1 = Fix{1}(f, 10)
@test fixed_f1(2, 3) == 10 + 2 * 3

fixed_f2 = Fix{2}(f, 5)
@test fixed_f2(1, 4) == 1 + 5 * 4

fixed_f3 = Fix{3}(f, 3)
@test fixed_f3(1, 2) == 1 + 2 * 3
end
end
@testset "Helpful errors" begin
let g = (x, y) -> x - y
# Test minimum N
fixed_g1 = Fix{1}(g, 100)
@test fixed_g1(40) == 100 - 40

# Test maximum N
fixed_g2 = Fix{2}(g, 100)
@test fixed_g2(150) == 150 - 100

# One over
fixed_g3 = Fix{3}(g, 100)
@test_throws ArgumentError("expected at least 2 arguments to `Fix{3}`, but got 1") fixed_g3(1)
end
end
@testset "Type Stability and Inference" begin
let h = (x, y) -> x / y
fixed_h = Fix{2}(h, 2.0)
@test @inferred(fixed_h(4.0)) == 2.0
end
end
@testset "Interaction with varargs" begin
vararg_f = (x, y, z...) -> x + 10 * y + sum(z; init=zero(x))
fixed_vararg_f = Fix{2}(vararg_f, 6)

# Can call with variable number of arguments:
@test fixed_vararg_f(1, 2, 3, 4) == 1 + 10 * 6 + sum((2, 3, 4))
@inferred fixed_vararg_f(1, 2, 3, 4)
@test fixed_vararg_f(5) == 5 + 10 * 6
@inferred fixed_vararg_f(5)
end
@testset "Errors should propagate normally" begin
error_f = (x, y) -> sin(x * y)
fixed_error_f = Fix{2}(error_f, Inf)
@test_throws DomainError fixed_error_f(10)
end
@testset "Chaining Fix together" begin
f1 = Fix{1}(*, "1")
f2 = Fix{1}(f1, "2")
f3 = Fix{1}(f2, "3")
@test f3() == "123"

g1 = Fix{2}(*, "1")
g2 = Fix{2}(g1, "2")
g3 = Fix{2}(g2, "3")
@test g3("") == "123"
end
@testset "Zero arguments" begin
f = Fix{1}(x -> x, 'a')
@test f() == 'a'
end
@testset "Dummy-proofing" begin
@test_throws ArgumentError("expected `N` in `Fix{N}` to be integer greater than 0, but got 0") Fix{0}(>, 1)
@test_throws ArgumentError("expected type parameter in `Fix` to be `Int`, but got `0.5::Float64`") Fix{0.5}(>, 1)
@test_throws ArgumentError("expected type parameter in `Fix` to be `Int`, but got `1::UInt64`") Fix{UInt64(1)}(>, 1)
end
@testset "Specialize to structs not in `Base`" begin
struct MyStruct
x::Int
end
f = Fix{1}(MyStruct, 1)
@test f isa Fix{1,Type{MyStruct},Int}
end
end
end