I have a function that takes an arg and callable type (shown below). I need to pass additional parameter information with the function. One of the clean ways that worked with Julia, is to define an abstract type with a parameter and define a function with the parameter (added dummy example). Probably this is not the intended use, and an alternative would be to define a function with Type or Val as input and use an anonymous function.
Will this lead to any unintended issues or performance deterioration? Also is there any other clean way to do this, without modifying the function main?
function main(arg, fn)
res = do_work(arg)
fn(res)
end
abstract type fun{T} end
fun{T}(x) where {T} = convert(T, x)
# Example
do_work(x) = x
main(2.0, fun{Int}) # Return 2 (type int)
I don’t know about performance but another approach is
function curried_converter(T::Type)
return (x) -> convert(T, x)
end
f = curried_converter(Int)
main(2.0, f)
# 2
Approach #2 (small edit to main)
function main(arg, T::Type)
res::T = do_work(arg)
return res
end
# Example
do_work(x) = x
main(2.0, Int) # Return 2 (type int)
I’m not at my computer, but my guess it that the last approach is best because it doesn’t need to compile an extra function. But I’m not sure how hard your requirement to not edit main is.
function main1(arg, fn)
res = do_work(arg)
fn(res)
end
do_work(x) = (x)
abstract type fun{T} end
fun{T}(x) where {T} = convert(T,x)
function curried_converter(T::Type)
return (x) -> convert(T, x)
end
f = curried_converter(Int)
function main2(arg, T::Type)
res::T = do_work(arg)
return res
end
If this is only going to be run once, the anonymous function is going to cost you compilation time. After running once, it’s faster to use the anonymous function. If you can edit main at all, looks like you get some benefit from using a more “normal” approach to specifying an output.
You really want to parameterise T in this case, or you are not specializing the function, and are actually putting a DataType in the field of the anonymous function struct instead of just inserting the the known T right into the convert method at compile time.
function curried_converter(::Type{T}) where T
return (x) -> convert(T, x)
end
f = curried_converter(Int)
Nice. Just learned about the impact of using parametric types in function signatures allowing generated functions recently. Clearly I have not internalized it yet!
While trying this (and variants thereof), I found strange behavior. Some sort of path-dependence in a Julia session that depends on whether it previously threw an error, it seems.
In this demo, I make it throw an error and it runs slow forevermore. Then I restart the session, re-execute without throwing an error, and it runs fast.
Any ideas what’s going on here?
PS C:\Users\unime> julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.9.0-beta4 (2023-02-07)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using BenchmarkTools
function main(arg, fn)
res = do_work(arg)
fn(res)
end
function curried_converter(::Type{T}) where T
return x -> convert(T, x)
end;
julia> main(2.0, curried_converter(Int))
ERROR: UndefVarError: `do_work` not defined
Stacktrace:
[1] main(arg::Float64, fn::var"#3#4"{Int64}) @ Main .\REPL[1]:3
[2] top-level scope @ REPL[2]:1
julia> do_work(x) = x
@btime main($2.0, f) setup=(f=curried_converter(Int))
30.519 ns (1 allocation: 16 bytes)
2
julia> exit()
PS C:\Users\unime> julia
_
_ _ _(_)_ | Documentation: https://docs.julialang.org
(_) | (_) (_) |
_ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help.
| | | | | | |/ _` | |
| | |_| | | | (_| | | Version 1.9.0-beta4 (2023-02-07)
_/ |\__'_|_|_|\__'_| | Official https://julialang.org/ release
|__/ |
julia> using BenchmarkTools
function main(arg, fn)
res = do_work(arg)
fn(res)
end
function curried_converter(::Type{T}) where T
return x -> convert(T, x)
end;
julia> do_work(x) = x
@btime main($2.0, f) setup=(f=curried_converter(Int))
4.900 ns (0 allocations: 0 bytes)
2
Probably not coincidentally, Base.Fix1 and my PartialFunsFix1 suffer from this too, as does the OP’s type-parameterized abstract typctor.
There do seems to be some bugs like this around. Sometimes including the code with Revise.jl will “fix” them.
But I’m not sure if its the fact the error was thrown, or just that it was compiled unspecialised to make a runtime function call and that’s what it’s still doing after you define do_work.
Your second version is only compiled after do_work is defined so will just inline it.
Anyway, that’s just a guess, probably actually investigating it further will give better answers. (e.g. with @code_lowered and @code_native or Cthulhu.@descend)