Совпадение шаблонов с типами идиоматическое или плохой дизайн?

18

Кажется, что код F # часто сопоставляется с типами. Конечно

match opt with 
| Some val -> Something(val) 
| None -> Different()

кажется обычным.

Но с точки зрения ООП это выглядит очень похоже на поток управления, основанный на проверке типа во время выполнения, которая обычно вызывает недовольство. Чтобы объяснить это, в ООП вы, вероятно, предпочтете использовать перегрузку:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

Это, конечно, больше кода. OTOH, мне кажется, у OOP-y есть структурные преимущества:

  • расширение к новой форме Tлегко;
  • Мне не нужно беспокоиться об обнаружении дублирования потока управления выбором маршрута; и
  • Выбор маршрута является неизменным в том смысле, что, как только у меня есть Fooв руках, мне не нужно беспокоиться о Bar.Route()реализации

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

Ларри Обриен
источник
3
Какой смысл рассматривать функциональный язык с точки зрения ООП? В любом случае, реальная сила сопоставления с образцом заключается во вложенных шаблонах. Возможна проверка самого внешнего конструктора, но отнюдь не всей истории.
Инго
Это But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.звучит слишком догматично. Иногда вы хотите отделить свои операции от своей иерархии: возможно, 1) вы не можете добавить операцию в иерархию, потому что у вас нет этой иерархии; 2) классы, которые вы хотите иметь, не соответствуют вашей иерархии; 3) вы можете добавить опцию в вашу иерархию, но не хотите, чтобы вы не хотели загромождать API своей иерархии кучей дерьма, которое не использует большинство клиентов.
4
Просто чтобы уточнить, Someа Noneне типы. Оба они являются конструкторами, чьи типы forall a. a -> option aи forall a. option a(извините, не уверен, какой синтаксис для аннотаций типов в F #).

Ответы:

20

Вы правы в том, что иерархии классов ООП очень тесно связаны с различимыми объединениями в F #, и что сопоставление с образцом очень тесно связано с тестами динамических типов. Фактически, именно так F # компилирует разрозненные объединения в .NET!

Что касается расширяемости, есть две стороны проблемы:

  • ОО позволяет вам добавлять новые подклассы, но затрудняет добавление новых (виртуальных) функций
  • FP позволяет добавлять новые функции, но затрудняет добавление новых случаев объединения

Тем не менее, F # выдаст вам предупреждение, когда вы пропустите случай при сопоставлении с образцом, поэтому добавление новых случаев объединения на самом деле не так уж и плохо.

Относительно поиска дубликатов в выборе корня - F # выдаст вам предупреждение, если у вас есть совпадение, которое дублируется, например:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

Тот факт, что «выбор маршрута является неизменным» также может быть проблематичным. Например, если вы хотите поделиться реализацией функции между Fooи Barслучаями, но сделать что-то еще для Zooслучая, вы можете легко это кодировать, используя сопоставление с образцом:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

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

Два подхода - ОО и ФП достаточно дополняют друг друга, и оба имеют свои преимущества и недостатки. Хитрость (с точки зрения ОО) заключается в том, что F # обычно использует стиль FP по умолчанию. Но если действительно нужно добавить новые подклассы, вы всегда можете использовать интерфейсы. Но в большинстве систем вам в равной степени необходимо добавлять типы и функции, поэтому выбор действительно не имеет большого значения - и использование различаемых объединений в F # более приятно.

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

Томас Петричек
источник
3
Хотя вы правы, я хотел бы добавить, что это не столько проблема ОО против ФП, сколько проблема объектов против типов сумм. Помимо ООП, они не имеют ничего общего с объектами, которые делают их неработоспособными. И если вы перепрыгиваете через достаточное количество обручей, вы можете также реализовать типы сумм в основных языках ООП (хотя это не будет красиво).
Доваль
1
«И если вы перепрыгиваете через достаточное количество обручей, вы можете также реализовать типы сумм в основных языках ООП (хотя это не будет красиво)». -> Полагаю, в итоге вы получите нечто похожее на то, как F # суммы кодируются в системе типов .NET :)
Tarmil
7

Вы правильно заметили, что сопоставление с образцом (по существу, оператор переключения с наддувом) и динамическая диспетчеризация имеют сходство. Они также сосуществуют на некоторых языках с очень приятным результатом. Тем не менее, есть небольшие различия.

Я мог бы использовать систему типов для определения типа, который может иметь только фиксированное количество подтипов:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

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

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

Кроме того, динамическая диспетчеризация метода (которая требуется для ООП) также подразумевает проверку типа во время выполнения, но другого типа. Поэтому довольно не имеет значения, если мы делаем эту проверку типа явно через сопоставление с образцом или неявно через вызов метода.

Амон
источник
«Это означает, что сопоставление с шаблоном больше похоже на специальное снижение, которое вынуждает нас обрабатывать все параметры» - на самом деле, я считаю, что (если вы сопоставляете только конструкторы, а не значения или вложенную структуру), оно изоморфно размещение абстрактного виртуального метода в суперклассе.
Жюль
2

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

Следует также отметить, что в функциональном стиле вы организуете вещи по функциональным возможностям, а не по данным, поэтому сопоставления с образцами позволяют объединять различные функциональные возможности в одном месте, а не разбросаны по классам. Это также имеет то преимущество, что вы можете видеть, как обрабатываются другие дела, рядом с тем местом, где вам нужно внести изменения.

Добавление новой опции выглядит следующим образом:

  1. Добавьте новую опцию в ваш дискриминационный союз
  2. Исправить все предупреждения о неполных совпадениях с образцом
N_A
источник
2

Частично, вы видите это чаще в функциональном программировании, потому что вы используете типы для принятия решений чаще. Я понимаю, что вы, вероятно, просто выбирали примеры более или менее случайным образом, но ООП, эквивалентный вашему примеру сопоставления с образцом, будет чаще выглядеть так:

if (opt != null)
    opt.Something()
else
    Different()

Другими словами, относительно редко можно использовать полиморфизм, чтобы избежать рутинных вещей, таких как нулевые проверки в ООП. Так же, как ОО-программист не создает нулевой объект в каждой маленькой ситуации, функциональный программист не всегда перегружает функцию, особенно если вы знаете, что ваш список шаблонов гарантированно будет исчерпывающим. Если вы используете систему типов в большем количестве ситуаций, вы увидите, что она используется не так, как вы привыкли.

С другой стороны , идиоматических функциональное программирование эквивалентно вашему примеру ООП, скорее всего , не использовать сопоставление с образцом, но будет иметь fooRouteи barRouteфункции , которые будут получать передаваемые в качестве аргументов в коде вызова. Если бы кто-то использовал сопоставление с образцом в этой ситуации, это обычно считалось бы неправильным, точно так же, как кто-то, переключающий типы, считался бы неправильным в ООП.

Итак, когда сопоставление с образцом считается хорошим функциональным программным кодом? Когда вы делаете больше, чем просто смотрите на типы, и при расширении требований не требуется добавлять больше случаев. Например, Some valон не просто проверяет, optимеет ли тип Some, он также привязывается valк базовому типу для безопасного использования типа на другой стороне ->. Вы знаете, что, скорее всего, вам никогда не понадобится третий случай, так что это хорошее применение.

Сопоставление с образцом может внешне напоминать объектно-ориентированный оператор switch, но происходит гораздо больше, особенно с более длинными или вложенными шаблонами. Убедитесь, что вы учитываете все, что он делает, прежде чем объявлять его эквивалентным некачественному ООП-коду. Часто он кратко обрабатывает ситуацию, которая не может быть четко представлена ​​в иерархии наследования.

Карл Билефельдт
источник
Я знаю , что вы знаете это, и это , вероятно , поскользнулся ум при написании ответа, но заметьте , что Someи Noneне являются типами, так что вы не соответствующий шаблон на типы. Вы шаблон матча на конструкторах этого же типа . Это не то же самое, что спрашивать об instanceof.
Андрес Ф.