Comment by ~araspik on ~icefox/garnet
I think that specifying the ABI for structs and tuples would be good, regardless of whether you decide to use a function for it in the end. About other languages not supporting the data ABI: Any language that can use the C ABI for function calling also allows using the C ABI for data structures, and so (assuming you're using a simple ABI very similar to C) there is no case where an external language would support the
new()
function without also supporting the ABI-based solution. As such, you don't need to generatenew
functions at all.
Comment by ~araspik on ~icefox/garnet
The benefit of requiring this at compile-time is that you don't need to generate and save the
VALUES
array in program memory - unless, of course, you're providing RTTI and want to includeenum
values in it (although IMO there's no point in doing so as you can't use anenum
without an understanding of what it does, which means that you already know theenum
in your code i.e. at compile-time).
Comment by ~araspik on ~icefox/garnet
Ooh - we seem to have arrived at a misunderstanding. Consider the following example:
fn foo1(T: type); fn foo2(T: type, sized: Sized for T); fn foo3(T: type, sized: Sized for T, pod: POD for T);While
Sized for T
is astruct
in its own right, it does not representT
(in that it cannot be substituted forT
directly in code), but provides additional information. As such,Trait1 for Trait2 for T
isn't providing implementations ofTrait1
andTrait2
forT
, but the much weirder case of the implementation of a trait upon the implementation of a trait upon a type. Traits for each generic type, as required by a function, are passed separately as individual parameters. There's still a lot of boilerplate involved, but there's no ordering issue. There is also (still) the issue of multiple implementations of a single trait for a single type. For some traits (particularly where specialization is concerned), multiple implementations makes perfect sense, and is not really an issue (except that code has to pick a single implementation) - but for traits likeSized
, we want a unique implementation. One option is to not regulate it, and to simply state that Bad Thingsā¢ will happen if multiple conflicting implementations are created, but this is quite against the spirit of safety of Rust. Another option is to use some sort of special annotation or attribute, and get the compiler to check uniqueness, which I guess can work.Actually, I just realised that the 'unique implementation' concept may have significance for tied types in general, not just with tied types as traits. I'll need to think about that.
You've hinted at a second issue - negative constraints. This entire system assumes that traits are meant as an opt-in feature, that code can only depend upon a type implementing a trait, not upon a type not implementing a trait. I don't even know where to begin with allowing for them.
Reading about linear typing on Wikipedia, it looks like linear (and affine) typing is simply Rust's ownership and borrowing rules. What do you mean by the term?
I'm happy to provide discussion - I've been aiming at the same space between Rust and C with my own experiments, and so it's really great to see work by the others in this area.
Comment by ~araspik on ~icefox/garnet
The issue with public stub functions is that they have to be output, for the sake of external (foreign language) code where it can't be inlined. Some sort of replacement keyword for one-element tuples (e.g.
newtype T
andnewtype pub T
) may resolve the issue without resorting to the Feels Bad solution - I've always had a deep distaste fornewtype X = Y
.I do suggest following in Rust's conservative path of making everything private by default. Consider it from an API guarantee standpoint: if users are allowed to access the internal stuff of library code by default, they are of course free to build upon it, and so assume that it is guaranteed to be provided even in later versions. If privacy is opt-in, then you just add a lot of boilerplate for library authors and add cognitive load because they have to include it every single time. With privacy opt-out, you ensure that only the necessary stuff is made public and thus included under API guarantees.
Comment by ~araspik on ~icefox/garnet
Under that assumption, your code would have to work with a
static foreach
kind of construct, so that iteration over the enumeration variants is achieved. There (IMO) isn't a better way to express this.The two use cases you've highlighted need to be separated, as they're used in typically quite different contexts. I think Zig pulls this off nicely with
enum
and aunion
of anenum
, but the syntax needs to be re-evaluated because it involves quite a bit of boilerplate (in particular, all the variants need to be named twice). With Rust, I think a basic, consistent ABI for aggregate types should have been specified a long time ago, probably based on (or the same as) the SysV ABI - that would significantly aid inter-language operation. As a rule, the less implementation-defined stuff, the better.
Comment by ~araspik on ~icefox/garnet
One very cool idea I've found, inspired by this, is of having 'tied types', i.e. types whose each value is tied to some other value. For example, an
Index
structure may be tied toVec
s, so that every value of typeIndex
is 'tied' to a value of typeVec
. This allows not only for compile-time-proven elision of runtime checking, but also for implementing traits!Consider a structure
Sized
(which containssize: usize
, i.e. the size in bytes) tied to values of typetype
(i.e. types). You would have an instantiation ofSized
for e.g.i32
wheresize = 4
. This neatly provides for run-time and compile-time generics, as if you want information about a type (such as its size), you simply request an instantiation of a tied structure providing that information (i.e.Sized
) which is tied to that type. For example:struct Sized for type { size: usize }; fn alloc(T: type, sized: Sized for T) -> Box<T> { // Allocate space of size `sized.size` and return it wrapped in a `Box` }Such a function could be inlined at compile-time when the arguments are known, making it essentially just a typesafe
malloc
, or used at run-time with unknown types with the help of RTTI. It's just very neat, and it can be used to implement all the 'props' you've listed as simple zero-sized types. Also, you could provide multiple traits for different kinds of sizing (compile-time known, which includes the length as a field, run-time known, which includes a function to calculate it, and unknown, which has no extra info). The only reasonSized
doesn't provide the length in Rust is because that information is implicitly provided everywhere (e.g. with dyn objects it's stored as an additional field).There are some unresolved problems with tied types as traits, however: they allow for multiple different instantiations, and so may provide conflicting information (e.g. two different
Sized
values are constructed fori32
which saysize = 4
andsize = 2
) - this can be overcome with Rust's orphan rules, so that only the module whereSized
is defined can construct new values for it; they add some boilerplate in the source code; the compiler would need to track the tied value around to ensure that it still corresponds; and there are complex interactions with (im)mutably borrowing the tied value.Just some food for thought.
Comment by ~araspik on ~icefox/garnet
I think that the Rust way of implementing newtypes - as single-element tuples - neatly solves the problem of generating constructors and destructors as they are just the tuple pack/unpack operations.
However, the single-element tuple model is not intuitive. It may help to provide a keyword, such as
inner
, so thaty.inner
orFoo.Inner
(or whatever bikeshedding you want) are intuitive, instead ofFoo_unwrap
or the like which makes less sense. Constructing can be a simpleFoo()
function, although it may be easier to make that magic syntax (similar to e.g. tuple creation{a, b}
), because adding a new essentially stub function that could get output in your machine code is a waste.
Comment by ~araspik on ~icefox/garnet
The envisioned code you've provided here makes one fundamental assumption: that enumeration values are consecutive integers from 0 to something. I think that's why there's no direct shortcut available in Rust: that assumption can't be taken for granted, as you can specify custom values that may not be in order. Writing it out manually also enforces that you associate the right variant to the right function.
Another option you have is to use a compile-time for-loop over the available variants, deciding which function to use by indexing a compile-time list (or iterating over a zip pairing the variants to the functions, if that's your thing). In D, at least, this would be implemented with something like
static foreach
(although D doesn't have sum types).
Comment by ~araspik on ~sircmpwn/gmni
Huh, it's not segfaulting for me anymore.
But even then, we can support other schemes through Gemini. Perhaps for gmlnm it could be supported through a special visit-with-other-scheme command? For gmni it could just be an option.