Reconstruct closure?

I have some function like

adder(x) = y->x+y

which is said to lower to roughly

struct ##1{T}
    x::T
end

(_::##1)(y) = _.x + y

function adder(x)
    return ##1(x)
end

Now if I do

g = adder(3)
println(typeof(g))

the output will look kind of like:

var"#1#2"{Int64}

If only have g, and adder can be any arbitrary function that I don’t know, then Is there a way I can construct an adder with a different x? e.g. I tried

julia> typeof(g)(5)
ERROR: MethodError: no method matching var"#1#2"{Int64}(::Int64)
Stacktrace:
 [1] top-level scope
   @ REPL[39]:1

Probably easiest to use Accessors.jl (or Setfield.jl) for this:

julia> plus(x) = y -> x + y
plus (generic function with 1 method)

julia> p2 = plus(2)
#plus##0 (generic function with 1 method)

julia> using Accessors

julia> p3 = @set p2.x = 3
#plus##0 (generic function with 1 method)

julia> p2(1)
3

julia> p3(1)
4
3 Likes

amazing! thank you!

While you can do this with Accessors.jl, if you actually need to do this, I think you’re much better off just making a callable struct yourself. I.e., you should probably just write

struct Adder{T}
    x::T
end
(a::Adder)(y) = a.x + y

and then work with Adder and ::Adder objects rather than trying to re-construct closures.

7 Likes

To be fair, “any arbitrary function” may be implying that we’re not developing it. However, I agree that bypassing an API has serious risks. Accessors does not bypass a type constructor or its invariants, but closures e.g. y->x+y and their lowered type constructors don’t enforce their own invariants, the outer functions e.g. adder do.

julia> evenadder(x1) = (x = iseven(x1) ? x1 : zero(x1) ; y->x+y)
evenadder (generic function with 1 method)

julia> g1 = evenadder(1) # odd input, so falls back to adding 0
#evenadder##0 (generic function with 1 method)

julia> g1(0)
0

julia> evenadder(2)(0)
2

julia> evenadder(3)(0)
0

julia> g3 = @set g1.x = 3
#evenadder##0 (generic function with 1 method)

julia> g3(0) # invariant not enforced
3

The Accessors approach also relies on the field x existing. Although closures are currently implemented to lower to fields sharing the names of arguments and we can discover these names with reflection (fieldnames, dump), this is not specified for the language. The link is to the devdocs, which describes Julia internals. I don’t expect this implementation to change anytime soon, but there’s nothing stopping restructuring or name-mangling from breaking @set p2.x = 3 down the line.

3 Likes

Yes, but the problem is that if @mattsignorelli isn’t the one writing the function, then they also don’t have control over what the property names are. i.e. the captured variable x could be renamed by the author to xxxx as a non-breaking change but that’d screw up the handling here.

I’m not saying you should never do this, but I am saying that one should think quite hard about how to avoid having to do the thing that the OP is asking for here.

1 Like