Part of my learning process with Julia has been to abuse the type system in various fun ways to see what’s possible. My latest monstrosity uses type parameters for partial application of functions:
module TypeAbuse
struct Fix{Fn,Args,KW}
Fix{Fn}() where {Fn} = new{Fn,(),(;)}()
Fix{Fn,Args}() where {Fn,Args} = new{Fn,Args,(;)}()
Fix{Fn,Args,KW}() where {Fn,Args,KW} = new{Fn,Args,KW}()
end
function (::Fix{Fn,Args,KW})(moreargs...) where {Fn,Args,KW}
Fn(_cat(Args, moreargs)...; KW...)
end
_cat(a, b) = (a..., b...)
end
let c = TypeAbuse.Fix{+,1}()
@show c
@show c(2) == 3
end
let c = TypeAbuse.Fix{digits,(),(; base = 2)}()
println()
@show c
@show c(5) == [1, 0, 1]
end
c = Main.TypeAbuse.Fix{+, 1, NamedTuple()}()
c(2) == 3 = true
c = Main.TypeAbuse.Fix{digits, (), (base = 2,)}()
c(5) == [1, 0, 1] = true
I benchmarked calling TypeAbuse.Fix{log,5}()
with rand()
against the equivalent Base.Fix1
call, and on my system calling both log(5, rand())
and the TypeAbuse.Fix
takes ~24ns, where Base.Fix1
takes ~33ns, so it’s basically zero overhead at least in that particular case. I assume this is due to it being a singleton type and having both the function and its partially applied arguments available statically, meaning no construction of a new Fix
instance and no field lookups?
Now, this benchmark result got me curious about how inadvisable this is, exactly. I know it will create a new type for every new Fn
, Args
and KW
, increase compilation time at least to some extent, and only work with Args
s / KW
s that are isbitstypes
(I think?), but considering those caveats is this actually as much of a crime against the type system as it originally seemed to me? As a beginner it feels like it could also actually be a useful optimization to shave off some nanoseconds from calling partially applied functions, assuming a case where those nanoseconds actually matter and the number of different type parameters doesn’t get out of hand.