Is there a package to that can make partial functions automatically?

I have a function say savejdf

savejdf(df::AbstractDataFrame, path::AbstractString)

and I would like to create all possible partial functions e.g.

savejdf(df::AbstractDataFrame)  =  path -> savejdf(df, path)
savejdf(path::AbstractString)  =  df -> savejdf(df, path)

and obviously would be nice if this generalises to n-argument functions.

Is there a macro a package that does that? I like how Haskell does it for you automatically.

3 Likes

Here’s a solution I worked out a while ago:

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.

I suppose this could go in a tiny package.

2 Likes

Yeah. Can we call it PartialCurry.jl?

I was thinking Currier.jl :smiley:

4 Likes

But partial functions are not the same as currying?

Is Currier.jl by position only?

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:

julia> using MagicUnderscores

julia> @_ [1,2,3,4] |> filter(_>2, _)
2-element Array{Int64,1}:
 3
 4

julia> @_ [1,2,3,4] |> filter(_>2, _) |> length
2
1 Like

Neat!

I often want to curry in this direction though, maybe @rcurried or something :stuck_out_tongue:

foo(1)(2)(3) == foo(3, 2, 1)

Done!

julia> @reverse_curried bar(x, y, z) = (x, y, z)
bar (generic function with 2 methods)

julia> bar(1)(2)(3)
(3, 2, 1)

julia> bar(3, 2, 1)
(3, 2, 1)
2 Likes

Sweet!

1 Like

I also made a Curry.jl using generated functions.

1 Like

Love that :curry: is an export function!

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

1 Like

Are you able to make that into a pacakge? I am noob at meta-programming but I would have a crack later on.

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

1 Like