ідея відповідності перемикача / зразка


151

Я недавно дивився на F #, і хоча я швидше за все не стрибну огорожу, але він безумовно виділяє деякі області, де C # (або підтримка бібліотеки) може полегшити життя.

Зокрема, я думаю про можливість узгодження шаблону F #, що дозволяє отримати дуже багатий синтаксис - набагато виразніше, ніж поточний перемикач / умовні еквіваленти C #. Я не буду намагатися наводити прямий приклад (мій F # не до цього), але коротше це дозволяє:

  • збігаються за типом (з повною перевіркою на дискриміновані союзи) [зауважте, це також підводить тип для пов'язаної змінної, надаючи доступ членам тощо]
  • відповідати присудком
  • комбінації вищевказаних (і, можливо, деяких інших сценаріїв, про які я не знаю)

Хоча C # було б прекрасно врешті-решт запозичити [ах] частину цього багатства, я тим часом переглядав, що можна зробити під час виконання - наприклад, досить легко збити деякі об'єкти, щоб дозволити:

var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

де getRentPrice - функція <транспортний засіб, int>.

[зауважте - можливо, Switch / Case тут неправильні терміни ... але це показує ідею]

Для мене це набагато зрозуміліше, ніж еквівалент із використанням повторного if / else, або складеного потрійного умовного (який стає дуже безладним для нетривіальних виразів - дужки в галас). Це також дозволяє уникнути багато кастингу та дозволяє просте розширення (безпосередньо або через методи розширення) на більш конкретні збіги, наприклад, відповідність InRange (...), порівнянна з VB Select ... Case "x To y "використання.

Я просто намагаюся оцінити, чи люди думають, що від подібних конструкцій (за відсутності мовної підтримки) багато користі?

Зауважте також, що я грав з трьома варіантами вищезазначеного:

  • версія Func <TSource, TValue> для оцінки - порівнянна зі складеними потрійними умовними твердженнями
  • версія Action <TSource> - порівнянна з if / else if / else if / else if / else
  • версія Expression <Func <TSource, TValue >> - як перша, але використана довільними постачальниками LINQ

Крім того, використання версії на основі Expression дозволяє переписати дерево Expression, по суті, вклавши всі гілки в єдиний складений умовний вираз, а не використовувати повторний виклик. Я нещодавно не перевіряв, але в деяких ранніх побудовах Entity Framework я, мабуть, згадую, що це потрібно, оскільки це не дуже сподобалось InvocationExpression. Це також дозволяє більш ефективно використовувати LINQ-до-об'єктів, оскільки це дозволяє уникнути повторних викликів делегувати - тести показують відповідність, як описано вище (використовуючи форму вираження), виконуючись з однаковою швидкістю [фактично швидше, насправді] порівняно з еквівалентною C # складений умовний вислів. Для повноти, заснована на Func <...> версія займала 4 рази довше, ніж умовна заява C #, але все ще дуже швидка і навряд чи буде головним вузьким місцем у більшості випадків використання.

Я вітаю будь-які думки / введення / критику / тощо щодо вищесказаного (або щодо можливостей багатшої підтримки мови # C ... ось сподіваюсь ;-p).


"Я просто намагаюся оцінити, чи люди думають, що від таких конструкцій (за відсутності мовної підтримки) багато користі?" ІМХО, так. Хіба щось подібне вже не існує? Якщо ні, то закликайте написати полегшену бібліотеку.
Конрад Рудольф

10
Ви можете використовувати VB .NET, який підтримує це у своєму операторі select select. Еек!
Джим Бергер

Я також зачепив власний ріг і додаю посилання на свою бібліотеку: funkcional-dotnet
Олексій Романов

1
Мені подобається ця ідея, і вона робить дуже приємною і набагато гнучкішою формою перемикача; однак, чи це насправді не прикрашений спосіб використання синтаксису, подібного до Linq, як обгортки як тоді? Я б заважав комусь використовувати це замість реальної угоди, тобто switch-caseзаяви. Не зрозумійте мене неправильно, я думаю, що в ньому є місце, і я, мабуть, шукатиму спосіб здійснити.
IАнотація

2
Незважаючи на те, що цьому питанню вже більше двох років, все ж доречно згадати, що C # 7 виходить незабаром (ish) з можливостями відповідності шаблону.
Авіон47

Відповіді:


22

Я знаю, що це стара тема, але в c # 7 ви можете:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Тут помітна різниця між C # і F # - повнота відповідності шаблону. Щоб відповідність шаблону охоплювала всі можливі наявні випадки, повністю описані, попередження від компілятора, якщо ви цього не зробите. Хоча ви з правом можете стверджувати, що випадок за замовчуванням це робить, це також часто є на практиці винятком під час виконання.
VoronoiPotato

37

Спробувавши зробити такі "функціональні" речі в C # (і навіть спробувати книгу на ньому), я дійшов висновку, що ні, за кількома винятками, такі речі не надто допомагають.

Основна причина полягає в тому, що такі мови, як F #, отримують багато своїх сил, істинно підтримуючи ці функції. Не "ти можеш це зробити", а "це просто, зрозуміло, очікується".

Наприклад, при зіставленні шаблонів ви отримуєте компілятор, який повідомляє вам про те, чи є неповна відповідність чи коли інша відповідність ніколи не потраплятиме. Це менш корисно для відкритих типів, але при поєднанні дискримінованого союзу чи кортежів це дуже чудово. У F # ви очікуєте, що люди будуть узгоджуватися, і це моментально має сенс.

«Проблема» полягає в тому, що як тільки ви починаєте використовувати деякі функціональні поняття, природно хотіти продовжувати. Однак використання кортежів, функцій, часткового застосування методу та каррінгу, узгодження шаблонів, вкладених функцій, дженериків, підтримки монади тощо у C # стає дуже негарним, дуже швидко. Це весело, і деякі дуже розумні люди зробили дуже цікаві речі на C #, але насправді користуватися цим відчувається важко.

Що я в кінцевому підсумку використовую часто (через проекти) у C #:

  • Функції послідовності методами розширення для IEnumerable. Такі речі, як ForEach або Process ("Застосувати"? - виконайте дії щодо елемента послідовності, як він перерахований), вписуються, оскільки синтаксис C # добре підтримує його.
  • Абстрагування загальних зразків висловлювань. Складні блоки спробу / лову / нарешті або інші блоки коду (часто дуже загальні). Розширення LINQ до SQL вписується і тут.
  • Кортежі, певною мірою.

** Але зауважте: відсутність автоматичного узагальнення та виводу типу дійсно перешкоджає використанню навіть цих функцій. **

Все це говорило, як хтось ще згадував, про невелику команду з певною метою, так, можливо, вони можуть допомогти, якщо ви застрягли з C #. Але, на мій досвід, вони зазвичай відчували себе більше клопоту, ніж коштували - YMMV.

Деякі інші посилання:


25

Можливо, причина того, що C # не спрощує вмикання типу, полягає в тому, що це в першу чергу об'єктно-орієнтована мова, а "правильним" способом зробити це в об'єктно-орієнтованих термінах було б визначити метод GetRentPrice на транспортному засобі та переосмислити його у похідних класах.

Це означає, що я провів трохи часу, граючи з багатопарадигмою та функціональними мовами, такими як F # і Haskell, які мають цей тип можливостей, і я натрапив на ряд місць, де це було б корисно раніше (наприклад, коли ви не пишуть типи, які потрібно ввімкнути, щоб ви не могли реалізувати на них віртуальний метод), і це я б вітав мовою разом з дискримінаційними спілками.

[Редагувати: Видалено частину про продуктивність, оскільки Марк вказував, що це може бути коротке замикання]

Ще одна потенційна проблема - зручність використання - з фінального дзвінка зрозуміло, що станеться, якщо відповідність не відповідає будь-яким умовам, але яка поведінка, якщо вона відповідає двом і більше умовам? Чи повинен це викинути виняток? Чи повинен повернути перший чи останній матч?

Спосіб, який я схильний використовувати для вирішення подібної проблеми, - це використання словникового поля з типом як ключем і лямбда як значення, яке досить лаконічно побудувати за допомогою синтаксису об'єктного ініціалізатора; однак, це пояснює лише конкретний тип і не допускає додаткових предикатів, тому може не підходити для більш складних випадків. [Побічна примітка - якщо ви дивитеся на висновок компілятора C #, він часто перетворює оператори перемикання в таблиці стрибків на основі словника, тому, здається, немає вагомих причин, щоб він не міг підтримувати перемикання типів]


1
Насправді - у мене є версія короткого замикання як у версії делегата, так і у виразі. Версія виразу компілюється до складної умовної; версія делегата - це просто набір предикатів і функцій / дій - після того, як він відповідає, він зупиняється.
Марк Гравелл

Цікаво - з побіжного погляду я припускав, що йому доведеться виконати принаймні базову перевірку кожної умови, як це було схоже на ланцюжок методів, але тепер я розумію, що методи насправді пов’язують екземпляр об'єкта для його побудови, щоб ви могли це зробити. Я відредагую свою відповідь, щоб видалити це твердження.
Грег Бук

22

Я не думаю, що подібні бібліотеки (які діють як розширення мови), швидше за все, отримають широке визнання, але з ними весело грати, і вони можуть бути дуже корисними для невеликих команд, які працюють у певних областях, де це корисно. Наприклад, якщо ви пишете багато "бізнес-правил / логіки", які роблять тести довільних типів, як це, і що ні, я можу зрозуміти, як це було б зручно.

Я не маю поняття, якщо це колись може бути мовою C # (здається сумнівною, але хто може бачити майбутнє?).

Для довідки, відповідний F # приблизно:

let getRentPrice (v : Vehicle) = 
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith "blah"

припускаючи, що ви визначили ієрархію класів за рядками

type Vehicle() = class end

type Motorcycle(cyl : int) = 
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) = 
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors

2
Дякуємо за версію F #. Напевно, мені подобається, як F # вирішує це, але я не впевнений, що на даний момент F # - це правильний вибір на даний момент, тому мені потрібно пройтись по цій середині ...
Марк Гравелл

13

Щоб відповісти на ваше запитання, так, я думаю, корисні синтаксичні конструкції, що відповідають шаблону. Я б хотів бачити синтаксичну підтримку в C # для цього.

Ось моя реалізація класу, який забезпечує (майже) той самий синтаксис, який ви описуєте

public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o), 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T, 
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

Ось кілька тестових кодів:

    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) 
            .Case<Bicycle>(30) 
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }

9

Узгодження шаблону (як описано тут ), його мета - деконструювати значення відповідно до їх специфікації. Однак поняття класу (або типу) в C # не з вами погоджується.

Неможливо зазначити, що в мовному дизайні багато парадигми є навпаки, дуже приємно мати лямбда в C #, і Haskell може робити необхідні речі, наприклад, IO. Але це не дуже елегантне рішення, не в моді Haskell.

Але оскільки послідовні мови процедурного програмування можна зрозуміти з точки зору лямбдального обчислення, і C #, як правило, добре вписується в параметри послідовної процедурної мови, це добре підходить. Але, взявши щось із чистого функціонального контексту, скажімо, Haskell, а потім перенести цю особливість на мову, яка не є чистою, ну, роблячи саме це, не гарантуватиме кращого результату.

Моя думка в тому, що те, що робить тик відповідності шаблону, пов'язане з мовною конструкцією та моделлю даних. Сказавши це, я не вважаю, що відповідність шаблонів є корисною особливістю C #, оскільки вона не вирішує типових проблем C #, а також не вписується в парадигму імперативного програмування.


1
Може бути. Справді, я б намагався придумати переконливий «вбивчий» аргумент, чому це буде потрібно (на відміну від «можливо приємного в кількох крайніх випадках ціною зробити мову більш складною»).
Марк Гравелл

5

IMHO OO спосіб робити такі речі - це шаблон відвідувачів. Методи вашого відвідувача просто діють як конструкції випадку, і ви дозволяєте самій мові обробляти відповідну розсилку, не потребуючи "зазирнути" на типи.


4

Хоча для вмикання типу не дуже "C-sharpey", я знаю, що конструкція була б дуже корисною для загального використання - у мене є принаймні один особистий проект, який міг би його використати (хоча керований банкомат). Чи існує велика проблема продуктивності компіляції з перезаписом дерева виразів?


Не якщо ви кешуєте об'єкт для повторного використання (це значною мірою, як працюють вирази C # lambda, за винятком компілятора, що приховує код). Перезапис безумовно покращує складену продуктивність, однак, для регулярного використання (а не LINQ-to-Something) я думаю, що версія делегата може бути кориснішою.
Марк Гравелл

Зауважте також - це не обов'язково перемикач типу - він також може бути використаний як складений умовний (навіть через LINQ) - але без безладного x => Тесту? Результат1: (Test2? Результат2: (Test3? Результат 3: Результат4))
Марк Гравелл

Приємно знати, хоча я мав на увазі виконання фактичної компіляції : скільки часу займає csc.exe - я недостатньо знайомий з C #, щоб знати, чи це колись справді проблема, але це велика проблема для C ++.
Саймон Бучан

csc не моргне на це - він настільки схожий на те, як працює LINQ, а компілятор C # 3.0 досить хороший у методах LINQ / розширення тощо
Marc Gravell

3

Я думаю, що це виглядає дуже цікаво (+1), але слід бути обережним: компілятор C # досить добре оптимізує оператори переключення. Не тільки для короткого замикання - ви отримуєте абсолютно різні ІЛ залежно від того, скільки випадків у вас є тощо.

Ваш конкретний приклад робить щось, що мені здається дуже корисним - немає синтаксису, еквівалентного випадку за типом, так як (наприклад) typeof(Motorcycle)не є постійною.

Це стає більш цікавим у динамічному застосуванні - ваша логіка тут може бути легко керована даними, надаючи стиль "двигуна правил" виконання.


0

Ви можете домогтися того, що вам потрібно, скориставшись бібліотекою, яку я написав під назвою OneOf

Основна перевага перед switchifі exceptions as control flow) полягає в тому, що це безпечно під час компіляції - немає обробника за замовчуванням і не проходить

   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

Він знаходиться на Nuget і націлений на Net451 та netstandard1.6

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.