I want to use a finalizer for a parametric type using the type parameter in the finalizer. I have a solution for that, but I wondered whether I can use the do syntax, but I simply do not know where the where needs to be put to be syntactically correct.
The following examples need the REPL to fire the finalizer.
Using the do syntax without a parameter works:
mutable struct MySimpleStruct
x::String
end
s = MySimpleStruct("What is the meaning of life?");
finalizer(s) do s
@async println("Finalizing: $(s.x)")
return s
end;
finalize(s)
With a parametrized type, this gets a “T not defined” error, because where is missing and I do not know where to put it:
mutable struct MyStruct{T}
x::T
end
s = MyStruct("What is the meaning of life?");
finalizer(s::T) do s
@async println("Finalizing: $(s.x) using $(T |> nameof)")
return s
end
Working around this is possible, but misses the typical Julia elegance:
mutable struct MyStruct{T}
x::T
end
s = MyStruct("What is the meaning of life?");
function myfinalize(s::T) where T
@async println("Finalizing: $(s.x) using $(T |> nameof)")
return s
end;
finalizer(myfinalize, s);
finalize(s)
I don’t recall anonymous functions (which is what do clauses really are) supporting where clauses see below for an example by @savq — the main purpose of where is to control dispatch, which doesn’t arise for anonymous functions, since they don’t (normally) have multiple methods.
You can just do T = typeof(s.x) in the body of your function (do clause) to get the type.
You could also pass an anonymous function without using do:
mutable struct MyStruct{T}
x::T
end
s = MyStruct("What is the meaning of life?");
finalizer(
function(s::MyStruct{T}) where T
println("Finalizing: $(s.x) using $(T)")
end,
s
)
I think this is a parser thing, not a functionality thing. It’s doable with anonymous functions:
julia> f = ((x::Vector{T}) where T) -> println(T)
#1 (generic function with 1 method)
julia> f(rand(4))
Float64
julia> f(rand(Float32,4))
Float32
However, doing the same with the do syntax fails because it’s parsed differently.
julia> dump(:(((x::Vector{T}) where T) -> println(T)))
Expr
head: Symbol ->
args: Array{Any}((2,))
1: Expr
head: Symbol where
args: Array{Any}((2,))
1: Expr
head: Symbol ::
args: Array{Any}((2,))
1: Symbol x
2: Expr
head: Symbol curly
args: Array{Any}((2,))
1: Symbol Vector
2: Symbol T
2: Symbol T
...
whereas the do arguments are parsed diffently:
julia> dump(:(finalizer(v) do ((x::Vector{T}) where T); println(T) end))
Expr
head: Symbol do
args: Array{Any}((2,))
1: Expr
head: Symbol call
args: Array{Any}((2,))
1: Symbol finalizer
2: Symbol v
2: Expr
head: Symbol ->
args: Array{Any}((2,))
1: Expr
head: Symbol tuple
args: Array{Any}((1,))
1: Expr
head: Symbol ::
args: Array{Any}((2,))
1: Symbol x
2: Expr
head: Symbol where
args: Array{Any}((2,))
1: Expr
head: Symbol curly
args: Array{Any}((2,))
1: Symbol Vector
2: Symbol T
2: Symbol T
...
i.e. a tuple arg, not a where arg for the -> operator.
Thanks for the insights. Sorry that I didn’t mention it, but inside the finalizerT is used to hand over a parameter to a @ccall (as Ref{T}), so using typeof is unfortunately not an option, as it results in:
ERROR: LoadError: could not evaluate ccall argument type (it might depend on a local variable)
Although where’s main purpose is to control dispatch, it’s not its only purpose.
That’s pretty ingenious.
Following from that, this is probably trivial, but I needed to see it, too, for anonymous functions created with a do-block:
julia> global global_f
julia> savef(f) = global global_f = f
savef (generic function with 1 method)
julia> savef() do x
"any"
end
#1 (generic function with 1 method)
julia> (::typeof(global_f))() = 7
julia> global_f
#1 (generic function with 2 methods)
Rather than finalizer(s::T) do s (with a where… somewhere), the syntax should be something like:
finalizer(s) do s::T where T
#...
end
Unfortunately this does currently mean something — it’s an anonymous function like:
function anon(s::T where T)
# no T binding available here :(
end
The do syntax doesn’t use parentheses, so I don’t think there’s a way to “move” the where clause outside of parentheses that don’t exist. I suppose we might be able to use something like do (elt::T) where T, but boy is that fiddly — especially when you start adding multiple args.
It’s actually possible with a quite simple macro which rewrites the do syntax to -> syntax. I haven’t tested it thoroughly. I suspect it’s just a bit of luck with the parser.
using MacroTools: @capture
macro wheredo(ex)
@capture(ex, F_(fargs__) do args__; body__; end) ||
error("Can't find do construct: $ex")
:($F($(args...) -> $(body...), $(fargs...))) |> esc
end
@wheredo map(1:4, ["a","b","c","d"]) do (x::S, y::T) where {S, T}
println("S: ", S, ", T: ",T)
string(x) * string(y)
end
Maybe it would be possible to change the parsing of
finalizer(s) do s::T where T
#...
end
to mean:
function anon(s::T) where T
# T binding available here :)
end
I see the following advantages for new code:
As there are no parentheses, that’s exactly what I would expect seeing this code
Every use case where T is only available in the function arguments should also be possible if T is additionally available in the function body
If there should come up the need of having T only available in the function arguments this could be added at any time by supporting a notation using parentheses (adding previously non-existing parentheses is far simpler than removing previously non-existing ones ;-))
I understand that, strictly speaking, this would be an incompatible change. However, I assume the risk is really low, because
If someone had used do together with where that would have been based on undocumented behavior
The current syntax seems to support only single arguments, where the T is only available in function arguments, and this is rather pointless
The behavior would only change if there is an identically named identifier (typically T) already used inside the do function body
The compound probability of all three small probabilities seems to be very small. If we wanted to be even more careful, we could probably parse the packages registered in the Julia package registry before and after the parser change and verify that the Exprs are unchanged.
The risk of non-public code being changed would not be eliminated, but it could strengthen the assumption that the probability for this is very low.
Interesting solution, thanks. Both using the parentheses after the do and the curly braces after the where seem to be essential in order to have this working.
I think it might be fairly easy to allow ... do (x::T) where T, i.e. with parentheses. And curly braces around {T, S} if more type variables. It’s already being parsed, and the simple rewriting to -> in the above @wheredo is sufficient. The parser rewrites to -> as well, but in a slightly different manner which makes this construction fail. However, who knows if this would break something.
However, the precedence of where must allow constructions like f(x::S, y::T where S<:T<:Real) where S = x+y being parsed as (T where S<:T<:Real). For this reason the @wheredo macro above will fail with ... do x::T where T, i.e. T will not be visible inside the body. This form of do is already legal and works, but T is not visible.
For this reason, where has a different precedence if it follows a ::. That’s why constructions like f(x::S, y::T)::S where {S,T} = x + y don’t work, you need parentheses: (f(x::S, y::T)::S) where {S,T} = x + y.
The Julia project already uses something like this as part of some workflows, Nanosoldier uses PkgEval to run the test suites of all (or selected) registered packages before and after a change to Julia. See here: