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