Draco-lang / Language-suggestions

Collecting ideas for a new .NET language that could replace C#
75 stars 5 forks source link

[WIP] Properties #47

Open LPeter1997 opened 2 years ago

LPeter1997 commented 2 years ago

In this issue I'll discuss the basics of C# properties (for completeness sake) and present what different languages went with.

What are properties?

Properties are - in essence - syntax sugar for calling a function when retrieving a member with syntax obj.Member, and calling another function when assigning to that member with syntax obj.Member = value. The former is called a getter, and the latter one is called a setter. While the code they execute can be arbitrary, it usually makes sense that they manipulate the object state in some sensible manner.

For example, if a Person stores their birthday, an Age property could compute the age in years from the current time and the birthday, when using the getter, and recomputing the birthday, when using the setter.

It can also make sense to omit the setter (get-only properties) or the getter (set-only properties). Set-only properties are relatively rare, it's more common to omit the setter, which are sometimed called derived or computed values, and are a good tool for reducing data-dependency, while keeping the syntax natural to the "field-notation".

Important to remember: Properties are really just syntax sugar for method calls with two kinds of signatures. Even when they look like fields, they can be declared in interfaces/traits!

Properties in different languages

Below I'll go through how different languages integrated properties and discuss how we could adapt it to Fresh.

C

C# has the general syntax

Type PropertyName
{
    get { /* ... */ }
    set { /* ... */ }
}

The get block has to return a value with type Type, and the setter receives an implicit parameter called value with type Type. Onitting one will make the property get/set-only.

Going back to the birthday example, this could be:

class Person
{
    private DateTime birthday;
    public int Age
    {
        get
        {
            return (DateTime.Now - this.birthday).Years;
        }
        set
        {
            this.birthday = DateTime.Now.AddYears(-value);
        }
    }
}

C# evolved and simplified a lot on the general syntax over the years. For example, one-liners can be written with =>, just like arrow-methods:

public int Age
{
    get => (DateTime.Now - this.birthday).Years;
    set => this.birthday = DateTime.Now.AddYears(-value);
}

If the property is get-only, it can be written in an even shorter form:

public int Age => (DateTime.Now - this.birthday).Years;

C# also has so-called auto-properties, where the accessed and modified field automatically generated. Examples:

public int Foo { get; } // An int field is automatically generated and returned in the getter
public string Bar { get; set; } // A string field is automatically generated and returned in the getter, assigned in the setter

The latter is too different from simply writing a public field, but it can come from an interface signature - and a fields can't. C# generally prefers auto-properties instead of public fields, at least for classes.

For some special cases - mainly UI frameworks -, C# is planning to introduce the field keyword, that can be used in not-auto-implemented properties, and will result in a backing field automatically being generated, accessed with the field keyword (proposal). This proposal simply eliminates the need to write a backing field ourselves.

Personal thoughts

C# properties are nice, but a bit too "special" for what they are. The grouped and special-cased syntax is fine, but it has some pain-points:

It makes sense why they went with this, as this ensures that the property truly has a unified purpose and the two methods are sort of related (at least by the fact that there is only one documentation for the getter and setter).

Integrating something like this into the language would be possible with some minor modifications. As of right now, the difference between static and nonstatic functions is that nonstatic functions take this as their first parameter. We'd either still have to introduce some keyword to annotate staticness, or modify the syntax slightly to include the parameters.

Python

Python properties are really close to their roots. They are simply methods annotated with an attribute, marking them as properties (and the attribute is a Python decorator, that is discussed already in the metaprogramming issue):

class Person:
    def __init__(self, name, birthday):
        self.name = name
        self.birthday = birthday

    # This is the getter
    @property
    def age(self):
        return (date.today() - self.birthday).year

    # This is the setter
    @age.setter
    def age(self, a):
        # I don't know the Python date lib, I made this up
        this.birthday = date.today() - date(a, 0, 0)

Personal thoughts

I believe this is very simple and elegant. Properties are nothing more, than markers on methods, and since Python methods are relatively lightweight, it's not a hassle to type it out. One huge advantage of this syntax is that everything for methods automatically becomes usable for properties too, since they are not a separate language element, but rather metadata.

Integrating something like this into the language would be pretty trivial without any nontrivial syntax modifications/considerations.

D

Properties in D before version 2 are as bare-bones as they can get. They are just getter and setter methods that you can simply use as properties.

class Person {
    // I don't know the D standard lib, made up something

    private DateTime birthday;

    public int age() {
        return (DateTime.today() - this.birthday).years();
    }

    public void age(int v) {
        // ...
    }
}

Properties after D version 2 have to be marked with an attribute:

class Person {
    // I don't know the D standard lib, made up something

    private DateTime birthday;

    @property public int age() { ... }
    @property public void age(int v) { ... }
}

Personal thoughts

The pre-version 2 alternative is a bit too implicit and not very clear. The second version is virtually identical to the Python one, all advantages apply. Both versions would be relatively easy to include in the language.

Delphi

Delphi separates the declaration of the property and the methods that are executed for the getter and setter. A property simply declares the getter and setter functions, which are written externally.

type Person = class
// Again, sorry, I don't know Delphi standard lib
private
    birthday: Date;

    function GetAge(): Integer;
    procedure SetAge(const v: Integer);

public
    property Age: Integer read GetAge write SetAge;
end;

// TODO: Implement GetAge and SetAge

Personal thoughts

A bit too verbose, but neatly connect properties back to just separate functions/methods. The advantage is that the functions are usable by themselves, they can be passed around and such.

Some syntax would have to be made up, but otherwise it's pretty simply implementable.

F

Unsurprisingly, F# ML-ifies the property syntax of C#:

type Person() = class
    // ...

    member this.Age
        with get() = (DateTime.Today - this.birthday).Years
        and set value = this.birthday = DateTime.Now.AddYears(-value);
end

Personal thoughts

By now you know how I feel about ML-syntax, but this is a relatively positive surprise. The setter has an explicit value argument and they are both method-like declarations in their shape. Something like this could be easily added to the language.

Proposed syntaxes

Below I'll propose some possible syntaxes for the language without any additional thoughts (those are already expressed in the lang-specific cases).

Python-style

impl Person {
    #[getter]
    func age(this): int = ...;

    #[setter]
    func age(this, v: int) = ...;
}

Functions with a different keyword (D-style)

impl Person {
    prop age(this): int = ...;
    prop age(this, v: int) = ...;
}

F#-style V1

impl Person {
    prop Age: int
    {
        get(this) = ...;
        set(this, v: int) = ...;
    }

    // Getter-only
    prop Age(this): int = ...;

    // Auto-prop
    prop Age: int { get(this); set(this); }
}

F#-style V2

(Placement of this changes)

impl Person {
    prop Age(this): int
    {
        get() = ...;
        set(v: int) = ...;
    }

    // Getter-only
    prop Age(this): int = ...;

    // Auto-prop
    prop Age(this): int { get; set; }
}

Delphi-style

impl Person {
    prop Age(get: get_age, set: set_age): int;

    // TODO: Implement get_age and set_age
}
333fred commented 2 years ago

Don't forget another powerful feature of properties: their usefulness in describing the values of a type. This can then be used in pattern matching via property patterns, allowing very easy matching against the values of a type. If I to choose between your proposed syntaxes, I'd go for something in the F# vein, though having a this parameter at all is weird to me. Will all instance methods need to explicitly declare that parameter? Will I need to call it in that form at the use site?

LPeter1997 commented 2 years ago

@333fred About this, the issue about records mentions everything I believe, but the short version is: Yes, all instance methods will explicitly take this and yes, you access all instance elements through it, similarly to Rust or Python.

Thanks for the suggestion about the syntax!

Jure-BB commented 2 years ago

What I miss in C# is an ability to pass some kind of reference to a property that allows receiver to access property's getter and setter methods and to capture that reference in it's own field.

Use case example would be writing a generic AnimationController<T> class/struct that could animate a value of any property of type T of any object.

WhiteBlackGoose commented 2 years ago

I wonder if we could do it with more convenient reflection. Like methodof(MyClass.SomeMethod) or propertyof(MyClass.SomeProperty), similar to typeof and nameof. E. g.

class MyClass
{
    public int Property => 5;
}

PropertyInfo pi = propertyof(MyClass.Property);
LPeter1997 commented 2 years ago

Huh, it looks like lenses pop up all around in practice as a need.

I'd say special-casing reflection for something like this is odd and would be relatively slow, might not be ideal to drive something like an animation system. Being able to extract the getter and setter as its own entity would not be so bad I believe. Our standard library could ship something like:

trait Property[T] {
    func get(this): T;
    func set(this, value: T);
}

And then give the ability to construct this type in a simple manner from properties.

Some of the proposed variants make this trivial, when the getters and setters are already function-like constructs explicitly, but the more hidden ones could also get some specialized syntax for this. For example, set(Prop) could access the setter function of a property.

Binto86 commented 2 years ago

From the proposed syntax i like the F# way, but i am not sure if this should be there or not

WhiteBlackGoose commented 2 years ago

It feels redundant, but it is also consistent from the PoV of variable binding. In C# a variable could be either local or field. Here, besides those two options, there's a third one: captured variable from primary constructor. Which makes it a bit more ambiguous