Discussion thread for the deinit proposal.
cc @clattner as proposal author
I like everything up to Step 3: Restrict usage of the deinit convention
. However, I think that making it so that you canāt have one type deinit
another is going to restrict some of the use-cases for linear types, such as compile-time refcounting were you want to deinit
your āsub-handleā to the type using some sort of main handle which is actually responsible for the destruction. Other places where I could see something like this are for allocators with pointer types designed to force you to āfreeā the pointer into the correct arena, where this makes it impossible to batch-deallocate objects without some annoying workarounds such as doing a typestate transformation on all of them, which may require a buffer to move them into as part of the transformation.
One other consideration is whether this unnecessarily restricts various forms of the typestate pattern, for instance if you wished to prove that two independent things had happened, the methods which cause them to happen could return linear-type āproof tokensā and require you to pass in some number of them to prove that prior events happened. As an ML-flavored example, you might want to use them to prove that 4 GPUs exist in order to construct a 4-GPU model pipeline, requiring 4 differently-typed tokens.
I prefer keyword del
for argument convention. Looks straightforward and self-explanatory:
struct Foo:
fn __del__(del self): ...
+1 for del
as the name of the argument convention. It is concise and denotes the argument that will be deleted eventually.
fn __moveinit__(out self, del existing: Self):
I think the proposed design for deinit
would be a mistake. Whether a function directly or indirectly destroys a value is an implementation detail, and should not be part of the functionās signature. The user of the function doesnāt need to know the difference between var and deinit, and putting this information in the public API of Mojo libraries is probably going to cause unnecessary confusion. Mojo learners will see ādeinitā in function signatures and be confused about what it means, when ultimately, from the userās perspective, it has identical semantics to āvarā.
I think the alternative Chris listed at the end of his proposal is the right one: give __disable_del
a better name, and require __del__
and __moveinit__
to invoke it explicitly. This makes the āfinal deinitialization stepā for a value an implementation detail, and therefore frees people from needing to think about it!
Final note: The proposal mentions that var
should be used instead of deinit
in function types. This is further evidence that deinit
should not appear in a functionās signature. The type checker doesnāt care about deinit vs var, and the programmers that are using the function shouldnāt need to care either.
The proposal mentions that var
should be used instead of deinit
in function types.
I do have a small concern about this. This means that I can pass the function fn foo(deinit x): #...
to a parameter that expects fn foo(var x)
. Even if this works since both functions have the same signatures, this is still a little unintuitive as I would (and did) assume initially that their signatures would be distinct.
I do also see __disable_del
as a little bit of a foot gun too. Iām not necessarily for or against this proposal; Iām just pointing out something that should be taken into consideration.
Final note: The proposal mentions that
var
should be used instead ofdeinit
in function types. This is further evidence thatdeinit
should not appear in a functionās signature.
I agree. This might make explaining argument conventions more difficult than before.
In this case I would opt to stick with __disable_del
but require it to be invoked explicitly: just remove the magic. How about renaming it to del
?
struct String:
...
fn __del__(var self):
"""Destroy the string data."""
self._drop_ref()
del self
fn __moveinit__(out self, var existing: Self):
...
del existing
@christoph_schlumpf I think the solution youāve proposed would be fantastic. Itās simple, and it keeps the function signature as var
, which is what we want.
Iām not sure whether it makes sense to use the keyword del
, because weāre discussing an unsafe/restricted feature (marking a struct instance as deleted), whereas in Python del
is a safe feature that anybody can invoke at any time.
Also, I think Mojo is currently using del
to mean āinvoke the destructor on this particular lineā. del x
is sugar for x.__del__()
In summary: Your code snippet is very compelling; but we might need to use a different keyword.
I did a scan on the modular repo and did not find any usage of del
in .mojo
files. But if the name should better indicate that it is a restricted unsafe feature a different name might be better. __del
?
struct String:
...
fn __del__(var self):
"""Destroy the string data."""
self._drop_ref()
__del self
fn __moveinit__(out self, var existing: Self):
...
__del existing
For linear types this feature will be used quite often and I thought del
is the closest to Phyton del
for Mojo structs. But I see that del
has additional/different semantics in Phyton that Mojo maybe wants to support in the future like deleting items from a list (__delitem__
).
Python:
numbers = [1, 2, 3, 4, 5]
del numbers[2]
numbers # [1, 2, 4]
So del
might not be a good choice if this is the case. __mark_del
or just __del
then?
I think if we require the devs to explicitly call __disable_del on some of the methods, it is no longer an implementation detail of free choice. Having it in function signature also documents what happens to the passed variable which i would see as a positive..
BTW another term we could use instead of `del is
`destroy`. But then it is inconsistent with many places we use del..
Should this be taken into account? I think no, as in both Python and Mojo del
will denote deleting object
Iām not a Python expert. But if itās semantically and compiler side possible del
would be ideal in Mojo IMO.
Yes, using arg convention is making __disable_del
more like an interface,
so that isolation of change is pretty useful!
Also, this makes it structured and a part of something,
which can be used for logic.
I see the point that specifying the disable del in the signature seems to be irrelevant for librabry user sand may cause inconsistency as `var` and `deinint`
may be used interchangeably at some places.
However, I think in some cases deinit does convey important information to the user, as it communicates āthere is a custom desctruction implemented hereā, which is important, since the custom behaviour may diverge from the otherwise implemented __del__. That means, I cannot rely on the things put in __del__
to be executed even though the type may be properly destroyed.
I would like to thorugh in some alternatives, not being convinved of them myself, but maybe they help in the further discussion. Often they would require some extra work to actually make proper sense. The main idea is to add another modifier to var
, defined to be ignored at the places where var and deinint could be used interchangeably:
fn fun(var del argument):
...
var
:fn fun(var_del argument):
...
var[ā¦] and ref[...]
):fn fun(var[] argument): # no destructor specified, so no del
...
fn fun_alternative(var[None] argument): # no destructor specified, so no del
...
fn fun_default(var[argument.__del__] argument): # default, same as no square brackets
...
fn fun(var[] argument): # no instance marking that the destructor is called when it is out of scope is specified, so no del
...
fn fun_alternative(var[None] argument): # same as above
...
fn fun_default(var[argument] argument): # default, same as no square brackets
...
@no_destroy(argument)
fn fun(var argument): # no destructor specified, so no del
...
Maybe we could find a solution along those lines⦠I think I like the idea of designing something in the direction of origins, i.e., option 3b.
Btw. if we stick to using some kind of __disable_del, I would prefer something like mark_as_destroyed(arg), as this is seems more like a proper function rather than a modifier to compiler behaviour (though it might in fact be the latter). I simply think it reduces the load of special syntax.
Re: __del
: Shouldnāt it be __no_del
? It disables the destructorā¦
Unfortunately this is not the case as deinit
would only be allowed for functions that directly destroy the argument. Therefore it is an internal implementation artefact that would change if the function calls another function that does the destruction. It would not show up in the function type declaration too. So the user will still not know from the function signatur if the argument will be destroyed eventually. A decorator would have the same issue. The same holds for functions that conditionally destroy the argument. Those functions too would have to use the var
argument convention.
Re:
__del
: Shouldnāt it be__no_del
? It disables the destructor
True. But from the viewpoint of the implementer and caller of the function __del value
indicates that the value
will be deleted eventually (by allowing the function to destroy the value
internals and postponing the immediate deletion of the whole value
).
Would an unsafe opt-out of the restriction work here?