Есть ли какая-либо польза от этой идеи переключения / сопоставления шаблонов?

#c# #switch-statement

Вопрос:

Я недавно изучал 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>.

[примечание — возможно, переключатель/Регистр здесь неверные термины… но это показывает идею]

Для меня это намного понятнее, чем эквивалент, использующий повторяющиеся if/else или составное троичное условие (что становится очень запутанным для нетривиальных выражений- в изобилии скобок). Это также позволяет избежать большого количества приведения и позволяет просто расширять (либо напрямую, либо с помощью методов расширения) до более конкретных совпадений, например, совпадение по диапазону (…), сопоставимое с выбором VB…Использование случая «От x До y».

Я просто пытаюсь оценить, считают ли люди, что есть большая польза от подобных конструкций (в отсутствие языковой поддержки)?

Обратите внимание дополнительно, что я играл с 3 вариантами вышеперечисленного:

  • версия функции<TSource,TValue> для оценки — сопоставима с составными троичными условными операторами
  • версия действия<TSource> — сопоставима с if/else, если/еще, если/еще, если/еще
  • Выражение<Func<TSource,значение TValue><TSource,значение TValue>> версия — как первая, но может использоваться произвольными поставщиками LINQ

Кроме того, использование версии на основе выражений позволяет переписывать дерево выражений, по сути, объединяя все ветви в одно составное условное выражение, а не используя повторный вызов. Я не проверял в последнее время, но в некоторых ранних сборках Entity Framework я, кажется, припоминаю, что это было необходимо, так как ему не очень нравилось выражение вызова. Это также позволяет более эффективно использовать LINQ-to-Объекты, поскольку позволяет избежать повторных вызовов делегатов — тесты показывают совпадение, подобное приведенному выше (с использованием формы выражения), выполняемое с той же скоростью [на самом деле немного быстрее] по сравнению с эквивалентным составным условным оператором C#. Для полноты, версия на основе функции<…> заняла в 4 раза больше времени, чем условный оператор C#, но все равно очень быстрая и вряд ли станет серьезным узким местом в большинстве случаев использования.

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

Комментарии:

1. «Я просто пытаюсь оценить, считают ли люди, что есть большая польза от конструкций, подобных приведенным выше (при отсутствии языковой поддержки)?» ИМХО, да. Разве что-то подобное уже не существует? Если нет, то рекомендуется написать облегченную библиотеку.

2. Вы можете использовать VB .NET, который поддерживает это в инструкции select case. Ик!

3. Я также подниму свой собственный гудок и добавлю ссылку на свою библиотеку: functional-dotnet

4. Мне нравится эта идея, и она создает очень красивую и гораздо более гибкую форму корпуса переключателя; однако разве это не приукрашенный способ использования синтаксиса, подобного Linq, в качестве оболочки «если-то»? Я бы посоветовал кому-нибудь использовать это вместо реальной сделки, то есть switch-case заявления. Не поймите меня неправильно, я думаю, что это имеет место, и я, вероятно, буду искать способ реализации.

5. Хотя этому вопросу уже более двух лет, кажется уместным упомянуть, что скоро выйдет C# 7(ish) с возможностями сопоставления шаблонов.

Ответ №1:

После попытки сделать такие «функциональные» вещи в C# (и даже попытки написать книгу об этом) я пришел к выводу, что нет, за некоторыми исключениями, такие вещи не слишком помогают.

Основная причина заключается в том, что такие языки, как F#, получают большую часть своей мощи от реальной поддержки этих функций. Не «ты можешь это сделать», а «это просто, это ясно, это ожидаемо».

Например, при сопоставлении шаблонов компилятор сообщает вам, есть ли неполное совпадение или когда другое совпадение никогда не будет найдено. Это менее полезно для открытых типов, но при сопоставлении различаемого объединения или кортежей это очень удобно. В F# вы ожидаете, что люди будут соответствовать шаблону, и это мгновенно приобретает смысл.

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

То, что я часто использовал (в разных проектах) в C#:

  • Функции последовательности с помощью методов расширения для IEnumerable. Такие вещи, как подготовка или процесс («Применить»? — выполните действие над элементом последовательности по мере его перечисления) вписывается, потому что синтаксис C# хорошо его поддерживает.
  • Абстрагирование общих шаблонов высказываний. Сложные блоки try/catch/finally или другие задействованные (часто в значительной степени общие) блоки кода. Расширение LINQ-to-SQL подходит и здесь.
  • В какой-то степени кортежи.

** Но обратите внимание: отсутствие автоматического обобщения и вывода типов действительно затрудняет использование даже этих функций. **

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

Некоторые другие ссылки:

Ответ №2:

Возможно, причина, по которой C# не упрощает включение типа, заключается в том, что это в первую очередь объектно-ориентированный язык, и «правильным» способом сделать это в объектно-ориентированных терминах было бы определить метод GetRentPrice на транспортном средстве и переопределить его в производных классах.

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

[Правка: Удалена часть о производительности, поскольку Марк указал, что это может быть короткое замыкание]

Еще одна потенциальная проблема связана с удобством использования — из заключительного вызова ясно, что произойдет, если совпадение не соответствует каким-либо условиям, но каково поведение, если оно соответствует двум или более условиям? Должно ли это вызвать исключение? Должен ли он вернуть первый или последний матч?

Способ, который я обычно использую для решения такого рода проблем, заключается в использовании поля словаря с типом в качестве ключа и лямбда в качестве значения, которое довольно сложно построить с использованием синтаксиса инициализатора объекта; однако это учитывает только конкретный тип и не допускает дополнительных предикатов, поэтому может не подходить для более сложных случаев. [Примечание — если вы посмотрите на выходные данные компилятора C#, он часто преобразует операторы switch в таблицы переходов на основе словарей, поэтому, похоже, нет веской причины, по которой он не мог бы поддерживать переключение типов]

Комментарии:

1. На самом деле — версия, которая у меня есть, вызывает короткое замыкание как в версиях делегата, так и в версиях выражения. Версия выражения компилируется в составное условие; версия делегата — это просто набор предикатов и функций/действий-как только она совпадает, она останавливается.

2. Интересно — с беглого взгляда я предположил, что он должен будет выполнить, по крайней мере, базовую проверку каждого условия, поскольку это выглядело как цепочка методов, но теперь я понимаю, что методы на самом деле связывают экземпляр объекта, чтобы построить его, чтобы вы могли это сделать. Я отредактирую свой ответ, чтобы удалить это утверждение.

Ответ №3:

В 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));
}
 

Комментарии:

1. Заметное различие здесь между C# и F# заключается в полноте соответствия шаблону. Что соответствие шаблону охватывает все возможные доступные случаи, полностью описанные, предупреждения от компилятора, если вы этого не сделаете. Хотя вы можете с полным правом утверждать, что это происходит в случае по умолчанию, на практике это также часто является исключением во время выполнения.

Ответ №4:

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

Я понятия не имею, может ли это когда-нибудь стать функцией языка 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
 

Комментарии:

1. Спасибо за версию F#. Я думаю, мне нравится, как F# справляется с этим, но я не уверен, что (в целом) F# в данный момент является правильным выбором, поэтому мне приходится идти по этой средней точке…

Ответ №5:

Да, я думаю, что синтаксические конструкции, соответствующие шаблону, полезны. Я, например, хотел бы видеть синтаксическую поддержку в 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 amp;amp; 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));
        }
    }
 

Ответ №6:

Цель сопоставления с образцом (как описано здесь) состоит в том, чтобы деконструировать значения в соответствии со спецификацией их типа. Однако концепция класса (или типа) в C# не согласуется с вами.

В многопарадигмальном языковом дизайне нет ничего плохого, наоборот, очень приятно иметь лямбды в C#, и Хаскелл может делать императивные вещи, например, для ввода-вывода. Но это не очень элегантное решение, не в стиле Хаскелла.

Но поскольку последовательные процедурные языки программирования могут быть поняты в терминах лямбда-исчисления, а C# хорошо вписывается в параметры последовательного процедурного языка, это хорошо подходит. Но, взяв что-то из чистого функционального контекста, скажем, Хаскелла, а затем поместив эту функцию в язык, который не является чистым, ну, просто это не гарантирует лучшего результата.

Я хочу сказать, что функция сопоставления шаблонов привязана к языковому дизайну и модели данных. Сказав это, я не считаю, что сопоставление шаблонов является полезной функцией C#, потому что оно не решает типичных проблем C# и не вписывается в парадигму императивного программирования.

Комментарии:

1. Может быть. В самом деле, я бы с трудом придумал убедительный «убийственный» аргумент в пользу того, почему это было бы необходимо (в отличие от «возможно, хорошо в нескольких крайних случаях за счет усложнения языка»).

Ответ №7:

По моему скромному мнению, объектно-ориентированный способ делать такие вещи-это шаблон посетителя. Ваши методы посетителя-участника просто действуют как конструкции регистра, и вы позволяете самому языку обрабатывать соответствующую отправку без необходимости «заглядывать» в типы.

Ответ №8:

Хотя это не очень «C-sharpey» для переключения типа, я знаю, что эта конструкция была бы довольно полезна в общем использовании — у меня есть по крайней мере один личный проект, который мог бы ее использовать (хотя это вполне выполнимо). Существует ли большая проблема с производительностью компиляции, связанная с переписыванием дерева выражений?

Комментарии:

1. Нет, если вы кэшируете объект для повторного использования (именно так в значительной степени работают лямбда-выражения C#, за исключением того, что компилятор скрывает код). Переписывание определенно повышает производительность компиляции — однако для регулярного использования (а не для связи с чем-то) Я ожидаю, что версия делегата может быть более полезной.

2. Обратите также внимание — это не обязательно тип включения — он также может использоваться как составное условие (даже через LINQ) — но без беспорядочного теста x=>? Результат 1 : (Тест2 ? Результат 2 : (Тест3 ? Результат 3 : Результат 4))

3. Приятно знать, хотя я имел в виду производительность самой компиляции : как долго csc.exe принимает — Я недостаточно знаком с C#, чтобы знать, действительно ли это когда-либо было проблемой, но это большая проблема для C .

4. csc не моргнет при этом — это так похоже на то, как работает LINQ, а компилятор C# 3.0 довольно хорош в методах LINQ/расширения и т.д.

Ответ №9:

Следует быть осторожным в одном: компилятор C# довольно хорошо оптимизирует операторы switch. Не только для короткого замыкания — вы получаете совершенно разные результаты в зависимости от того, сколько у вас дел и так далее.

Ваш конкретный пример делает то, что я нахожу очень полезным — нет синтаксиса, эквивалентного регистру по типу, так как (например) typeof(Motorcycle) не является константой.

Это становится более интересным в динамическом приложении — ваша логика здесь может легко управляться данными, обеспечивая выполнение в стиле «механизма правил».

Ответ №10:

Вы можете достичь того, что вам нужно, используя библиотеку, которую я написал, под названием oneOf

Основное преимущество перед switch if и 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