IISResetMe / PSType-vNext

Dummy project to keep track of proposed changes to PowerShell's type definition facility
MIT License
4 stars 0 forks source link

Investigate and prototype new type emission templates (struct, record) #9

Open IISResetMe opened 2 years ago

IISResetMe commented 2 years ago

This task is to explore, design and prototype proposals for:

Value types

PowerShell's type definer only supports emission of reference types:

These constraints limit the usefulness of PSType definitions in interop scenarios.

The usefulness of value types do have some detractors in the context of PowerShell, since ETS relies on identity (ie. reference equality) for PSObject resurrection, but since value type creation is trivial to implement both syntax and compilation for (it's basically a subset of existing functionality with a few additional .ctor constraints), this topic should definitely be explored further.

Record types

Record types, introduced in C# 9, are a syntax feature that causes the compiler to generate default behaviors that make instances behave like value types even though they're not.

The following type definition:

public record MyRecord(int A, int B);

will cause generation of IL closer resembling:

public class MyRecord : IEquatable<MyRecord>
{
    protected virtual Type EqualityContract
    {
        get
        {
            return typeof(MyRecord);
        }
    }

    public int A
    {
        get;
        set;
    }

    public int B
    {
        get;
        set;
    }

    public MyRecord(int A, int B)
    {
        this.A = A;
        this.B = B;
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append(nameof(MyRecord));
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(' ');
        }
        stringBuilder.Append('}');
        return stringBuilder.ToString();
    }

    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("A = ");
        builder.Append(A.ToString());
        builder.Append(", B = ");
        builder.Append(B.ToString());
        return true;
    }

    public static bool operator !=(MyRecord left, MyRecord right)
    {
        return !(left == right);
    }

    public static bool operator ==(MyRecord left, MyRecord right)
    {
        return (object)left == right || (left?.Equals(right) ?? false);
    }

    public override int GetHashCode()
    {
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(A)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(B);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as MyRecord1);
    }

    public virtual bool Equals(MyRecord1 other)
    {
        return (object)this == other || ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(A, other.A) && EqualityComparer<int>.Default.Equals(B, other.B));
    }

    public virtual MyRecord <Clone>$()
    {
        return new MyRecord(this);
    }

    protected MyRecord(MyRecord original)
    {
        A = original.A;
        B = original.B;
    }

    public void Deconstruct(out int A, out int B)
    {
        A = this.A;
        B = this.B;
    }
}

This means you get by-val like equality comparison but retain identity on assignment:

$a = [MyRecord]::new(1, 2)
$b = [MyRecord]::new(1, 2)
$c = $b

$a -eq $b # $true
[MyRecord]::ReferenceEquals($a, $b) # $false
[MyRecord]::ReferenceEquals($b, $c) # $true

We don't necessarily need to cargo-cult this exact set of behaviors, but the default implementation of IEquatable<T> on reference types makes a lot of sense for data structures in PowerShell.


Outstanding items:

JustinGrote commented 2 years ago

I think the key aspects of a powershell Record that should be preserved from C# should be:

  1. Value-type like equality (walks like a duck, talks like a duck)
  2. Friendly ToString output, maybe not exactly what C# outputs but something PS users would be comfortable with, similar to PSCustomObject output maybe.
  3. Deep Copy by default, or at least something similar to "with" that is like Add-Member but creates a new object.
  4. Easily Serializable by default, maybe even forbid methods that would make it a "live" object
  5. Support validation attributes for members

These are the features I think would make it most useful, give Powershell users a model for data that is easily validatable and transportable between processes.