Чи монади є життєздатною (можливо, кращою) альтернативою ієрархіям спадкування?


20

Я буду використовувати мовно-агностичний опис монад на кшталт цього, спочатку опишу моноїди:

Моноїд це (приблизно) набір функцій , які приймають певний тип в якості параметра і повертають один і той же тип.

Монада є (приблизно) набір функцій , які приймають обгортки типу в якості параметра і повертає один і той же тип обгортки.

Зауважте, що це описи, а не визначення. Не соромтеся атакувати цей опис!

Так, мовою ОО, монада дозволяє виконувати такі операції, як:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Зауважте, що монада визначає та керує семантикою цих операцій, а не класом, що міститься.

Традиційно в мові ОО ми використовували ієрархію класів і успадкування для надання цієї семантики. Таким чином , ми будемо мати Birdклас з методами takeOff(), flyAround()і land(), і качка успадкує ті.

Але тоді ми потрапляємо в біду з нелітаючими птахами, бо penguin.takeOff()не вдається. Ми маємо вдатися до викидів та поводження з винятками.

Крім того, як тільки ми кажемо, що Пінгвін - це Bird, ми стикаємося з проблемами багаторазового успадкування, наприклад, якщо ми також маємо ієрархію Swimmer.

По суті ми намагаємось класти класи в категорії (з вибаченнями для хлопців із Теорії категорій) та визначати семантику за категоріями, а не в окремих класах. Але монади здаються набагато зрозумілішим механізмом для цього, ніж ієрархії.

Тож у цьому випадку у нас буде Flier<T>монада, як у наведеному вище прикладі:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... і ми ніколи не створимо це Flier<Penguin>. Ми навіть можемо використовувати статичне введення тексту, щоб не допустити цього, можливо, із інтерфейсом маркера. Або перевірка можливостей виконання, щоб отримати заставу. Але дійсно, програміст ніколи не повинен ставити Пінгвіна в Flier, в тому ж сенсі вони ніколи не повинні ділитися на нуль.

Крім того, він більш загальноприйнятний. Летчик не повинен бути птахом. Наприклад Flier<Pterodactyl>, або Flier<Squirrel>, не змінюючи семантики цих окремих типів.

Після того, як ми класифікуємо семантику за складовими функціями на контейнері - замість ієрархій типів - вона вирішує старі проблеми з класами, які "роблять, роблять, не" вписуються в певну ієрархію. Це також легко і чітко дозволяє проводити кілька семантик для класу, як Flier<Duck>і Swimmer<Duck>. Схоже, ми боролися з невідповідністю імпедансу, класифікуючи поведінку за ієрархіями класів. Монади справляються з цим елегантно.

Отже, моє запитання полягає в тому, що ми також віддаємо перевагу складу над спадщиною, чи має сенс віддавати перевагу монадам над спадщиною?

(До речі, я не був впевнений, чи має це бути тут, чи в Comp Sci, але це здається більше проблемою практичного моделювання. Але, можливо, там краще.)


1
Не впевнений, що я розумію, як це працює: білка і качка не летять однаково - тому "дії мухи" потрібно здійснити в цих класах ... І літаку потрібен метод зробити білку і качку fly ... Можливо, у загальному інтерфейсі Flier ... На жаль, почекайте хвилину ... Я щось пропустив?
assylias

Інтерфейси відрізняються від успадкування класів, оскільки інтерфейси визначають можливості, тоді як функціональне успадкування визначає фактичну поведінку. Навіть у «складі над успадкуванням» визначення інтерфейсів все ще є важливим механізмом (наприклад, поліморфізм). Інтерфейси не стикаються з одними і тими самими проблемами успадкування. Крім того, кожен флаєр може забезпечити (за допомогою інтерфейсу та поліморфізму) властивості можливостей, як "getFlightSpeed ​​()" або "getManuverability ()" для використання контейнера.
Роб

3
Ви намагаєтеся запитати, чи завжди використання параметричного поліморфізму є життєздатною альтернативою підтипу поліморфізму?
ChaosPandion

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

6
Я не розумію вашого опису моноїдів і монад. Ключовою властивістю моноїдів є те, що вона включає асоціативну бінарну операцію (додавання з плаваючою комою, множення чисел чи конкатенація рядків). Монада - це абстракція, яка підтримує послідовність різних (можливо залежних) обчислень у певному порядку.
Rufflewind

Відповіді:


15

Коротка відповідь - ні , монади не є альтернативою ієрархії спадкування (також відомий як політип поліморфізму підтипу). Здається, ви описуєте параметричний поліморфізм , яким користуються монади, але це не єдине, що потрібно зробити.

Наскільки я їх розумію, монади по суті не мають нічого спільного з спадщиною. Я б сказав, що дві речі є більш-менш ортогональними: вони призначені для вирішення різних проблем, і так:

  1. Їх можна використовувати синергетично принаймні у двох сенсах:
    • ознайомтеся з Typeclassopedia , який охоплює багато класів типів Haskell. Ви помітите, що між ними є спадкові відносини. Наприклад, Монада походить від Applicative, який сам походить від Functor.
    • типи даних, які є екземплярами Monads, можуть брати участь у ієрархіях класів. Пам'ятайте, Monad більше схожий на інтерфейс - реалізація його для заданого типу розповідає вам деякі речі про тип даних, але не все.
  2. Намагатися використовувати одне для іншого, буде важко і негарно.

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


Тепер, щоб вибрати своє запитання (вибачте, я маю на увазі, щоб це було корисно, а не для того, щоб почувати себе погано): Я відчуваю, що існує багато сумнівних приміщень, на які я спробую пролити трохи світла.

  1. Монада - це набір функцій, який приймає тип контейнера як параметр і повертає той самий тип контейнера.

    Ні, це є Monadв Haskell: параметризрвані типу m aз реалізацією return :: a -> m aі (>>=) :: m a -> (a -> m b) -> m b, що задовольняє наступні законами:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Є деякі екземпляри Monad, які не є контейнерами ( (->) b), а є деякі контейнери, які не є (і не можуть бути зроблені) екземплярами Monad ( Setчерез обмеження класу типів). Так що "контейнерна" інтуїція погана. Дивіться це для додаткових прикладів.

  2. Так, мовою ОО, монада дозволяє виконувати такі операції, як:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

    Ні, зовсім ні. Цей приклад не вимагає монади. Все, що потрібно, - це функції, що відповідають типам вводу та виводу. Ось ще один спосіб його написання, який підкреслює, що це просто функціональна програма:

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Я вважаю, що це схема, відома як "вільний інтерфейс" або "ланцюжок методів" (але я не впевнений).

  3. Зауважте, що монада визначає та керує семантикою цих операцій, а не класом, що міститься.

    Типи даних, які також є монадами, можуть (і майже завжди це робити!) Мають операції, не пов'язані з монадами. Ось приклад Haskell, що складається з трьох функцій, []які не мають нічого спільного з монадами: []"визначає та контролює семантику операції", а "міститься клас" не робить, але цього недостатньо для створення монади:

    \predicate -> length . filter predicate . reverse
    
  4. Ви правильно зазначили, що існують проблеми із використанням ієрархій класів для моделювання речей. Однак у ваших прикладах немає жодних доказів того, що монади можуть:

    • Робіть хорошу роботу в тих речах, в яких є спадщина
    • Робіть гарну роботу в тих речах, на які спадщина погана

3
Дякую! Багато для мене обробляти. Я не відчуваю себе погано - дуже ціную прозріння. Мені б гірше переносити погані ідеї. :) (Доходить до всієї точки stackexchange!)
Роб

1
@RobY Вас вітають! До речі, якщо ви про це раніше не чули, я рекомендую LYAH, оскільки це чудове джерело для вивчення монад (і Haskell!), Оскільки в ньому є багато прикладів (і я вважаю, що робити тонни прикладів - найкращий спосіб боротися з монадами).

Тут багато лотів; Я не хочу заглиблювати коментарі, але кілька коментарів: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))не працює (принаймні в ОО), тому що ця конструкція вимагає порушити інкапсуляцію, щоб дізнатися про деталі Flier. Шляхом ланцюжкової операції на класі деталі Flier залишаються прихованими, і це може зберегти його семантику. Це аналогічно тому, що в Хаскеллі монада пов'язується, (a, M b)а не (M a, M b)так, що монаді не потрібно піддавати свій стан функції "дії".
Роб

№1, на жаль, я намагаюся розмити суворе визначення Монади в Хаскеллі, оскільки зіставлення чого-небудь з Haskell має велику проблему: функціональний склад, включаючи композицію на конструкторах , що ви не можете легко зробити на пішохідній мові, як Java. Так він unitстає (здебільшого) конструктором для утримуваного типу і bindстає (здебільшого) мається на увазі операцією компіляції в часі (тобто ранньою прив'язкою), яка пов'язує функції "дії" з класом. Якщо у вас є першокласні функції, або функція <A, Monad <B>>, то bindметод може зробити пізнє прив'язування, але я зловживаю цим зловживанням далі. ;)
Роб

№3 погоджуємось, і в цьому краса. Якщо Flier<Thing>керує семантикою польоту, то вона може викрити безліч даних і операцій, що підтримують семантику польоту, в той час як специфічна семантика "монади" насправді полягає в тому, щоб зробити її доступною і інкапсульованою. Ці проблеми можуть не (а з тими, якими я користувався, ні) стосуються класу всередині монади: наприклад, Resource<String>має властивість httpStatus, але String - ні.
Роб

1

Отже, моє запитання полягає в тому, що ми також віддаємо перевагу складу над спадщиною, чи має сенс віддавати перевагу монадам над спадщиною?

Так, мовами, що не належать до ОО, так. У більш традиційних мовах ОО я б сказав «ні».

Проблема полягає в тому, що більшість мов не мають спеціалізації типів, тобто ви не можете робити Flier<Squirrel>та Flier<Bird>мати різні реалізації. Ви повинні зробити щось на кшталт static Flier Flier::Create(Squirrel)(а потім перевантажувати для кожного типу). Що в свою чергу означає, що вам потрібно змінювати цей тип кожного разу, коли ви додаєте нову тварину, і, ймовірно, дублюєте зовсім небагато коду, щоб вона працювала.

Ну, і в не декількох мовах (наприклад, C #) public class Flier<T> : T {}є незаконним. Він навіть не будуватиметься. Більшість, якби не всі програмісти OO очікували, що Flier<Bird>вони все ще будуть Bird.


дякую за коментар У мене є ще кілька думок, але просто тривіально, хоча Flier<Bird>це параметризований контейнер, ніхто не вважає, що це Bird(!?) List<String>- це список, а не рядок.
Роб

@RobY - Flierце не просто контейнер. Якщо ви вважаєте це просто контейнером, чому б ви коли-небудь думали, що він може замінити використання спадщини?
Теластин

Я втратив тебе там ... моя думка - монада - це розширений контейнер. Animal / Bird / Penguinзазвичай є поганим прикладом, оскільки він приносить всіляку семантику. Практичний приклад - монада REST-ish, яку ми використовуємо: Resource<String>.from(uri).get() Resourceдодає семантику поверх String(або якийсь інший тип), так що, очевидно, це не є String.
Роб

@RobY - але тоді це жодним чином не пов’язане зі спадщиною.
Теластин

За винятком того, що це інший вид стримування. Я можу вставити String в Resource, або я можу абстрагувати клас ResourceString і використовувати спадкування. Думаю, що розміщення класу в ланцюговому контейнері - це кращий спосіб абстрагувати поведінку, ніж переводити його в ієрархію класів із успадкуванням. Тож "ніяк не пов'язаний" у значенні "заміни / усунення" - так.
Роб
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.