Skip to content

Commit 74ba57d

Browse files
authored
Merge pull request #7 from avik-pal/ap/generalized
Adds a partial function macro `@$`
2 parents cf363b7 + 353bc47 commit 74ba57d

File tree

5 files changed

+218
-51
lines changed

5 files changed

+218
-51
lines changed

Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
name = "PartialFunctions"
22
uuid = "570af359-4316-4cb7-8c74-252c00c2016b"
33
authors = ["Thomas Marks <[email protected]> and contributors"]
4-
version = "1.1.1"
4+
version = "1.2.0"
5+
6+
[deps]
7+
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
58

69
[compat]
10+
MacroTools = "0.5"
711
julia = "1.5"
812

913
[extras]

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,20 @@ isequal(1, 2, ...)
125125
julia> isequal $ (1, 2) <| () # equivalent to a() or isequal(1, 2)
126126
false
127127
```
128+
129+
## The `@$` Macro
130+
131+
`@$` allows users to create general partial functions by replacing the currently unknown
132+
arguments with `_`. For example, we can implement matrix multiplication as:
133+
134+
```julia
135+
julia> matmul(A, X, B) = A * X .+ B
136+
137+
julia> A = randn(2, 2); B = rand(2, 2); X = randn(2, 2);
138+
139+
julia> pf = @$ matmul(_, X, _)
140+
matmul(_, X, _)
141+
142+
julia> pf(A, B) matmul(A, X, B)
143+
true
144+
```

docs/src/index.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,20 @@ julia> isequal $ (1, 2) <| () # equivalent to a() or isequal(1, 2)
137137
false
138138
```
139139

140+
## The `@$` Macro
141+
142+
`@$` allows users to create general partial functions by replacing the currently unknown
143+
arguments with `_`. For example, we can implement matrix multiplication as:
144+
145+
```jldoctest
146+
julia> matmul(A, X, B) = A * X .+ B
147+
matmul (generic function with 1 method)
148+
149+
julia> A = randn(2, 2); B = rand(2, 2); X = randn(2, 2);
150+
151+
julia> pf = @$ matmul(_, X, _)
152+
matmul(_, X, _)
153+
154+
julia> pf(A, B) ≈ matmul(A, X, B)
155+
true
156+
```

src/PartialFunctions.jl

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
module PartialFunctions
22

3-
export $
3+
using MacroTools
4+
5+
export $, @$
46
export <|
57
include("reversedfunctions.jl")
68

79
name(x) = (string Symbol)(x)
810

9-
struct PartialFunction{F<:Function, T<:Tuple, N<:NamedTuple} <: Function
11+
struct PartialFunction{KL, UL, F<:Function, T<:Tuple, N<:NamedTuple} <: Function
12+
expr_string::String
1013
func::F
1114
args::T
1215
kwargs::N
13-
PartialFunction(f, args::T, kwargs::N) where {T<:Tuple, N<:NamedTuple} = let
14-
new{typeof(f), T, N}(f, args, kwargs)
15-
end
16+
end
17+
18+
function PartialFunction(f, args::T, kwargs::N) where {T<:Tuple, N<:NamedTuple}
19+
return PartialFunction{nothing, nothing, typeof(f), typeof(args), typeof(kwargs)}("", f, args, kwargs)
20+
end
21+
22+
function PartialFunction(known_args_locations, unknown_args_locations, expr_string::String, f, args::T, kwargs::N) where {T<:Tuple, N<:NamedTuple}
23+
return PartialFunction{known_args_locations, unknown_args_locations, typeof(f), T, N}(expr_string, f, args, kwargs)
1624
end
1725

1826
function PartialFunction(f::PartialFunction, newargs::Tuple, newkwargs::NamedTuple)
@@ -21,13 +29,13 @@ function PartialFunction(f::PartialFunction, newargs::Tuple, newkwargs::NamedTup
2129
PartialFunction(f.func, allargs, allkwargs)
2230
end
2331

24-
(p::PartialFunction)(newargs...; newkwargs...) = p.func(p.args..., newargs...; p.kwargs..., newkwargs...)
32+
(p::PartialFunction{nothing})(newargs...; newkwargs...) = p.func(p.args..., newargs...; p.kwargs..., newkwargs...)
2533

2634
"""
2735
(\$)(f::Function, args...)
2836
Partially apply the given arguments to f. Typically used as infix `f \$ args`
2937
30-
The returned function is of type [`PartialFunctions.PartialFunction{typeof(f), typeof(args)}`](@ref)
38+
The returned function is of type [`PartialFunctions.PartialFunction{nothing, nothing, typeof(f), typeof(args)}`](@ref)
3139
3240
# Examples
3341
@@ -52,6 +60,87 @@ end
5260
end
5361

5462
($)(f::DataType, args) = ($)(identityf, args)
63+
64+
@generated function (pf::PartialFunction{kloc, uloc})(args...; kwargs...) where {kloc, uloc}
65+
L = length(args)
66+
if L == length(uloc)
67+
# Execute the function
68+
final_args = []
69+
total_args = length(uloc) + length(kloc)
70+
j, k = 1, 1
71+
for i in 1:total_args
72+
if i uloc
73+
push!(final_args, :(args[$j]))
74+
j += 1
75+
else
76+
push!(final_args, :(pf.args[$k]))
77+
k += 1
78+
end
79+
end
80+
return quote
81+
pf.func($(final_args...); pf.kwargs..., kwargs...)
82+
end
83+
else
84+
return :(pf $ (args..., (; kwargs...)))
85+
end
86+
end
87+
88+
89+
"""
90+
@\$ f(args...; kwargs...)
91+
92+
Partially apply the given arguments to `f`. Unknown arguments are represented by `_`.
93+
94+
!!! note
95+
If no `_` is present, the function is executed immediately.
96+
97+
# Examples
98+
99+
```jldoctest
100+
julia> matmul(A, X, B; C = 1) = A * X .+ B .* C
101+
matmul (generic function with 1 method)
102+
103+
julia> A = randn(2, 2); B = rand(2, 2); X = randn(2, 2);
104+
105+
julia> pf = @\$ matmul(_, X, _; C = 2)
106+
matmul(_, X, _; C = 2)
107+
108+
julia> pf(A, B) ≈ matmul(A, X, B; C = 2)
109+
true
110+
```
111+
"""
112+
macro ($)(expr::Expr)
113+
if !@capture(expr, f_(args__; kwargs__) | f_(args__))
114+
throw(ArgumentError("Only function calls are supported!"))
115+
end
116+
117+
kwargs = kwargs === nothing ? [] : kwargs
118+
119+
underscore_args_pos = Tuple(findall(x -> x == :_, args))
120+
if length(args) == 0 || length(underscore_args_pos) == 0
121+
return :($(esc(expr)))
122+
end
123+
124+
kwargs_keys = Symbol[]
125+
kwargs_values = Any[]
126+
for kwarg in kwargs
127+
@assert kwarg.head == :kw "Malformed keyword argument!"
128+
push!(kwargs_keys, kwarg.args[1])
129+
push!(kwargs_values, kwarg.args[2])
130+
end
131+
kwargs_keys = Tuple(kwargs_keys)
132+
133+
non_underscore_args_pos = Tuple(setdiff(1:length(args), underscore_args_pos))
134+
non_underscore_args = map(Base.Fix1(getindex, args), non_underscore_args_pos)
135+
stored_args = NamedTuple{Symbol.(non_underscore_args_pos)}(non_underscore_args)
136+
137+
return :(PartialFunction($(esc(non_underscore_args_pos)),
138+
$(esc(underscore_args_pos)),
139+
$(esc(string(expr))), $(esc(f)),
140+
$(esc(tuple))($(esc.(non_underscore_args)...)),
141+
$(NamedTuple{kwargs_keys})(tuple($(esc.(kwargs_values)...)))))
142+
end
143+
55144
"""
56145
<|(f, args)
57146
@@ -90,7 +179,9 @@ function Base.Symbol(pf::PartialFunction)
90179
Base.Symbol("$(func_name)$(argstring)")
91180
end
92181

93-
Base.show(io::IO, pf::PartialFunction) = print(io, name(pf))
94-
Base.show(io::IO, ::MIME"text/plain", pf::PartialFunction) = show(io, pf)
182+
Base.show(io::IO, pf::PartialFunction{nothing}) = print(io, name(pf))
183+
Base.show(io::IO, ::MIME"text/plain", pf::PartialFunction{nothing}) = show(io, pf)
184+
Base.show(io::IO, pf::PartialFunction) = print(io, pf.expr_string)
185+
Base.show(io::IO, ::MIME"text/plain", pf::PartialFunction) = print(io, pf.expr_string)
95186

96187
end

test/runtests.jl

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,87 @@ using Test
44
a(x) = x^2
55
greet(greeting, name, punctuation) = "$(greeting), $(name)$(punctuation)"
66

7-
@testset "Partial functions" begin
8-
@test map((+)$2, [1,2,3]) == [3, 4, 5]
9-
@test repr(map $ a) == "map(a, ...)"
10-
@test (map $ a)([1, 2, 3]) == [1, 4, 9]
11-
12-
@test greet("Hello", "Bob", "!") == "Hello, Bob!"
13-
sayhello = greet $ "Hello"
14-
@test repr(sayhello) == "greet(\"Hello\", ...)"
15-
@test repr("text/plain", sayhello) == repr(sayhello)
16-
17-
@test sayhello("Bob", "!") == "Hello, Bob!"
18-
hi_bob = greet $ "Hi" $ "Bob" $ "!"
19-
@test hi_bob isa PartialFunctions.PartialFunction{typeof(greet), Tuple{String, String, String}, NamedTuple{(), Tuple{}}}
20-
@test hi_bob <| () == "Hi, Bob!"
21-
@test sayhello <| ("Jimmy", "?")... == "Hello, Jimmy?"
22-
23-
@test greet $ ("Hi", "Bob") <| "!" == "Hi, Bob!"
24-
end
7+
@testset "PartialFunctions.jl" begin
8+
@testset "Partial functions" begin
9+
@test map((+)$2, [1,2,3]) == [3, 4, 5]
10+
@test repr(map $ a) == "map(a, ...)"
11+
@test (map $ a)([1, 2, 3]) == [1, 4, 9]
12+
13+
@test greet("Hello", "Bob", "!") == "Hello, Bob!"
14+
sayhello = greet $ "Hello"
15+
@test repr(sayhello) == "greet(\"Hello\", ...)"
16+
@test repr("text/plain", sayhello) == repr(sayhello)
2517

26-
@testset "Reversed functions" begin
27-
revmap = flip(map)
28-
@test flip(revmap) == map
29-
@test revmap([1,2,3], sin) == map(sin, [1,2,3])
30-
31-
func(x, y) = x - y
32-
func(x, y, z) = x - y - z
33-
@test func(1, 2) == -1
34-
@test func(1, 3, 6) == -8
35-
flipped = flip(func)
36-
@test flipped(2, 1) == -1
37-
@test flipped(3, 2, 1) == -4
38-
end
18+
@test sayhello("Bob", "!") == "Hello, Bob!"
19+
hi_bob = greet $ "Hi" $ "Bob" $ "!"
20+
@test hi_bob isa PartialFunctions.PartialFunction{nothing, nothing, typeof(greet), Tuple{String, String, String}, NamedTuple{(), Tuple{}}}
21+
@test hi_bob <| () == "Hi, Bob!"
22+
@test sayhello <| ("Jimmy", "?")... == "Hello, Jimmy?"
23+
24+
@test greet $ ("Hi", "Bob") <| "!" == "Hi, Bob!"
25+
end
26+
27+
@testset "Reversed functions" begin
28+
revmap = flip(map)
29+
@test flip(revmap) == map
30+
@test revmap([1,2,3], sin) == map(sin, [1,2,3])
31+
32+
func(x, y) = x - y
33+
func(x, y, z) = x - y - z
34+
@test func(1, 2) == -1
35+
@test func(1, 3, 6) == -8
36+
flipped = flip(func)
37+
@test flipped(2, 1) == -1
38+
@test flipped(3, 2, 1) == -4
39+
end
40+
41+
@testset "Keyword Arguments" begin
42+
a = [[1,2,3], [1,2]]
43+
sort_by_length = sort $ (; by = length)
44+
@test sort(a, by = length) == sort_by_length(a)
45+
46+
sort_a_by_length = sort $ (a, (;by = length))
47+
@test sort(a, by = length) == sort_a_by_length()
3948

40-
@testset "Keyword Arguments" begin
41-
a = [[1,2,3], [1,2]]
42-
sort_by_length = sort $ (; by = length)
43-
@test sort(a, by = length) == sort_by_length(a)
49+
sort_a_by_length_2 = sort $ ((a,), (;by = length))
50+
@test sort_a_by_length == sort_a_by_length_2
4451

45-
sort_a_by_length = sort $ (a, (;by = length))
46-
@test sort(a, by = length) == sort_a_by_length()
52+
@test repr(sort_a_by_length) == "sort([[1, 2, 3], [1, 2]], ...; by = length, ...)"
53+
end
4754

48-
sort_a_by_length_2 = sort $ ((a,), (;by = length))
49-
@test sort_a_by_length == sort_a_by_length_2
55+
@testset "Generalized Partial Functions" begin
56+
@test map(@$(+(2, _)), [1,2,3]) == [3, 4, 5]
57+
@test map(@$(+(_, 2)), [1,2,3]) == [3, 4, 5]
58+
@test repr(@$(map(a, _))) == "map(a, _)"
59+
@test (@$(map(a, _)))([1, 2, 3]) == [1, 4, 9]
60+
61+
@test greet("Hello", "Bob", "!") == "Hello, Bob!"
62+
sayhello = @$ greet("Hello", _, _)
63+
@test repr(sayhello) == "greet(\"Hello\", _, _)"
64+
@test repr("text/plain", sayhello) == repr(sayhello)
5065

51-
@test repr(sort_a_by_length) == "sort([[1, 2, 3], [1, 2]], ...; by = length, ...)"
52-
end
66+
@test sayhello("Bob", "!") == "Hello, Bob!"
67+
68+
sayhellobob = sayhello("Bob")
69+
@test repr(sayhellobob) == "greet(\"Hello\", \"Bob\", ...)"
70+
@test sayhellobob("!") == "Hello, Bob!"
71+
72+
hi_bob = @$(@$(greet("Hi", _, _))("Bob", _))
73+
@test @$(hi_bob("!")) == "Hi, Bob!"
74+
@test hi_bob isa PartialFunctions.PartialFunction
75+
@test sayhello <| ("Jimmy", "?")... == "Hello, Jimmy?"
76+
77+
@test hi_bob <| "!" == "Hi, Bob!"
78+
79+
@testset "Keyword Arguments" begin
80+
a = [[1,2,3], [1,2]]
81+
sort_by_length = @$(sort(_; by = length))
82+
@test sort(a, by = length) == sort_by_length(a)
83+
84+
sorted_a_by_length = @$(sort(a; by = length))
85+
@test sort(a, by = length) == sorted_a_by_length
86+
87+
@test repr(sort_by_length) == "sort(_; by = length)"
88+
end
89+
end
90+
end

0 commit comments

Comments
 (0)