struct FullyCurried end
macro curried(fdef)
f = fdef.args[1].args[1]
fargs = fdef.args[1].args[2:end]
arity = length(fargs)
body = fdef.args[2]
err_str = "Too many arguments. Function $f only takes $arity arguments"
quote
begin
function $f(args...)
if length(args) < $arity
x -> $f((args..., x)...)
elseif length(args) == $arity
$f(FullyCurried(), args...)
else
throw($err_str)
end
end
$f(::FullyCurried, $(fargs...)) = $body
end
end |> esc
end
julia> @curried foo(x, y, z) = (x^2 + y^2)/(x^2 + y^2 +z^2)
foo (generic function with 2 methods)
julia> foo(1)(2)(3)
0.35714285714285715
julia> foo(1, 2, 3)
0.35714285714285715
The main thing I like is that instead of a call-site macroinvocation like @curry f(1)(2)(3), the macro instead lives at the function definition site.
Oh, my mistake, I thought you just wanted currying. I beleive what you want requires a callsite annotation in general (unless this PR somehow gets merged https://github.com/JuliaLang/julia/pull/24990). Here’s a package that does it somewhat nicely:
Thanks. But all libraries seem to work with positions only. I want an auto-partial function creator that drops Typed arguments that it can match. See the original example.
I didn’t know I wanted that but I do now. Sound you want a macro that defines @generated functions.
@partials function f(a::String, b::Int, c::Dict)
And the partials macro outputs something like:
@generated f(a::Type{A}) where A = begin
pos = if A <: String
1
else if A <: Int
2
else if A <: Dict
3
end
... # write the expression for the anonymous func with `pos` argument filled in with `a`
end
@generated f(a::Type{A}, b::Type{B}) where {A, B} = begin
# as above for both A and B parameters
end
# and so on...
In the macro you could loop over the arguments to f and define @generated function for 1 argument, 2 arguments, 3 arguments etc partial functions. The @generated function would deal with the arbitrary argument types.
Edit: or just define all the method permutations in the macro manually without @generated
Capable or able!?? A few of my packages do worse things… but the 10 or so currently unfinished ones say I am not “able” to, lol.
You will be surprised how easy it is, that could be a third of the code already. It should be a sub 100 lines package. You probably can just use for loops to push the right expressions to the right positions in vectors of expressions for the function arguments. It will take you a while to get used to code as data, but once you do it’s fairly simple - except correctly escaping user inputs, that’s always confusing.
Edit: seriously just jump in, Mixers.jl, FieldMetadata.jl and Flatten.jl are all from my first year of julia, for better or worse! but they definitely have a lot of “clever” metaprogramming, like nested macros and recursive @generated functions…
i have some (horrible) lines of code that can do this:
_getsig(f::Function) = map(b -> b.sig, methods(f).ms)
function _argTypes(f::Function)
filtered = filter(x -> !(x isa UnionAll) && !(Any in x.parameters), getsig(f))
return map(x -> x.parameters[2:end], filtered)
end
function _select_args(args,other_args,pos,n)
other_arg_count = 1
pos_count = 1
max_pos = length(pos)
max_other = length(other_args)
counter = 1
final_args = Any[]
while counter<=n
if (pos[pos_count] == counter)
push!(final_args,args[pos_count])
pos_count < max_pos && (pos_count+=1)
else
push!(final_args,other_args[other_arg_count])
other_arg_count < max_other && (other_arg_count+=1)
end
counter +=1
end
return final_args
end
#just works with unique types
function partial_application(f,args...)
f_typelist = _argTypes(f)[1]
f_name = Symbol(f)
total_args_length = length(f_typelist)
arg_typelist = (typeof(i) for i in args)
if !allunique(f_typelist)
throw("the arguments of $f_name are not of unique types!")
elseif !allunique(arg_typelist)
throw("the input arguments are not of unique types!")
end
total_args_length
if length(args) < total_args_length
pos = findall(i-> any(j -> <:(i,j),arg_typelist),f_typelist)
function partial_f(other_args...)
return f(_select_args(args,other_args,pos,total_args_length)...)
end
return partial_f
else
return f(args...)
end
end
macro partials(arg)
fname = arg.args[1]
arguments = arg.args[2:end]
quote
partial_application($fname,$arguments...)
end
end
the usage is the following:
fn(a::Float64,b::Vector{Float64},c::Int) = c*a*b
f = @partials fn(b)
f(a,c) # fn(a,b,c)
f = @partials fn(c,a)
f(b)
is the worst way possible, but is flexible, accepts n arguments in any order, but you have to define all types in your function