ufcpp-live / UfcppLiveAgenda

@ufcpp live streaming agenda
MIT License
24 stars 2 forks source link

久しぶりにDesign Notes話する #22

Closed ufcpp closed 3 years ago

ufcpp commented 3 years ago

https://youtu.be/XMp-cf0_5vA

(1) Require Property 改め、Nominal Parameter Lists

https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-07.md

class Laptop
{
    float ScreenSize { get; set; }
    float ProcessorFrequency { get; set; }

    public Laptop() init { ScreenSize, ProcessorFrequency }
    {}
}

↑みたいな書き方で、「このコンストラクターは ScreenSizeProcessorFrequency プロパティを必ず初期化します」を表明する案はどうか?という話。

コンストラクターに限らず init method (プロパティ/フィールドの初期化を保証するメソッド)とか作るとして「どのメソッドがどのプロパティの初期化を保証します」みたいな情報はメソッド境界で明示させたい。 中身まで追うのはパフォーマンス的リスク高い(停止性問題に引っかかる)。 ただ、今回出た文法案は記述量がだいぶ増えるし、MemberNotNull でやってる解析と重複する問題あり。

(2) List Patterns

https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-14.md https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-16.md

あくまで「初期化とパターンマッチの対称性」にはこだわりたい。 List に対するパターンとして [] を使う案があったけど、初期化が new[] { ... } なのにパターンに [] は使いたくない。 [] はインデックスにマッチさせて、[index] { element, ... } なパターンにしたい。 [] 単体で「空リストにマッチ」にするかどうかとか、IEnumerable にもこのパターン使えるかとかみたいな問題あり。

(3) Definite assingment changes

https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-16.md

if (c != null && c.M(out object obj0))
{
    obj0.ToString(); // ok
}

↑これが OK なのに

if (c?.M(out object obj3) == true)
{
    obj3.ToString(); // undesired error
}

↑これがダメなの、変じゃない? という話、前々から出てたけど最近実装に乗り気になったっぽい。

ufcpp-live commented 3 years ago

本題である Nominal Parameter Lists、実は今すでにある MemberNotNull 解析が近いことをしてる。

で、MemberNotNull、not null を名乗りつつ、値型に対してもフロー解析が働いている雰囲気。

#nullable enable

using System.Diagnostics.CodeAnalysis;

var ok1 = new InitRequired { A = "a", B = "b" };
var ok2 = new InitRequired("a") { B = "b" };
var ng1 = new InitRequired { A = "a" }; // B 未初期化
var ng2 = new InitRequired("a");     // B 未初期化

class InitRequired
{
    public string A { get; set; }
    public string B { get; set; }
    public int PositiveNumber { get; set; }

    public InitRequired() //init { PositiveNumber }
    {
        Init();
    }

    [MemberNotNull(nameof(A), nameof(B), nameof(PositiveNumber))]
    private void Init()
    {
        //A = null!;
        A = "aaa";
        B = "bbb";
        //PositiveNumber = 0;
    }

    public InitRequired(string a) { A = a; }
}

image

ufcpp-live commented 3 years ago

Nominal Parameter Lists の現在提案されてる構文。 冗長じゃない?とか MemberNotNull との被り気になるとか課題はあり…

class InitRequired
{
    public string A { get; init; }
    public string B { get; init; }
    public int PositiveNumber { get; init; }

    public InitRequired() init { A, B, PositiveNumber }
    {
        Init();
    }

    // init が入ったら MemberNotNull 要らなくなるんじゃない?
    [MemberNotNull(nameof(A), nameof(B), nameof(PositiveNumber))]
    private void Init() init { A, B, PositiveNumber }
    {
        A = "aaa";
        B = "bbb";
        PositiveNumber = 0;
    }

    public InitRequired(string a) init { B, PositiveNumber } 
    {
        A = a;
    }
}
ufcpp-live commented 3 years ago

プライマリコンストラクター + init で型定義がすごいことになりそう… インターフェイスとか where とかがあるので今更ではあるものの。

record InitRequired<T>
    : IDisposable,
    IEquatable<T>,
    IComparable<T>
    where T : class
    init
    {
        RequiredStringA,
        RequiredStringB
     } // ここの }{ の段差許せる???
{
    public string RequiredStringA { get; init; }
    public string RequiredStringB { get; init; }
}
ufcpp-live commented 3 years ago

インターフェイスというと、インターフェイスって線形探索だから利用頻度が高いものほど前に書いた方が速いという話が。

C c = new();

// せいぜい1桁程度のインターフェイスでも差が出るらしい
if (c is I1) { } // 線形探索だから is I1 は速い
if (c is I8) { } // 線形探索だから is I8 は遅い
interface I1 { }
interface I2 { }
interface I3 { }
interface I4 { }
interface I5 { }
interface I6 { }
interface I7 { }
interface I8 { }

class C : I1, I2, I3, I4, I5, I6, I7, I8 { }

// public class List<T> : IList<T>, IList, IReadOnlyList<T>
// ↑たぶんこの IList (non-generic) が2個目になってるのこれのせい
ufcpp-live commented 3 years ago

「初期化子で必ず代入させたい」って言いだすと、メンバー追加が破壊的変更に。

// あとつけできるのが nominal(名前指定)初期化のいいところの1つだったけど…
// この init の仕様が入ると nominal でもあとつけしにくい
var ok1 = new InitRequired { A = "a" }; // 後から警告増えるべき?

record InitRequired init { A /* , B を足すのは警告増えて破壊的変更 */ }
{
    public string A { get; init; }

    // B は後から足したものとする
    public string B { get; init; }

    // 後から足すものには ? をつけろ説はある
}
ufcpp-live commented 3 years ago

init 指定があるもの(init { A, B }) を呼びつつ、「A, B は自分が初期化するから大丈夫」みたいな指定も必要で、init { A = _, B = _ } みたいな案が出てる。

// クラスにもプライマリコンストラクターが入ったとして
class C() init { A, B }
{
    public string A { get; init; }
    public string B { get; init; }

    public C(string a, string b) : this() init { A = _, B = _ }
    {
        A = a;
        B = b;
    }
}
ufcpp-live commented 3 years ago

List Patterns あれこれ。

#nullable enable
using System;
using System.Collections.Generic;

// C# チーム的には生成と分解が対であってほしい
var t = (x: 1, y: 2);
if(t is (x: var x1, y: var y1))
{
}

var a = new { x = 1, y = 2 };
if (a is { x: var x2, y: var y2 })
{
}

var x = new int[] { 1, 2, 3 };

var y = new List<int>
{
    Capacity = 5,
    [0] = 1,
    [1] = 2,
    [2] = 3,
};

// コミュニティ実装でこれが動いてるコードがあったりはする
// 生成が {} なのにパターンが [] は変だ
if (x is [1, 2, 3])
{
}

// まず [] だけのパターン
// length/count に対するパターンになるのまでは確実。でも…
if (x is [3]) ; // x.Count is 3

// [] はどういう意味にしよう案1
if (x is []) ; // empty?
if (x is [_]) ; // any?

// [] はどういう意味にしよう案2
if (x is []) ; // any?
if (x is [0]) ; // empty?

// [] {} パターン
if (x is [] { 1, 2, 3 }) ; // x[0] is 1 and ...

if (x is [] { head, { /* tail? */ } }) ;

// [] {} {} パターン???
// 1個目の {} はプロパティ、2個目の {} はリスト
if (x is [] { Count: 3 } { 1, 2, 3 }) ;

// 好印象な提案
if (x is { [0]: var head, [1..]: var tail }) ;
if (x is { [0]: var first, [^1]: var last }) ;
if (x is { [0]: 0 }); // x[0] is 0
if (x is { Count: 3, [0]: var head, [1..]: var tail }) ;
ufcpp-live commented 3 years ago

List Decomposition の話も上がったけども… まあ、パターンが先。 というか、であれば、プロパティパターン相当の分解もあっていいはず(これはこれで検討されてる)。

var t = (1, 2);

var (x, y) = t;

// List Patterns があるとして、List Decomposition は?

// たぶん、List より先にプロパティ decomposition。
_ = t is { Item1: var x1, Item2: var y1 }; // これはいけるわけで
// これもあってよくない?
var { Item1: x2, Item2: y2 } = t;
ufcpp-live commented 3 years ago

definite assignment あれこれ。 「言われてみれば」っていう感じ。 最初の報告( ?: のやつ、できないのはバグじゃない?って言われてた)、2015年だった。

#nullable enable

using System.Diagnostics.CodeAnalysis;

C c = new C();
if (c.M(out object? obj0))
{
    obj0.ToString(); // ok
}

if (c.M(out object? obj4) is true) // == true だと大丈夫だった!
{
    obj4.ToString(); // 警告。 ok でよくない?
}

// まあ、いつやるかだけの問題
if (c != null ? c.M(out object? obj1) : false)
if (c?.M(out object? obj2) ?? false)
if (c?.M(out object? obj3) is true)
{
    obj1.ToString(); // ok でもよくない?
    obj2.ToString(); // ok でもよくない?
    obj3.ToString(); // ok でもよくない?
}

public class C
{
    public bool M([NotNullWhen(true)]out object? obj)
    {
        obj = new object();
        return true;
    }
}