Осиротілі випадки в Хаскелі


86

Під час компіляції моєї програми Haskell з цією -Wallопцією GHC скаржиться на випадки, що залишились без батьків, наприклад:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

Клас типу ToSElemне є моїм, він визначається HStringTemplate .

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

Причиною того, що я хочу оголосити свої ToSElemекземпляри в модулі Publisher, є те, що саме модуль Publisher залежить від HStringTemplate, а не від інших модулів. Я намагаюся підтримувати відокремленість проблем і уникати, щоб кожен модуль залежав від HStringTemplate.

Я думав, що однією з переваг класів типів Хаскелла, якщо порівнювати їх, наприклад, з інтерфейсами Java, є те, що вони відкриті, а не закриті, і тому екземпляри не повинні оголошуватися в тому самому місці, що і тип даних. Порада GHC, схоже, полягає в тому, щоб ігнорувати це.

Отже, те, що я шукаю, - це або підтвердження того, що моє мислення обґрунтоване, і що я міг би бути виправданим, ігноруючи / придушуючи це попередження, або більш переконливий аргумент проти того, щоб робити все по-своєму.


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

Відповіді:


94

Я розумію, чому ви хочете це зробити, але, на жаль, це може бути лише ілюзією того, що класи Haskell здаються "відкритими" так, як ви говорите. Багато людей вважають, що можливість зробити це є помилкою в специфікації Haskell, з причин, які я поясню нижче. У будь-якому випадку, якщо це дійсно не підходить для екземпляра, вам потрібно оголосити або в модулі, де оголошено клас, або в модулі, де оголошено тип, це, мабуть, знак того, що ви повинні використовувати newtypeоболонку або якусь іншу навколо вашого типу.

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

Проблема випливає з того, що коли для одного класу та типу існує більше одного оголошення екземпляра, у стандартному Haskell не існує механізму, який би вказав, який використовувати. Швидше, програма відхиляється компілятором.

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

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

Люди, які хочуть гарантувати, що з ними ніколи не трапиться цих проблем, повинні дотримуватися правила, згідно з яким якщо хтось і де-небудь коли-небудь оголосив екземпляр певного класу для певного типу, жоден інший екземпляр не повинен бути оголошений знову в будь-якій написаній програмі будь-ким. Звичайно, є обхідний шлях використання a newtypeдля оголошення нового екземпляру, але це завжди є принаймні незначною незручністю, а іноді і основною. Отже, у цьому сенсі ті, хто навмисно пишуть екземпляри-сироти, є досить неввічливими.

То що слід зробити з цією проблемою? У таборі, що описується анти-сиротою, кажуть, що попередження GHC - це помилка, це має бути помилка, яка відхиляє будь-яку спробу оголосити сироту-екземпляр. Тим часом ми повинні проявляти самодисципліну і уникати їх будь-якою ціною.

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

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

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


4
Зокрема, це дедалі більша проблема зростання числа бібліотек. Маючи> 2200 бібліотек на Haskell та 10 тисяч окремих модулів, ризик отримання екземплярів різко зростає.
Дон Стюарт,

16
Re: "Я вважаю, що правильним рішенням було б додати розширення до механізму імпорту Haskell, яке контролювало б імпорт екземплярів". Якщо ця ідея когось цікавить, можливо, варто поглянути на мову Scala для прикладу; він має такі функції, як ця, для управління сферою дії "імпліцитів", які можуть бути використані дуже як екземпляри typeclass.
Matt

5
Моє програмне забезпечення - це додаток, а не бібліотека, тому можливість спричинити проблеми для інших розробників практично нульова. Ви можете розглядати модуль Publisher як додаток, а решту модулів - як бібліотеку, але якби я розповсюджував бібліотеку, це було б без Publisher і, отже, безсистемних екземплярів. Але якби я перемістив екземпляри в інші модулі, бібліотека надходила б із непотрібною залежністю від HStringTemplate. Тож у цьому випадку я думаю, що з сиротами все гаразд, але я послухаюся вашої поради, якщо зіткнуся з тією ж проблемою в іншому контексті.
Dan Dyer

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

@Matt: справді, напрочуд Scala отримує цей саме там, де цього не робить Haskell! (крім звичайно, Scala не має першокласного синтаксису для машин класу типу, що ще гірше ...)
Ерік Каплун,

44

Вперед і придушіть це попередження!

Ви в хорошій компанії. Conal робить це в "TypeCompose". "chp-mtl" та "chp-transformers" це роблять, "control-monad-виняток-mtl" та "control-monad-виняток-monadsfd" тощо.

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

{-# OPTIONS_GHC -fno-warn-orphans #-}

Редагувати:

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

У своїй короткій відповіді я використав лише знак оклику, оскільки ваше запитання свідчить про те, що ви вже добре знаєте про проблеми. Інакше я був би менш захоплений :)

Трохи відвернення, але те, що я вважаю, є ідеальним рішенням у ідеальному світі без компромісів:

Я вважаю, що проблеми, про які згадує Іц (не знаючи, який екземпляр вибрано), можуть бути вирішені за допомогою "цілісної" системи програмування, де:

  • Ви не редагуєте лише текстові файли примітивно, а скоріше вам допомагає середовище (наприклад, заповнення коду пропонує лише речі відповідних типів тощо)
  • Мова "нижчого рівня" не має спеціальної підтримки для класів типів, і замість цього таблиці функцій передаються явно
  • Але середовище програмування "вищого рівня" відображає код так само, як представлений Haskell зараз (зазвичай ви не бачите переданих таблиць функцій), і вибирає явні типи класів для вас, коли вони очевидні (для Наприклад, усі випадки функціонера мають лише один вибір), і коли є кілька прикладів (стиснення списку Applicative або list-monad Applicative, First / Last / lift можливо Monoid), це дозволяє вибрати, який екземпляр використовувати.
  • У будь-якому випадку, навіть коли екземпляр був обраний для вас автоматично, середовище легко дозволяє побачити, який екземпляр був використаний, за допомогою простого інтерфейсу (гіперпосилання, інтерфейс наведення або щось інше)

Повернувшись із фантастичного світу (або, сподіваємось, з майбутнього), прямо зараз: я рекомендую намагатися уникати випадків-сиріт, одночасно використовуючи їх, коли вам "дійсно потрібно"


5
Так, але, можливо, кожне з цих випадків є помилкою певного порядку. Погані випадки в control-monad-виняток-mtl та monads-fd для будь-якого приходять на розум. Було б менш нав'язливо, якби кожен із цих модулів був змушений визначати свої власні типи або поставляти обгортки нового типу. Майже кожен примірник-сирота - це головний біль, який чекає, і якщо ніщо інше не вимагатиме вашої постійної пильності, щоб переконатись, що він імпортований чи не належним чином.
Едвард КМЕТТ

2
Дякую. Я думаю, що буду використовувати їх у цій конкретній ситуації, але завдяки Іцу я тепер краще розумію, які проблеми вони можуть викликати.
Ден Дайер,

37

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

Ідея про те, що вам слід обернути тип у новий тип, коли вам потрібно надати екземпляр, є ідеєю з теоретичними достоїнствами, але вона просто занадто нудна в багатьох обставинах; це така ідея, висунута людьми, які не пишуть код Хаскелла на життя. :)

Тож продовжуйте і надавайте випадки-сироти. Вони нешкідливі.
Якщо ви можете зірвати ghc із екземплярами-сиротами, тоді це помилка, і про неї слід повідомляти як таку. (Помилка ghc про відсутність декількох екземплярів не така важка для виправлення.)

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


2
Хороший приклад - це (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)використання QuickCheck.
Ерік Каплун,

17

У цьому випадку я вважаю, що використання випадків-сиріт є нормальним. Загальне правило для мене таке: ви можете визначити примірник, якщо ви "володієте" типовим класом або якщо ви "володієте" типом даних (або деяким його компонентом - тобто, екземпляр для Можливо MyData також добре, принаймні іноді). У межах цих обмежень, коли ви вирішите розмістити екземпляр, це ваша власна справа.

Є ще один виняток - якщо ви не володієте типовим класом або типом даних, але створюєте двійковий файл, а не бібліотеку, то це теж добре.


5

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

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


3

З цього приводу я розумію позицію табору табору примірників антисиріт у бібліотеках WRT, але для виконуваних цілей екземпляри-сироти не повинні бути нормальними?


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

5
Майже у всіх випадках реалізації екземпляра-сироти у виконуваному файлі це заповнення прогалини, яку ви хотіли б уже визначити для вас. Отже, якщо екземпляр з’являється вище за течією, отримана помилка компіляції є лише корисним сигналом, який повідомляє, що ви можете видалити декларацію екземпляра.
Ben
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.