Pre-RFC user-defined types #126
Labels
Design document
This one came out from an idea but considers many cases and tries to prove the usabity
Language idea
One single idea which may be a start of a design document
Syntax
This issue is about syntax
Type system
This issue is about type system
User-defined types
This document attempts to describe the datatypes that can be declared in Draco. We will attempt to summarize all "class-related features" of C#, our equivalents and how we would be able to interop with each of them, given that C# interop is an important aspect for us.
Prior issues:
C# features to cover
Here is a loose list of C# features we are trying to cover that is needed for interop, or we intend to carry on because of its value regardless of the necessity (or I just mentioned for consideration). If there is anything missing, please notify under this proposal.
Data types:
class
declarationsstruct
declarationsenum
declarationsrecord class
declarationsrecord struct
declarationsinterface
declarationsData type modifiers:
sealed
class modifierpartial
class/struct modifierabstract
class modifierreadonly
struct modifierref
struct modifierMembers:
Member modifiers:
static
member modifierreadonly
field modifierrequired
property modifierabstract
property/method modifierabstract override
property/method modifiervirtual
property/method modifieroverride
property/method modifierOur philosophy
I'd like to take the approach of the ML family of languages here and introduce two main categories of user defined types:
All C# types, except enums (and interfaces) fit into product-types, enums can be considered as a very limited version of sum-types.
There are certain things we can already sort of decide based on our prior philosophy:
sealed
by default. Openness for extension should be explicitly allowed by the user, annotating that the class is intended to be reused through inheritance.var
andval
differentiated, this is a good basis for when we want to consider field modifiers likereadonly
, or get-only/get-set auto-properties.impl
blocks. This likely makes thepartial
modifier obsolete. Note, that the feature will likely introduce additional design considerations when considering this extension mechanism across assemblies.Interfaces
I believe one of the simplest places to tackle is interfaces, since there has been a lot of implications about taking heavy inspiration from Rust traits. For now I'd like to propose to leave out associated types intentionally until we have generic constraints fleshed out. That leaves the following features (straight from the traits issue):
static abstract
s in C#)An example for all features:
Important details to note:
This
is an alias within the trait to reference the type implementing this traitthis
parameterthis
parameter, implicitly typedThis
This
, though this could be made implicitthis
, no implicit context like in C# or JavaImplementing a trait happens in a separate syntactical construct from the type. It does not even have to be in the same assembly, but either the trait, or the implementing type has to be defined in the assembly providing the implementation. Example:
Trait inheritance
Traits can declare that the implementing type must also implement certain other traits:
Implicit vs explicit implementations, defaulting
C# allows for implementing methods of a trait implicitly or exmplicitly. Furthermore, default members are implemented explicitly, making them painful to utilize well. Note that there is a reason C# does this, to avoid the diamond-inheritance problem:
In our case, we can use the same disambiguation that Rust uses. Since each trait implementation is explicit to its own block, we can disambiguate by calling the defaulted/ambiguous member as a static method of the trait:
I1.Foo(default(T))
orI2.Foo(default(T))
.Product types (classes, records, structs)
The usual, general-purpose class declaration is provided under the
class
keyword. The only syntactic change is that the class is only allowed to declare instance state in its declaration, which means auto-properties (properties automatically backed by fields) and fields. Example:Some things to note:
Associating behavior to this class would look like so:
For value-types (structs), the class can be prefixed with the
value
keyword:Tuple-classes
From
record
types in C#, it is obvious that a more lightweight datatype is needed for cases, where all of the state is public, and a constructor would be automatically provided to initialize this state. While the C#record
is a little more opinionated in terms of design (as it includes aToString
and equality implementation by default), this basic concept would be worthy enough to add sugar to classes to make POCOs easier to declare. The rest of therecord
features could be achieved through decorators, which are planned as a more generic solution to implement boilerplate code.Example:
The
value
andopen
modifiers ase also valid for this construct. TheColor
constructor in this case is always public.Inheritance
While inheritance is heavily discouraged, it is unavoidable because of interop with the existing ecosystem. The proposed syntax for inheritance looks exactly like how it looks in C#:
While this might seem like a weird divergence from the commitment to the external trait implementation style, it is necessary. There can only be a single base class, and the base has to be known in the assembly declaring the type.
Draco classes are
sealed
by default, unlike C# classes, which are open to inheritance by default. Draco classes can be made open for inheritance using theopen
modifier:Constructors
We'd like to push logic out from constructors. If there's significant logic involved with a types' construction, one should write a factory function. The construction logic itself should be fairly minimal. Initializer lists sort of facilitate a minimalistic construction, where the sole logic involved is copying some consistent initial state into the new instances' members. For example, constructing the above
Foo
type would look like so:The visibility of this constructor/initializer is always private, meaning that factory methods should be exposed for constructing the type from the outside world.
Calling base constructors
Calling the base class constructor can be done by assigning the special member
base
in the initializer list with one of the base constructor methods:TODO: This implies, that one could write something like:
Which might be something we don't want to, or simply can't support.
Abstract classes
Abstract classes are not central to Draco (because of the flexibility of traits), so their design solely serves interop with C#. An abstract class can be generated from a trait using an attribute:
Which generates the following class in metadata:
Sum types (DUs, enums)
Discriminated unions take heavy inspiration from Rust enums. In general, they take the following shape:
The members within the enum declaration directly follow the same logic as classes, only instance state is declared. Note, that this declaration is a reference type, unlike enums in C# by default (see the C# equivalent later).
Behavior can be associated the same way, as for classes:
Value-type enums can be declared the same way as classes, by prefixing with
value
:Variant tag, backing type
The "tag-value" (the field marking what number is assocaiated with the alternative) can be specified with
= constant
, like in C#:The backing tag field type can be changed in the declaration itself using the inheritance syntax:
C# compatibility
C# enums can be declared by defining a
value enum
, where no members are compound types. Example:The equivalent C# would be:
Properties
Auto-properties
Auto-properties take two forms,
var Name: Type
for get-set, andval Name: Type
for get-only properties:Custom properties
Static properties take the form (within
implement
blocks):The getter needs to return
int32
, and the setter implicitly receives avalue: int32
parameter, similarly to C#.The usual
= expr
sugar is available for both the getter and setter blocks:Nonstatic properties only differ in receiving the
this
parameter in the property signature:Computed get-only properties have sugar in the following form:
In interfaces, properties often need to annotate what accessors they have without computing anything, for this the
get
/set
accessors are not followed by expression or block, but only by;
:Visibility
The visibility of properties is a bit more complicated topic, because sometimes the accessors need to have different visibilities declared. C# solves this by allowing to declare a visibility for the properties, and then restrict the individual accessors.
For the sake of simplicity - and to avoid introducing an explicit
private
visibility modifier when the rest of the design is built around implicit private, I'd like to propose the following:While the latter would be enough to cover all cases, having properties with the same visibility for all its accessors is common enough to justify the shortcut. Specifying visibility for both the property and the accessors is illegal.
Some examples:
val X: int32;
: private get, no setvar X: int32;
: private get, private setpublic var X: int32;
: public get, public setprop X(this): int32 { get = ...; set = ...; }
: private get, private setpublic prop X(this): int32 { get = ...; set = ...; }
: public get, public setprop X(this): int32 { public get = ...; set = ...; }
: public get, private setpublic prop X(this): int32 { public get = ...; set = ...; }
: illegalStatic state
Non-member, static data can be associated to types in the
implement
blocks. For example, adding a static instance counter field to the typeFoo
:Operators
Since .NET 7, a bunch of interfaces have been introduced to model operators inside System.Numerics. I'd like to propose that instead of custom syntax, we'd treat operators as trait implementations:
operator +
:IAdditionOperators
operator -
:ISubrtactionOperators
An example:
Indexers
Indexers are a little special in a way, they are modeled as parameterized properties in the .NET world. In itself that would not be a problem, interfaces/traits can declare properties. The problematic part here is twofold:
Because of that, I propose that indexers should stay regular properties. Since our property syntax already looks like functions, we can easily add parameters. For indexers especially, we could reserve a special name, like
index
. Example:What to do with
mod
andrem
TODO
Abstract, virtual, override member modifiers
Abstract member modifier
TODO
Virtual member modifier
Virtual members can use the
open
modifier. Similarly, howopen
opens up a type for extension, it also does the same for the marked members (properties and functions). The modifier is only valid on instance (nonstatic) members.Examples:
Override member modifier
For virtual and abstract members
override
becomes a valid modifier for prividing an overriding implementation in derived types. Example:Note, that
overriding
members aresealed
by default, unless they are explicitly keptopen
:Not yet considered
A section to list what this proposal hasn't considered yet (partially copied from the section listing the C# elements).
Data type modifiers:
readonly
struct modifierref
struct modifierMembers:
abstract
property/method modifiermod
andrem
operatorsMember modifiers:
Elaborate examples
TODO
The text was updated successfully, but these errors were encountered: