I am really impressed by recent improvements due to the good fight against invalidations. I’m just wondering if the ubiquity of invalidations doesn’t indicate a missing syntax feature. I know Julia is not at that stage of development and all but wouldn’t it be great if you could assure that all methods with a particular name returned the same type? Something like (but prettier?)
@invariant function fun(x)::Int
5
end
then writing
function fun(x::Float64)
return 5.0
end
fun(1.0) # runtime error, broken invariant,
or better yet, sometimes error after defining, when the compiler detects that the invariant will be broken
For instance, we know that constructor-named functions should really return their respective structs so that
function Int(x)
Float64(x) # because I hate you
end
should really be an error. But also certain other well regarded symbols
function Base.!(x)
Float64(x) # I am an evil idiot
end
should also really just error. It’s kind of like the new wonderful global type annotations… in many cases the problem wasn’t mutation per se, it was the type instability. Functions are global variables too shouldn’t there be a way to enforce the type invariance of their calls? Or is this too much breaking the power of Julia’s mega-dispatch abilities? Would this kind of thing break autodiff and other important stuff more than run-of-the-mill type signitures?
EDIT: I guess invariant is a loaded comp sci term that I’m probably misusing here. I just mean the return value is always the same type, hope this is clear from context.
I’m going to be very honest (and I’m surprised I’m saying that, as I’m very much in favor of more static guarantees): This feels like trying to bolt features of static languages onto a dynamic one. My concern is not necessarily that this breaks other parts of the ecosystem, but that its use becomes too pervasive out of fears of invalidation, even if they’re unfounded.
Indeed. I share these concerns. The types in signatures are already overused, I’m always hearing: “Don’t annotate unless it’s for dispatch!” But that’s really because annotations don’t often fix code_warntype anyway, you have to annotate inside the function at the unstable function calls. This would attack that issue more directly in many cases.
I believe something like this syntax would be very useful, both for dispatch, and for correctness. Though I imagine it would have to be parametric and allow for more powerful things. But for some functions like converts, constructors and booleans it might lead to a large reduction in invalidations.
I don’t think it can be allowed to affect dispatch because that would be a spooky action at a distance effect: you write some code and it doesn’t do what it should because there’s some “contract” or “invariant” that you’re unaware of somewhere. What it can do is implicitly add some type assertions at every call site. Unfortunately in order for this to be useful for a lot of built-in functions we’d have to make a breaking change, but we can probably figure out some way to make it opt-in.
It would be breaking for sure. I imagine implementing something like it wouldn’t be breaking, and I don’t know how it could handle overloading from two different packages for example. Because it becomes a bit like member functions where you have to follow something defined somewhere else and possibly hidden. I think something like it would be interesting but I imagine a looot of design work would be needed for it.
Here’s a sketch: there’s some special function name like __function_type__ that is used to compute the return type assertions on functions. If it’s not defined then they don’t get emitted. Then you can write using FunctionTypes to get a definition that works. Instead of having to put fiddly annotations everywhere you just put that in a module in one place. In Julia 2.0 it can be imported by default. We’d need to think a little about what the function interface looks like, maybe a bit like tfuncs?
I really don’t know what I’m doing but i tried the somewhat obvious and probably slow
mutable struct IFun{T,F}
fun::F
function IFun(::Type{T},fun::F) where {T,F}
new{T,F}(fun)
end
end
function (a::IFun{T})(args...)::T where {T} # type annotation gets enforced
a.fun(args...)
end
function funs(x::Float64)
x + 2.0
end
ifun= IFun(Float64, funs) #result of macro á la @memoize
function funs(x::Int) # extends the referenced function
x+1
end
ifun(2) # 3.0
ifun(2.0) # 4.0
which could be macro’ed but doesn’t quite do what we want, because funs and ifun have different names. If only function ... end itself could dispatch on IFun type to actually only extend the function that’s its only field, then this could be turned into a macro annotation? Anyway before we all agree that this is super hard to get right in a robust, performant way… @StefanKarpinski liked my idea! Yay.