Ответ 1
Clojure assoc-in
позволяет указать путь через вложенные стропы данных с использованием целых чисел и ключевых слов и ввести новое значение на этом пути. Он имеет партнеров dissoc-in
, get-in
и update-in
, которые удаляют элементы, получают их без удаления или изменяют соответственно.
Линзы - это особое понятие двунаправленного программирования, в котором вы указываете связь между двумя источниками данных, и эта привязка позволяет отражать преобразования от одного к другому. В Haskell это означает, что вы можете создавать линзы или линзоподобные значения, которые соединяют целую структуру данных с некоторыми ее частями, а затем используют их для передачи изменений от частей к целому.
Здесь есть аналогия. Если мы посмотрим на использование assoc-in
, это будет написано как
(assoc-in whole path subpart)
и мы могли бы получить некоторое представление, думая о path
как объективе и assoc-in
как комбинатор линз. Аналогичным образом вы можете написать (используя пакет Haskell lens
)
set lens subpart whole
чтобы мы соединили assoc-in
с set
и path
с lens
. Мы также можем заполнить таблицу
set assoc-in
view get-in
over update-in
(unneeded) dissoc-in -- this is special because `at` and `over`
-- strictly generalize dissoc-in
Это начало для сходства, но там тоже огромное несходство. Во многом, lens
гораздо более общий, чем *-in
семейство функций Clojure. Обычно это не проблема для Clojure, потому что большинство данных Clojure хранится в вложенных структурах, состоящих из списков и словарей. Haskell использует множество других пользовательских типов очень свободно, и система их типов отражает информацию о них. Объективы обобщают семейство функций *-in
, потому что они работают плавно над гораздо более сложным доменом.
Во-первых, вставьте типы Clojure в Haskell и напишите семейство функций *-in
.
type Dict a = Map String a
data Clj
= CljVal -- Dynamically typed Clojure value,
-- not an array or dictionary
| CljAry [Clj] -- Array of Clojure types
| CljDict (Dict Clj) -- Dictionary of Clojure types
makePrisms ''Clj
Теперь мы можем использовать set
как assoc-in
почти напрямую.
(assoc-in whole [1 :foo :bar 3] part)
set ( _CljAry . ix 1
. _CljDict . ix "foo"
. _CljDict . ix "bar"
. _CljAry . ix 3
) part whole
У этого, очевидно, намного больше синтаксического шума, но он означает более высокую степень ясности относительно того, что означает "путь" в тип данных, в частности, это означает, что мы спускаемся в массив или словарь. Мы могли бы, если бы захотели, устранить некоторые из этого дополнительного шума, создав экземпляр Clj
в классной строке Haskell Ixed
, но вряд ли это стоит на этом этапе.
Вместо этого необходимо указать, что assoc-in
применяется к очень определенному типу спуска данных. Это более общее, чем типы, которые я изложил выше из-за Clojure динамической типизации и перегрузки IFn
, но очень похожая фиксированная структура вроде этого может быть встроена в Haskell с небольшими дополнительными усилиями.
Линзы могут идти намного дальше, но, и делают это с большей безопасностью типа. Например, приведенный выше пример на самом деле не является истинным "объективом", а вместо него "Призма" или "Траверс", который позволяет системе типов статически идентифицировать возможность отказа от этого обхода. Это заставит нас думать об ошибках, подобных этому (даже если мы решим игнорировать их).
Важно, что это означает, что мы можем быть уверены, что когда у нас есть настоящий объектив, который сбой типа данных не может потерпеть неудачу, такую гарантию невозможно сделать в Clojure.
Мы можем определять пользовательские типы данных и создавать пользовательские линзы, которые спускаются в них по типу.
data Point =
Point { _latitude :: Double
, _longitude :: Double
, _meta :: Map String String }
deriving Show
makeLenses ''Point
> let p0 = Point 0 0
> let p1 = set latitude 3 p0
> view latitude p1
3.0
> view longitude p1
0.0
> let p2 = set (meta . ix "foo") "bar" p1
> preview (meta . ix "bar") p2
Nothing
> preview (meta . ix "foo") p2
Just "bar"
Мы также можем обобщить на Lenses (действительно Traversals), которые одновременно нацелены на несколько одинаковых подчасти
dimensions :: Lens Point Double
> let p3 = over dimensions (+ 10) p0
> get latitude p3
10.0
> get longitude p3
10.0
> toListOf dimensions p3
[10.0, 10.0]
Или даже целевые моделированные подчасти, которые на самом деле не существуют, но все равно образуют эквивалентное описание наших данных.
eulerAnglePhi :: Lens Point Double
eulerAngleTheta :: Lens Point Double
eulerAnglePsi :: Lens Point Double
В широком смысле объективы обобщают вид взаимодействия на основе пути между целыми значениями и подчастими значений, которые абстрактно абстрактны. Clojure *-in
. Вы можете сделать намного больше в Haskell, потому что у Haskell гораздо более развитое понятие типов и объективов, как объектов первого класса, широко обобщают понятия получения и настройки, которые просто представлены функциями *-in
.