Skip to content

Instantly share code, notes, and snippets.

@RikkiGibson
Last active February 1, 2021 21:37
Show Gist options
  • Save RikkiGibson/9267a57189c9e0388843a39520418115 to your computer and use it in GitHub Desktop.
Save RikkiGibson/9267a57189c9e0388843a39520418115 to your computer and use it in GitHub Desktop.
Design notes on "required properties" from Feb 1st 2021

inits() vs other keywords

In the 28th January meeting we decided the inits() syntax is the one we're happiest with so far. There are likely multiple areas of concern about it, but one noteworthy concern is about the keyword that begins the "inits" clause.

A few reasons for concern about the keyword "inits":

  • inits is VB-ish, in the way that VB says Overrides while C# says override.
  • inits strongly resembles init, but init has an unrelated meaning, which we may want to introduce independently to method signatures. (The proposed init modifier on a method enables it to call init accessors, while making the method only callable during construction.)

The biggest reason I can think of to separate inits from init is the following: A "helper" should not be limited to only being called during construction. If object instances are sometimes allocated from scratch and used, and other times are fetched from a pool and used, it may be desirable to call a helper both during and after construction.

Using either init() or set()

Consider if we just used the init keyword for this purpose, and for helpers either the init or set keywords, depending on whether the helper needs to call init accessors.

public class Widget
{
    public required string Name { get; set; }
    public required int Id { get; init; }

    public Widget(string name, int id)
        // perhaps 'set()' would be disallowed on constructors because a constructor can only be called "during construction" anyway.
        : init(Name = name, Id = id)
    {
    }

    public Widget(string name)
        : init(Name, Id)
    {
        SetName(name);
        InitId();
    }

    // this "helper" can only be called during construction, and would likely be restricted to at most 'protected' accessibility.
    protected void InitId()
        : init(Id = 42)
    {
    }

    // this "helper" can be called both before or after construction.
    public void SetName(string name)
        : set(Name = name)
    {
    }

    // - empty 'init' clause permits us to set 'init' properties without requiring us to set any in particular.
    protected void MaybeInitId()
        : init() // perhaps optionally elide the parentheses? or even allow it to be used as a modifier instead of as a clause.
    {
        Id = 42;
    }
}

A consequence of this is that it pushes users to use the init "clause" syntax even when they aren't interested in specifying which properties they are going to init. But it's also a little more "symmetrical" with the accessor declarations which treat "set" and "init" as mutually exclusive.

Using set() for the clause and init as a modifier

Alternatively: init could be a standalone modifier on method signatures, and set() could be the "only" way to specify a "set clause".

public class Widget
{
    public required string Name { get; set; }
    public required int Id { get; init; }

    public Widget(string name)
        : set(Name = name, Id)
    {
        InitId();
    }

    protected init void InitId()
        : set(Id = 42)
    {
    }

    protected init void MaybeInitId()
    {
        Id = 42;
    }
}

I think I prefer the init()/set() scheme in the previous section, perhaps because

  1. set indicating that the method calls an init accessor "feels weird", versus init indicating that a method calls a set accessor.
  2. During construction we colloquially refer to to the process of "initializing" an object's properties in construction, not "setting" them
  3. set and init coexisting in a signature "feels weird"

Delimiter between base/this and init

I also wanted to reiterate my support for the idea of grouping the set clause as "part of" the base clause, a la Widget() : base(), set(Name) { }.

  • It is syntactically consistent with base clauses on types.
  • There are some concerns about the likely ordering requirement where a set clause must come after a base/this when both are present, and the aesthetics of comma-separating. However, this also makes us more consistent with types in a way, because in a type's base clause, the base type is required to appear before any interface types. This also strengthens the mnemonic of how we decide which properties are required by the caller: first we look at the base/this constructor call, then we take away anything set by this constructor. I'm open to other delimiters between base/this and set, though.

How to do "init all"

To support the concept of "init all", we could use the syntax init(..). This introduces a requirement that a constructor definitely assigns all the "remaining" required properties for the constructor. When used on a method, it would probably mean that the method must set all the required properties on the type--although can be adjusted as we start prototyping the feature.

Using the .. token evokes the idea of "do it to all of them", a la the expression arr[..] which produces a slice of an entire collection.

public class Widget
{
    public required string Name { get; set; }
    public required int Id { get; init; }

    public Widget(string name)
        : init(..)
    {
        Name = name;
        Id = 42;
    }
}

Factories

It feels like we could include a "modifier" on the init()/set() clause to indicate that it is applied to the return value, not to the this parameter:

public class WidgetFactory
{
    // factory sets some of the widget properties. If the widget has more properties then the user needs to set them.
    public Widget CreateWidget()
        : return init(Name, Id)
    {
        return new Widget { Name = "Fred", Id = 42 };
    }
    
    // for a factory which doesn't initialize anything, we use an "empty" init clause.
    public Widget CreateUninitializedWidget()
        : return init()
    {
        return new Widget { Name = "Fred", Id = 42 };
    }

Alternatively, a modifier on the method could indicate the same thing.

    // factory sets some of the widget properties. If the widget has more properties then the user needs to set them.
    public factory Widget CreateWidget()
        : init(Name, Id)
    {
        return new Widget { Name = "Fred", Id = 42 };
    }

    // factory doesn't initialize the required properties of 'Widget' on the return value. The user needs to do that.
    public factory Widget CreateWidget()
    {
        return new Widget();
    }

    // factory sets *all* Widget properties on return value. It feels like this is oxymoronic--just make it a regular method in this case.
    public factory Widget CreateWidget()
        : init(..)
    {
        return new Widget { Name = "Fred", Id = 42 };
    }
}

I think I personally prefer the return init() but do not have a strong preference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment