# Thoughts and tempering expectations on redefining structs

**URL:** <https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190>\
**Category:** Internals & Design\
**Tags:** struct\
**Created:** [July 5, 2023, 12:11am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190 "2023-07-05T00:11:43Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 5, 2023, 12:11am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/1 "2023-07-05T00:11:43Z")

</div>

TLDR: musings on how redefining structs cannot be as convenient as we’d like, nothing really important so feel free to ignore this thread

Overoptimistic expectations

We enjoy the interactivity and incremental code modifications that a dynamic language offers, and Julia’s multimethods achieve a particular kind of intermodule composability, if we sensibly avoid type piracy. But while we can add or replace methods in a function and invalidate its callers to adjust, we cannot change anything within a struct definition. There have been other discussions about this “missing feature”, and they generally expect an analog to method invalidation: change the struct, and the callers adjust accordingly. After all, it’s a dynamic language, you can redefine classes in Python, so why not structs in Julia?

It’s much more complicated than method invalidation

If we expect an analog to method invalidation, then we should compare structs and functions more carefully. First we should recognize that they’re not distinct concepts. A function has a singleton subtype of `Function`, a `const` name, and no fields. Like a function, a type constructor is a multimethod and also causes invalidations of callers. However, unlike independent methods, a `struct`’s parameters, supertype, fields, and inner constructors are rooted in the type and are usable by any method. It’s not possible to make multiple versions of these like multimethods, so a `struct` block necessarily attempts to reassign the `const` name of the type. Currently, it’s not possible to replace a struct like that, but it’s also not possible (or even desired) to replace a function, either. When we write `function` blocks for an existing function, we do not reassign that `const` name, we just add or replace one of multiple methods.

But let’s assume that we’re trying to make replacement of structs possible. You can invalidate a caller `foo() = MyType(1, "a")`, and that does happen already. However, types don’t just show up in calls. A big one is argument annotations, like `bar(::MyType) = 0`. If the annotated types would be replaced, these methods would be essentially replaced, and the method dispatch tree would be modified. Commenters feel free to correct me, but this could be tracked with some version of backedges, though I expect much more overhead as I believe type annotations outnumber function calls. Bear in mind, automatic inheritance adjustment does not occur in Python: the obsoleted class remains in superclass lists, so subclasses must be reevaluated (though it’s easy to find them).

The thornier issue is existing instances in the global scope, whether of the obsoleted type or from a function call that relied on the obsoleted type on some level. Tracking and reevaluating these can become very expensive (`const ohgodwhy = takes5minutes(args...)`). Unlike recompilation of invalidated methods, reevaluation of expressions cannot be lazy and incremental much because different orders of evaluations can result in wildly different behavior.

Reevaluation causes artifacts, replacing instances is questionable

The straightforward way to replace a struct is to restart the REPL and evaluate everything from scratch, which can take an undesirably long time to load necessary dependencies and recompile methods. However, redefinitions alone cannot replicate the behavior of a full restart, and this is true even for method redefinitions e.g. `baz() = 1; a = baz(); baz() = 2; a # still 1`. That raises the possibility of using automatic reevaluation to have the active code fully adapt; `baz() = 2` triggers an implicit `a = baz()`. Problem is if we replaced the call signature, then past code would throw `MethodError`s if reevaluated automatically after redefinition. In most cases, there won’t be a sensible way to 1-to-1 replace a call signature in source code, that must be done manually, and only after that is reevaluation possibly useful. This can get much more complicated than global instances, with cross-module influences and conditional evaluation of methods.

Even assuming the call signature is not changed, one suggestion (that I also have made) has been to only reevaluate in the module the struct belongs to, the idea being it’s isolated, but modules are rarely fully isolated from one another. Other modules will also need to be reevaluated to adjust behavior, and it’s not as “simple” as tracking functions extended on the type and modules importing such functions or the type. For an example, let’s say my working module uses custom units types in a method to compute the speed of light `const c = computec()`, which gets exported very liberally. Then I change the types to different units, which the value of `c` adjusts to. However, none of the modules that imported `c` imported my types, so they happily use the obsolete value. You might then suggest to track all modules that import anything from my working module, but that doesn’t cover modules that import different values, like a light-year, computed from `c` outside my working module. If we play it safe and reevaluate in order every module our working module can reach via imports, it could easily save very little time. If we extended any function in `Base` on the redefined type, which is imported in every module except `baremodule`s, we might as well restart the REPL to save on tracking overhead.

Let’s assume that it’s just unfeasible to find and evaluate a subset of past code that both replicates a restart and saves a lot of time, and we choose the latter: just replace the type, its global instances, and the live methods using them; every other necessary change, like methods that call the type constructor, must be done manually. Then we have to also decide if that retroactivity is the sort of dynamic feature we want. Take method redefinitions again `baz() = 1; a = baz(); baz() = 2; b = baz(); (a, b)`; we _expect_ to see that change in `(1,2)`. (FYI avoid method redefinitions in source code, those are unnecessary and break incremental compilation.)

Strategies and package highlights

This doesn’t mean we need to restart the REPL every time. Replacing a struct only becomes a big reevaluation problem if you had built many methods and modules on top of it.

1. When you are still experimenting, you can make throwaway structs and test a few interface methods on them, and try to reach a decision on the structure and supertyping before extending the functionality.
2. If you carefully make a supertype interface and make many methods rely on that supertype instead, a new subtype struct and its interface methods can smoothly integrate into the supertype’s methods (this doesn’t handle global instances though).
3. If you haven’t exported the type or extended other modules’ functions with it, you can replace the whole module without affecting other modules at all. Julia v1.9 makes this smoother by letting you change the module you interact with in the REPL. Packages don’t have to be reloaded, just reimported. Other modules’ functions extended with obsolete types should not get in the way if you never use the obsolete type again, it’s just that those obsolete methods will show up in reflection, which can be confusing.

People only desired redefinable structs for experimenting, so it’s reasonable to keep the experiments small and isolated so the widespread changes are feasible without restarting everything. There are a couple packages for this context; bear in mind that neither package does retroactive reevaluation.

1. RedefStructs.jl is not kept up to date with Julia, but the macro `@redef` renames a `struct` block to make a hidden struct assigned to a non-`const` global name you provided. It’s a bit more convenient version of throwaway structs.

2. ProtoStructs.jl is a bit more updated, and the macro `@proto` changes a `struct` block to a different struct parameterized by and containing the fields as a `NamedTuple` or `Dict{Symbol,Any}`, depending on if the struct was immutable or mutable. Another `@proto` block just changes the active parameter of the type constructor. Unlike RedefStructs.jl, this strategy does not accommodate supertyping, and it appears the type parameters must be entirely inferable from field values.

---

<div class="post-metadata">

**Author:** ![BeastyBlacksmith](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/beastyblacksmith/32/4741_2.png) [@BeastyBlacksmith](https://discourse.julialang.org/u/BeastyBlacksmith)\
**Post date:** [July 5, 2023, 7:00am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/2 "2023-07-05T07:00:53Z")

</div>

Great writeup, thank you!

> [@Benny](#):
>
> Unlike RedefStructs.jl, this strategy does not accommodate supertyping, and it appears the type parameters must be entirely inferable from field values.

If you happen to have some examples for this, I would be glad if you put those in an issue on the repository 🙂

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 5, 2023, 7:20am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/3 "2023-07-05T07:20:53Z")

</div>

AFAIK there is nothing that _conceptually_ prevents redefining structs, it’s just that the issue involves reworking the internals of Julia that only a few people know how to do, and they currently do not have the bandwidth for this. The latest proposal is

> <https://github.com/JuliaLang/julia/issues/40399>
>
> I've been doing some work that has a fairly big dependency stack (100-200 packag…es) and as such takes a few minutes to warm up. Because of this non-Revise-able code is extremely painful, so I'd like to take another look at https://github.com/timholy/Revise.jl/issues/18. This has some fairly long history of course, but to avoid getting buried in the discussion, I figured a new issue for my specific proposal would be best.
> 
> One solution that was previously discussed (in https://github.com/JuliaLang/julia/pull/22721) was to have backedges for bindings, but this was rejected as too expensive. My proposal aims to basically do this, but shift the cost of backedge tracking to the redefinition stage where it is more acceptable.
> 
> In particular, my proposal is:
> \- Make each module's binding table partitioned by world ages. Introducing a new binding does not raise the world age, but bindings do record the world age where they were introduced.
> \- Change binding lookup to only match bindings that are visible to the current world age
> \- When wanting to redefine a binding (e.g. to replace a struct definition by an updated struct definition of the same name), we do a GC-like walk of all methods in the system and look at their source. If they contain a GlobalRef for the binding being replaced, we truncate the world bounds of any associated method instances (in effect having the GlobalRef serve as "forward edges" that get re-validated by the redefinition). This operation would increase the world age.
> \- We add an array for explicit forward edges from non-syntactic resolutions of bindings during inference. This can basically happen for two reasons:
> 1. Something like \`getfield(Main, :foo)\` which will get resolved via the getfield\_tfunc.
> 2. Generated functions
> 
> cc @JeffBezanson @vtjnash
> 
> If this proposal seems reasonable, I'd hope to nerdsnipe @timholy into taking charge :)

While not being able to redefine structs is irksome, it does not prevent Julia from being usable, so the developers can take their time addressing this in a way that meshes well with other features.

---

<div class="post-metadata">

**Author:** ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)\
**Post date:** [July 5, 2023, 12:32pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/4 "2023-07-05T12:32:34Z")

</div>

I think redefining structs is in a different category, most likely in the bucket “we don’t want to go there.” The compiler is allowed to make all sorts of decisions during codegen conditioned on the assumption that types are `const`, and there’s no equivalent of backedges documenting consequences of those decisions. Unless those get added, we can’t accurately perform invalidations if types change their definitions. And adding all those backedges would likely bloat the system enormously. That’s a pretty huge cost to pay for a little extra developer convenience.

Given that recent versions of Julia allow you to replace `Main` with a new module, there’s a decent workaround that could stand a bit more usage (and perhaps tools to make it easier). But I’ve not played with that much myself.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 5, 2023, 12:50pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/5 "2023-07-05T12:50:15Z")

</div>

> [@tim.holy](#):
>
> there’s no equivalent of backedges documenting consequences of those decisions. Unless those get added, we can’t accurately perform invalidations if types change their definitions.

I am aware that this was the approach in [#22721](https://github.com/JuliaLang/julia/pull/22721), and it turned out to be expensive, but does this apply to the more recent world-age based approach linked above?

> [@tim.holy](#):
>
> That’s a pretty huge cost to pay for a little extra developer convenience.

Of course this depends on the cost of the solution, but arguably the convenience is not trivial. We have learned to work around this, but solving this would make exploratory coding much more convenient.

---

<div class="post-metadata">

**Author:** ![gbaraldi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gbaraldi/32/22101_2.png) [@gbaraldi](https://discourse.julialang.org/u/gbaraldi)\
**Post date:** [July 5, 2023, 1:58pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/6 "2023-07-05T13:58:34Z")

</div>

So the issue with this is that there isnt a concept of world age for objects/types, what is the behaviour if I have an array of the struct that was redefined, should it be invalid to use or should it behave as the old type, are you allowed to make a copy etc.

---

<div class="post-metadata">

**Author:** ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)\
**Post date:** [July 5, 2023, 2:19pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/7 "2023-07-05T14:19:46Z")

</div>

> I am aware that this was the approach in [#22721](https://github.com/JuliaLang/julia/pull/22721), and it turned out to be expensive, but does this apply to the more recent world-age based approach linked above?

Hmm, I’d forgotten about that PR (I should have clicked, sorry). The storage requirements are more minimal, although not zero because the number of forward edges might be substantial in some code. The walk of all compiled code would take a while, but it might be less time than, say, building a large package.

> what is the behaviour if I have an array of the struct that was redefined, should it be invalid to use or should it behave as the old type, are you allowed to make a copy etc.

Handling existing data is a thorny problem. There’s no plausible way to automatically migrate over to the new type. You might still have old compiled code apply to old data, and it would be easy to inadvertently forget to recreate something and get misleading results. I’m not saying it’s not solvable, but replacing `Main` conveniently clears your data as well as your bindings.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 5, 2023, 3:06pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/8 "2023-07-05T15:06:18Z")

</div>

> [@tim.holy](#):
>
> Handling existing data is a thorny problem.

I could live with the following heuristic:

1. If field names match, try to `convert` their contents,
2. Otherwise wrap fields in a type dedicated for this purpuse, in a `NamedTuple`, with some metadata.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 5, 2023, 6:37pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/9 "2023-07-05T18:37:33Z")

</div>

> [@BeastyBlacksmith](#):
>
> If you happen to have some examples for this, I would be glad if you put those in an issue on the repository 🙂

I meant a couple things. `@proto struct X <: Number end` won’t incorporate the supertype as intended; looking at the `@macroexpand`, you get `struct X{NT <: NamedTuple}` and a `(X() where Number)` method that prints a warning. It is pretty doable to bake the supertype into the struct, but the other problem is a second `@proto struct X <: Real end` cannot possibly change the supertype, which makes incorporating a supertype a liability. This is a “downside” but I think it’s worth it to have a clearly named parametric type, compared to RedefStructs’ non-`const` aliasing of `gensym`ed hidden structs, and it is fine for early experimental stages of development.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 5, 2023, 6:53pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/10 "2023-07-05T18:53:50Z")

</div>

I’m still not well-versed on world age, so a lot of #40399 goes over my head. However, it seems like it’s about adjusting methods specifically, but I could be very wrong.

> [@tim.holy](#):
>
> but replacing `Main` conveniently clears your data as well as your bindings.

That’s good, I should’ve mentioned that in the essay where I talked about replacing modules. Still, good to do this before that type gets exported to other modules or used to extend other modules’ functions.

> [@Tamas\_Papp](#):
>
> > [@tim.holy](#):
> >
> > Handling existing data is a thorny problem.
> 
> I could live with the following heuristic:
> 
> 1. If field names match, try to `convert` their contents,
> 2. Otherwise wrap fields in a type dedicated for this purpuse, in a `NamedTuple`, with some metadata.

I don’t see how this is related to what tim.holy was saying. Maybe I’m misinterpreting the conversation, I thought this was about global variables. For example, what to do with `ant::Insect = Insect(3.4, 1.2); ant2::Insect = foo(ant); getants(::Insect) = (ant, ant2); @replace struct Insect mass::Float64 end`.

I can imagine `getants`’ type annotation changing automatically, but as I said in the essay, automatically changing existing data like `ant::Insect` isn’t dynamic, it’s retroactive. It’s also ambiguous in that example which input/field is intended to be kept, so an automatic change isn’t possible. Another way an automatic change isn’t possible is if the type constructors (`Insect`) or other methods (`foo`) haven’t been adapted to the struct definition yet. In the case where you planned to change some methods along with the struct, would you even want automatic changes to kick in right after the struct redefinition? These issues aren’t particular to Julia, redefining classes in Python can’t do retroactive changes for the same reasons.

Even if we mark `ant::Insect` as temporarily undefined and `ant2::Insect` as its dependent (order matters!), things get messy when we try to run and recompile `getants(::Insect)`:

1. If we go full retroactive, the type annotation adjusts to the new type but the `ant::Insect` is undefined. We run `getants(Insect(1.1))` to recompile it for the new type, but it errors because we don’t have an `ant` value.

2. If we accept that all global instances must be manually replaced and thus keep their old type, then running `getants(Insect(1.1))` would run and compile to return the old type if we forgot to replace `ant::Insect`. It’s even worse because the types have the same name so best case scenario they are printed with their world ages and we’re squinting at numbers.

This example would be even worse if it had been `const ant` instead, which is expected to never change so the data gets incorporated into the compiled code, instead of a reference to the global scope. Or if I had involved side effects and program state in `foo`.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 6, 2023, 12:35pm UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/11 "2023-07-06T12:35:45Z")

</div>

> [@Benny](#):
>
> It’s also ambiguous in that example which input/field is intended to be kept, so an automatic change isn’t possible.

Yes, this is a valid point.

Perhaps the API to redefine a struct should also involve a method for converting existing structures. Otherwise, it is indeed a nightmare: even if heuristics are allowed to simple cases (eg changing the field types to something `convert` can deal with), even trivial modifications could leave global variables in a state that could lead to subtle errors with a lot of time wasted.

One approach I can imagine is “version” old types with eg the world age, or a counter that maps to that. So when

```julia
struct Foo # 1
   value::Int
end

```

is redefined to

```julia
struct Foo # 2
    x::Float64
end

```

then previous instances of `Foo` (1) remain available, along with methods that dispatch on them etc, so that each type is tied to a world age. The user, of course, would not care about this in most cases, as the symbol would automatically map to the latest. But other types could be around as long as they are needed.

(No doubt this proposal has a lot of corner cases I haven’t thought of)

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 7, 2023, 12:46am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/12 "2023-07-07T00:46:20Z")

</div>

I’m not exactly sure whether you mean to retroactively replace existing instances or to make copies in a new world age. I lean toward the latter because it could accommodate a dynamic language. A simple example would be redefining the struct in a loop and pushing an instance to a vector per iteration; in a dynamic language, the vector’s elements would have different types (this is doable in Python).

Making copies and isolating their behavior from the obsolete world is difficult though. Unlike invalidated methods, types take up spots in the type hierarchy and affect method dispatch. If we also try to consider redefining abstract types, this gets even more complicated. A true isolation would have to duplicate the type tree and all the functions that are affected by the changes. Given 1-to-1 conversions of existing global instances from the obsolete type to the new type _and_ the type constructor calls in functions, this could result in a program copy that doesn’t suffer from artifacts of reevaluation order. I just don’t think 1-to-1 conversions between different structs could exist or be desired in practice, so manually changing the type constructor calls or writing out new methods for an additional type in the source code would probably be far less buggy. I mean, we don’t have 1-to-1 conversions for when we change a method’s intended signature, and that’s a far simpler case.

---

<div class="post-metadata">

**Author:** ![jling](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jling/32/212909_2.png) [@jling](https://discourse.julialang.org/u/jling)\
**Post date:** [July 7, 2023, 1:29am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/13 "2023-07-07T01:29:18Z")

</div>

> [@tim.holy](#):
>
> And adding all those backedges would likely bloat the system enormously. That’s a pretty huge cost to pay for a little extra developer convenience.

I’m clearly not qualified to judge the complexity of the changes needed to support it. But just from a developer perspective, I don’t think I can agree in a general sense.

I think being unable to revise struct is fundamentally breaking from the almost-perfect interactive/dynamic story for Julia development. I would almost go as far as to say it’s qualitatively the same pain you encounter in C++/C, where you have to recompile every time (they have partial recompile too!)

I’m not saying this should be a high priority, but just that as long as we can’t do it, we’re definitively in a different tier than Lisp/Python in terms of actual dynamic/interactive developer experience.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 7, 2023, 4:00am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/14 "2023-07-07T04:00:46Z")

</div>

> [@jling](#):
>
> we’re definitively in a different tier than Lisp/Python in terms of actual dynamic/interactive developer experience.

I can’t speak for Lisp, but someone should because `change-class` and `update-instance-for-redefined-class` look interesting, like a way to adjust instances without reevaluating past code and running into the issues of evaluation order and side effects. I also wonder about how instances are implemented to allow this, and whether there is an automatic adjustment of multimethod argument annotations or call sites.

I can say you’re wrong about Python. A key detail is that Python doesn’t have `const` variables, so that’s why it’s possible to “redefine” a class. However, “redefine” there does not mean backedges or forward edges are involved. It’s just creating a new class and assigning it to the existing name. Every other class that subtyped the obsolete class will not change, every existing instance will not change its type. Instantiation calls do adapt to the new class because the name cannot be `const`, but in practice, the call sites need to be edited because the call signature almost always changes.

We can do the same in Julia with non-`const` global variables, and indeed RedefStructs.jl did that.

```julia
julia> struct Foo1 end
julia> Foo::DataType = Foo1;
julia> bar() = Foo();
julia> bar()
Foo1()
julia> struct Foo2 end
julia> Foo = Foo2;
julia> bar()
Foo2()

```

Conceivably, a macro could change the way the type is printed and represented so that we could write blocks that act like a non-`const` struct. If someone does this, I only request that the printing for obsolete structs be changed to have numbering like the above or something because multiple distinct classes sharing the same name in Python is a nightmare, so nobody actually tries to redefine classes, they usually just add or change methods. Changing ` __init__ ` does make a class have a malleable structure, but it’s not really done because as I mentioned before, you need to edit all the call sites. I also don’t think we should have struct instances emulate dictionary-backed class instances, the inefficiency would be horrible.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 7, 2023, 8:38am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/15 "2023-07-07T08:38:23Z")

</div>

> [@Benny](#):
>
> It’s just creating a new class and assigning it to the existing name.

That’s pretty much what I proposed above.

> [@Benny](#):
>
> A key detail is that Python doesn’t have `const` variables, so that’s why it’s possible to “redefine” a class.

IMO `const` in Julia should be relaxed from “you promise that this will not change, otherwise dire consquences follow” to “if this changes, a bunch of stuff needs to be recompiled”. Technically this is already possible with

```julia
_my_constant() = 42

```

instead of

```julia
const MY_CONSTANT = 42

```

it just has different access syntax.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 7, 2023, 9:52am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/16 "2023-07-07T09:52:48Z")

</div>

`_my_constant` is an implicitly `const` name, but that’s really just splitting hairs because methods can be redefined. The real issue is that those are not actually equivalent; the method body is run every call. `_my_constant() = [42]` would not be equivalent to `const MY_CONSTANT = [42]`. `_my_constant() = rand((4,2))` would not be equivalent to `const MY_CONSTANT = rand((4,2))`. Your case is only equivalent because every call returns the same immutable value.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 7, 2023, 11:13am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/17 "2023-07-07T11:13:09Z")

</div>

> [@Benny](#):
>
> the method body is run every call

First, there is nothing to “run” here, the function just returns a value.

Second, for this particular method, the compiler will just inline it. Eg

```julia
julia> _my_constant() = 42
_my_constant (generic function with 1 method)

julia> foo(x) = x + _my_constant()
foo (generic function with 1 method)

julia> @code_llvm foo(10)
; @ REPL[81]:1 within `foo`
define i64 @julia_foo_8728(i64 signext %0) #0 {
top:
; ┌ @ int.jl:87 within `+`
   %1 = add i64 %0, 42
; └
  ret i64 %1
}

```

The semantics is not that different from constants. Except that backedges take care of redefinition.

The point is that `const`s might as well be modifiable (at a cost). It is a choice. `struct`s are “constant” too, but that is something that could change without fundamental upheavals of the language semantics (and of course a lot of work on the part of developers).

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 7, 2023, 11:19am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/18 "2023-07-07T11:19:32Z")

</div>

> [@Tamas\_Papp](#):
>
> First, there is nothing to “run” here, the function just returns a value.
> 
> Second, for this particular method, the compiler will just inline it. Eg

Semantically, the function _constructs_ and returns a value, 42. The compiler optimizes by making a deterministic immutable value at compile time and inlining it in other methods. It can’t do that for `_my_constant() = [42]` or `_my_constant() = rand((4,2))`.

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [July 7, 2023, 11:25am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/19 "2023-07-07T11:25:24Z")

</div>

You can do that with one level of indirection though, eg

```julia
_MY_CONSTANT1 = [42]
_my_constant() = _MY_CONSTANT1

```

and then each time you want to modify it, you just make a different constant.

Regretfully, Julia does not have anything equivalent to [`CL:LOAD-TIME-VALUE`](http://www.lispworks.com/documentation/lw60/CLHS/Body/s_ld_tim.htm); that would get rid of the indirection.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 7, 2023, 11:29am UTC](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190/20 "2023-07-07T11:29:23Z")

</div>

Isn’t this just making the point that if you want a global variable that can change its instance, you can’t make it `const`?

[Next page](https://discourse.julialang.org/t/thoughts-and-tempering-expectations-on-redefining-structs/101190.md?page=2)
