Чи відповідність шаблонів проти типів ідіоматична чи поганий дизайн?


18

Схоже, що F # код часто узгоджується з типом. Звичайно

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

здається загальним.

Але з точки зору OOP, це виглядає надзвичайно багато, як контрольний потік на основі перевірки типу виконання, який, як правило, нахмуриться. Щоб прописати це, в OOP ви, мабуть, віддасте перевагу використовувати перевантаження:

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"

Це, звичайно, більше коду. ОТОХ, на мій погляд, OOP-y має структурні переваги:

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

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


4
Скільки сенсу бачити функціональну мову з точки зору ООП? У будь-якому разі реальна сила узгодження зразків припадає на вкладені візерунки. Можливо, просто перевірити самий зовнішній конструктор, але далеко не всю історію.
Інго

Це - 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.- звучить занадто догматично. Іноді, ви хочете відокремити свій ops від своєї ієрархії: можливо, 1) ви не можете додати оп до ієрархії b / c, у вас немає ієрархії; 2) класи, які ви хочете мати, не відповідають вашій ієрархії; 3) ви можете додати оп у свою ієрархію, але не хочете перебирати / не хочете переповнювати API вашої ієрархії безліччю лайна, якою більшість клієнтів не користується.

4
Просто для уточнення, Someі Noneце не типи. Вони обидва конструктора , чиї типи forall a. a -> option aі forall a. option a(вибачте, не впевнений , що синтаксис для анотації типу в F #).

Відповіді:


21

Ви вірні в тому, що ієрархії класів OOP дуже тісно пов'язані з дискримінованими об'єднаннями в F #, і відповідність шаблонів дуже тісно пов'язана з тестами динамічного типу. Насправді саме так F # збирає дискриміновані спілки до .NET!

Щодо розширюваності, існує дві сторони проблеми:

  • OO дозволяє додавати нові підкласи, але ускладнює додавання нових (віртуальних) функцій
  • 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 більш орієнтована на спочатку проектування типів, а потім додавання функцій. Тож це дійсно вигідно від того, що ви можете помістити свої типи (модель домена) в пару рядків в одному файлі, а потім легко додати функції, що працюють на доменній моделі.

Два підходи - OO і FP є досить взаємодоповнюючими і мають і переваги, і недоліки. Найважливіша річ (з точки зору ОО) полягає в тому, що F # зазвичай використовує стиль FP як стандартний. Але якщо дійсно більше необхідності в додаванні нових підкласів, ви завжди можете використовувати інтерфейси. Але в більшості систем вам однаково потрібно додати типи та функції, тому вибір насправді не має великого значення - і використовувати дискриміновані об'єднання у F # приємніше.

Я рекомендую цю велику серію блогів для отримання додаткової інформації.


3
Хоча ви правильні, я хотів би додати, що це не стільки проблема OO проти FP, скільки питання про об’єкти та типи суми. Нав'язливість OOP у них осторонь, нічого не стосується предметів, що робить їх нефункціональними. І якщо ви перескочите достатню кількість обручів, ви можете реалізовувати типи сум і в основних мовах OOP (хоча це буде не дуже).
Доваль

1
"І якщо ви перескочите достатню кількість обручів, ви також можете реалізувати типи сум у основних мовах OOP (хоча це буде не дуже)." -> Я думаю, ви закінчите щось подібне до того, як типи суми F # закодовані в системі типів .NET :)
Tarmil

8

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

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

// 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. Виправте всі попередження на неповних збігах шаблонів

2

Частково ви бачите це частіше у функціональному програмуванні, оскільки ви використовуєте типи для прийняття рішень частіше. Я усвідомлюю, що ви, мабуть, просто вибрали приклади більш-менш випадково, але OOP, еквівалентний вашому прикладу відповідності, частіше виглядатиме так:

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

Іншими словами, порівняно рідко застосовувати поліморфізм, щоб уникнути звичайних речей, таких як нульові перевірки в ООП. Так само, як програміст ОО не створює нульового об'єкта в кожній маленькій ситуації, функціональний програміст не завжди перевантажує функцію, особливо коли ви знаєте, що ваш список шаблонів гарантовано є вичерпним. Якщо ви використовуєте систему типів у більшій кількості ситуацій, ви побачите, що вона використовується тим способом, до якого ви не звикли.

І навпаки, ідіоматичне функціональне програмування, еквівалентне вашому прикладу OOP, швидше за все, не використовує відповідність шаблонів, але має fooRouteі barRouteфункції, які передаються як аргументи до викличного коду. Якщо хтось використовував відповідність шаблонів у цій ситуації, це зазвичай вважається неправильним, подібно до того, як хтось перемикає типи, вважатиметься неправильним в OOP.

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

Зіставлення шаблонів може поверхнево нагадувати об'єктно-орієнтований оператор перемикача, але відбувається набагато більше, особливо з довшими або вкладеними шаблонами. Не забудьте взяти до уваги все, що він робить, перш ніж оголосити його еквівалентним погано розробленому коду OOP. Часто це стисло вирішує ситуацію, яка не може бути чітко представлена ​​в ієрархії спадкування.


Я знаю , що ви знаєте це, і це , ймовірно , послизнувся розум при написанні відповіді, але зауважте , що Someі Noneне є типами, так що ви не відповідає шаблон на типи. Ви шаблон матчу на конструкторах цього ж типу . Це не так, як запитувати "instanceof".
Андрес Ф.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.