One reason is that we don’t keep backedges to type constructors, and consequently Julia doesn’t know what code it needs to invalidate if you change a type definition. There’s good reason to avoid backedges for type constructors: can you imagine if every Int(x) was tracked in the codebase? Bloat-city.
That’s intentional, because there’s no good solution to the problem of “what do I need to recalculate if my definitions change?” If you want to recalculate everything, just put your function definitions in one file that you includet and then your calculations in another file that you include. If you just need selective re-calculation (a few lines here and there), use inline evaluation from VSCode.
The Revise workflow is indeed not ideal, for the very reasons you cite. But from what I understand it is much better than Python if you’re developing Python libraries (which you may not).
This is really a curiosity I have: Some people say that they simply rename all their structs and continue coding with Revise, and that that is a working workaround. Could that be a workaround implemented in Revise directly, like it renaming all user-defined structs under the hood to something like MyStruct_rev1, each time? Could that be an optional parameter for revise?
I think there the problem is that every method that specialized to the previous type would break. If the type is changed, the methods need to specialize again, and for that they must know that the type changed. Thus the name change is required.
Hmm… Actually, the old specializations would apply to the old structure, if you still have some objects of that type lying around, but the function would have to specialize anew for the redefined structure. And moreover, isn’t this issue similar to updating specializations when you redefine a function that you depend on? Also in that case you have to detect somehow that you must update the specializations.
As I understand, for a new specialization to occur, the object type must change its “name”, that is why the renaming is a possible workaround.
Redefining a function is different, because while the types are constant, either the function recognizes that it is been called by with a new type and must specialize again, or the method is already defined. The old specialized method continues around, and that is not an issue.
The bottom line seems to be which information is needed for function specialization. If it is the type names, than the names must change.
Well, yes and no. Again, this works fine for scripts and without using Revise, by exploiting the fact that in the REPL you are allowed to replace modules (you still get a reprimand WARNING: replacing module Data., but who cares?). This however doesn’t help much when developing packages imported with Revise.
Sorry maybe I expressed myself badly. What I meant is, if you have
g(x) = ...
f(x) = g(x)
specialize f and then redefine g, then f must detect that the specialization it already has is old and must be updated. I think this is something that wasn’t present from the beginning and got implemented around 0.6. So there is already a mechanism to detect when a specialization has to be updated.
Now, regarding structures, I imagine that every structure has a representation somewhere in the internals of Julia. When I do struct S ... end, something is allocated somewhere to keep track of this structure. What I’m suggesting is that if I execute struct S ... end again, then a new structure is created, which happens to have the same name (but is still a different entity internally). This is similar to what Python does:
prints two different identities, because two classes with the same name are created, and the latter shadows the former in the current namespace (but the old still exists).
Then, functions could respond with the specializations they already have to arguments of the old type (if you happen to still have some around), and would produce a fresh specialization if you pass objects of the new type. Internally the two types are distinguishable, you just lose a “handle” to refer to the old type.
I’ll just be so bold as to quote myself (though I’m fairly certain I’ve read something along those lines from other people as well )
To expand on that (and not make this shameless self promotion):
Julia has eval (and its macro companion, @eval), which evaluates an Expression at runtime in global scope. Now imagine the following:
a.a= + 1
a = IntWrapper(1)
# no wait, actually, I want IntWrapper to wrap Floats as well
@eval struct IntWrapper
b = IntWrapper(5.0) # wait, which Constructor will be called now? And which type will `b.a` have?
increment(a) # wait, do we print here as well..?
Julia is a compiled language, not interpreted. This means that, when I call f(), the function fas a whole is compiled to machine code. But I have eval code in there that changes the behaviour of the code afterwards while staying in the same function! That’s impossible, so it’s disallowed.
There are more details here, but that’s the gist of it. You could of course change all function calls to make a dynamic lookup in case the definition of the types or the function has changed… but then you lose all benefit of compiling functions in the first place and you’re back to interpreted speeds (which, I’m sure, you also don’t want).
It’s easy to say “why not just do x” and of course you could do x, but that often comes at the cost of things we really want to have, like fast, native, compiled code that can run everywhere. If you insert dynamic lookups at any point that you call a function, writing e.g. GPU kernels is right out (without resorting to a second compiler). There are a lot of design decisions like this, and often there sadly isn’t a straightforward, compact answer why it is that way.
In any case, there have been A LOT of topics on this forum about this and similar things already - you’re not the first to encounter them and won’t be the last. There’s also a lot of issues on the issue tracker, so please do a little more research before pointing to feature X from Y and ask why julia doesn’t do that and that it’ll be just so easy to do.
This section of the manual also provides some insight on the difficulties of what you propose:
The types Bool, Int8 and UInt8 all have identical representations: they are eight-bit chunks of memory. Since Julia’s type system is nominative, however, they are not interchangeable despite having identical structure. A fundamental difference between them is that they have different supertypes: Bool's direct supertype is Integer, Int8's is Signed, and UInt8's is Unsigned. All other differences between Bool, Int8, and UInt8 are matters of behavior – the way functions are defined to act when given objects of these types as arguments. This is why a nominative type system is necessary: if structure determined type, which in turn dictates behavior, then it would be impossible to make Bool behave any differently than Int8 or UInt8.
Nothing in your answer is a good argument of why it shouldn’t be possible to redefine structures. The example you give with @eval can be flipped around and used to argue that redefining functions is bad too. Consider
h() = "old"
g() = println(h())
@eval h() = "new"
What should it print? "old" and "old", or "old" and "new"? It turns out that the answer is the former, so we could decide that your example should have a similar semantic and the redefinition of IntWrapper comes into effect only after the execution of f, not during. This however is not an argument against redefining structures.
Moreover, consider the following code too:
function foo end
struct S end
Main.foo(::S) = print("old")
s1 = M.S()
Main.foo(s::S) = print("new $(s.x)")
s2 = M.S(42)
foo(s1) # prints "old"
foo(s2) # prints "new 42"
Here, we can play the trick of redefining the module M, so we end up with two indipendent types both called M.S. The function foo is still able to distinguish them and specializes accordingly. Where is the problem?
What I’m suggesting is an extension of the current semantic of the language. I propose that we should be able to do the same as in the last example, without putting S inside a module. The following code
function foo end
struct S end
foo(::S) = print("old")
s1 = S()
foo(s::S) = print("new $(s.x)")
s2 = S(42)
foo(s1) # prints "old"
foo(s2) # prints "new 42"
should have the same behavior. Namely, two distinct types S are created, both named Main.S, and the function foo specializes separately for the two types.
This doesn’t look at all impossible to do to me. When you “redefine” a structure, in reality you actually define a new structure which happens to have the same name. The old structure still exists, you simply lose the possibility to refer to it by the same name in the current namespace, because that name now refers to the new structure.
I don’t like much the implication of what you are trying to say here. I’m not being lazy and just complaining “Oh Julia is so crappy compared to COBOL!”. I’m exploring outside the current semantic of the language and proposing options for possible extensions.