Є щонайменше 4 бібліотеки, які я знаю, що надають об'єктиви.
Поняття лінзи полягає в тому, що вона забезпечує щось ізоморфне
data Lens a b = Lens (a -> b) (b -> a -> a)
надання двох функцій: геттер і сетер
get (Lens g _) = g
put (Lens _ s) = s
відповідно до трьох законів:
По-перше, якщо ви щось покладете, ви зможете повернути його назад
get l (put l b a) = b
По-друге, отримання та встановлення не відповідає
put l (get l a) a = a
І по-третє, два рази ставити - це те саме, що ставити один раз, а точніше, виграти другий ставок.
put l b1 (put l b2 a) = put l b1 a
Зауважте, що типової системи недостатньо, щоб перевірити ці закони для вас, тому вам потрібно їх забезпечити самостійно незалежно від того, якою реалізацією об'єктивів ви користуєтесь.
Багато з цих бібліотек також надають купу додаткових комбінаторів зверху, і, як правило, певну форму шаблонного механізму haskell для автоматичного генерування об'єктивів для полів простих типів записів.
Зважаючи на це, ми можемо звернутися до різних реалізацій:
Впровадження
fclabels
fclabels , мабуть, є найбільш легко аргументованим щодо бібліотек об'єктивів, оскільки його a :-> b
можна безпосередньо перекласти на вищевказаний тип. Він пропонує примірник категорії,(:->)
який корисний, оскільки дозволяє складати лінзи. Він також забезпечує беззаконний Point
тип, який узагальнює поняття використовуваної тут лінзи, а також деяку сантехніку для боротьби з ізоморфізмами.
Одним із перешкод на шляху прийняття fclabels
є те, що основний пакет включає сантехніку шаблону-haskell, тому пакет не є Haskell 98, і він також вимагає (досить не суперечливого) TypeOperators
розширення.
дані-доступ
[Редагувати: data-accessor
більше не використовує це представлення, але перейшло до форми, подібної до форми data-lens
. Я все ж зберігаю цей коментар.]
data-accessor є дещо популярнішим fclabels
, відчасти тому, що це Haskell 98. Однак його вибір внутрішнього представництва змушує мене трохи кинути в рот.
Тип, який T
він використовує для представлення лінзи, внутрішньо визначається як
newtype T r a = Cons { decons :: a -> r -> (a, r) }
Отже, для get
значення об’єктива потрібно подати невизначене значення для аргументу "a"! Це вражає мене як неймовірно некрасивою та спеціальною реалізацією.
При цьому Геннінг включив сантехніку шаблону-haskell для автоматичного генерування для вас аксесуарів в окремому пакеті " data-accessor-template ".
Він має перевагу пристойно великого набору пакетів, які вже використовують його, будучи Haskell 98, і надає все важливий Category
екземпляр, тому якщо ви не звертаєте уваги на те, як виготовлена ковбаса, цей пакет насправді є досить розумним вибором .
лінзи
Далі є пакет лінз , який зазначає, що лінза може забезпечити гомоморфізм стану монади між двома монадами стану, визначаючи лінзи безпосередньо як такі мономорфізми монади.
Якщо б насправді було задумано надати тип лінз, вони мали б тип 2 типу:
newtype Lens s t = Lens (forall a. State t a -> State s a)
Як результат, мені не подобається такий підхід, оскільки він марно позбавляє вас від Haskell 98 (якщо ви хочете, щоб тип надав вашим об'єктивам абстрактно) і позбавляв вас Category
примірника лінз, що дозволить вам скласти їх .
. Реалізація також вимагає класів типів з декількома параметрами.
Зауважте, всі інші бібліотеки об'єктивів, згадані тут, забезпечують деякий комбінатор або можуть бути використані для надання цього самого ефекту фокалізації стану, тому нічого не можна отримати, кодуючи ваш об'єктив безпосередньо таким чином.
Крім того, зазначені на початку побічні умови насправді не мають приємного вираження в цій формі. Як і у випадку з "fclabels", це дає метод шаблону haskell для автоматичного генерування об'єктивів для типу запису безпосередньо в основному пакеті.
Через відсутність Category
примірника, кодування в стилі бароко та вимога шаблону-haskell в основному пакеті, це моя найменш улюблена реалізація.
даних-об'єктив
[Редагувати: Станом на 1.8.0 вони перемістилися з пакету комонад-трансформаторів до об'єктива даних]
Мій data-lens
пакет пропонує лінзи з точки зору магазину Comonad.
newtype Lens a b = Lens (a -> Store b a)
де
data Store b a = Store (b -> a) b
Розгорнуте це рівнозначно
newtype Lens a b = Lens (a -> (b, b -> a))
Ви можете розглядати це як факторинг загального аргументу від геттера та сеттера для повернення пари, що складається з результату вилучення елемента, та сеттера, щоб повернути нове значення. Це пропонує обчислювальну вигоду, що "сеттер" тут можна переробити частину робіт, які використовуються для виведення значення, зробивши для цього більш ефективну операцію "модифікувати", ніж у fclabels
визначенні, особливо коли ланцюги приєднань.
Існує також приємне теоретичне обгрунтування цього уявлення, оскільки підмножина значень об'єктива, що відповідає 3 законам, викладеним на початку цієї відповіді, є саме тими лінзами, для яких загорнута функція є «комонадною вугільною когеброю» для комонаду магазину . Це перетворює 3 волохаті закони для лінзи l
до 2 чудово точкових еквівалентів:
extract . l = id
duplicate . l = fmap l . l
Цей підхід був вперше відзначений і описаний в Russell О'Коннор Functor
є , Lens
як Applicative
це Biplate
: Вводячи багатодискове і писав про на основі препринт Джеремі Gibbons.
Він також включає ряд комбінаторів для суворої роботи з лінзами та деякі лінзи для контейнерів, наприклад Data.Map
.
Таким чином, лінзи у data-lens
формі Category
(на відміну від lenses
пакета) - Haskell 98 (на відміну від fclabels
/ lenses
), є здоровими (на відміну від заднього кінця data-accessor
) та забезпечують дещо ефективнішу реалізацію, data-lens-fd
забезпечують функціональність для роботи з MonadState для тих, хто хоче вийти назовні від Haskell 98, і техніка шаблону haskell тепер доступна через data-lens-template
.
Оновлення 28.06.2012: Інші стратегії впровадження об'єктива
Лінзи ізоморфізму
Є ще два кодування об'єктива, які варто врахувати. Перший дає приємний теоретичний спосіб розглянути лінзу як спосіб розбити структуру на значення поля та "все інше".
Дано тип ізоморфізмів
data Iso a b = Iso { hither :: a -> b, yon :: b -> a }
таким, що дійсні члени задовольняють hither . yon = id
, таyon . hither = id
Ми можемо представити лінзу за допомогою:
data Lens a b = forall c. Lens (Iso a (b,c))
Вони в першу чергу корисні як спосіб подумати про значення лінз, і ми можемо використовувати їх як інструмент міркування для пояснення інших лінз.
лінзи ван Лаарховена
Ми можемо моделювати лінзи таким чином, що вони можуть бути складені з (.)
і id
навіть без Category
наприклад , з використанням
type Lens a b = forall f. Functor f => (b -> f b) -> a -> f a
як тип для наших лінз.
Тоді визначити об'єктив так само просто, як:
_2 f (a,b) = (,) a <$> f b
і ви можете переконатись, що композиція функції - це лінзова композиція.
Нещодавно я писав про те, як можна додатково узагальнити лінзи ван Лаарховена, щоб отримати сімейства лінз, які можуть змінювати типи полів, просто узагальнивши цей підпис до
type LensFamily a b c d = forall f. Functor f => (c -> f d) -> a -> f b
Це має невдалий наслідок того, що найкращий спосіб говорити про лінзи - це використовувати поліморфізм 2-го рангу, однак вам не потрібно використовувати цю підпис безпосередньо при визначенні лінз.
Lens
Я визначив вище _2
насправді LensFamily
.
_2 :: Functor f => (a -> f b) -> (c,a) -> f (c, b)
Я написав бібліотеку, яка включає лінзи, сімейство лінз та інші узагальнення, включаючи геттери, сетери, складки та обходи. Він доступний при злому як lens
пакет.
Знову ж таки, велика перевага цього підходу полягає в тому, що технічні працівники бібліотеки можуть фактично створювати об'єктиви в такому стилі у ваших бібліотеках, не несучи ніякої залежності від бібліотеки об'єктивів, просто надаючи функції типу Functor f => (b -> f b) -> a -> f a
, для їх конкретних типів 'a' та 'b'. Це значно знижує вартість усиновлення.
Оскільки вам не потрібно використовувати пакет для визначення нових лінз, це потребує великого тиску з моїх попередніх проблем щодо збереження бібліотеки Haskell 98.
lens
пакет має найбагатший функціонал та документацію, тому якщо ви не заперечуєте проти його складності та залежностей, це шлях.