Подтвердить что ты не робот

При манипулировании неизменяемыми структурами данных, какая разница между объектами Clojure и объективами Haskell?

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

Clojure assoc-in
Haskell lens

4b9b3361

Ответ 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.

Ответ 2

Вы говорите о двух разных вещах.

Вы можете использовать объектив для решения аналогичных проблем, таких как ассоциативный, где вы используете типы коллекций (Data.Map, Data.Vector), которые соответствуют семантике, но есть различия.

В нетипизированных языках, таких как Clojure, он обычно структурирует данные вашего домена в терминах коллекций, которые содержат нестатические данные (хеш-карты, векторы и т.д.), даже когда он моделирует данные, которые обычно статичны.

В Haskell вы структурируете свои данные с помощью записи и ADT, где, когда вы можете выражать содержимое, которое может или не может существовать (или обернуть коллекцию), по умолчанию это статически известное содержимое.

В одной библиотеке будет http://hackage.haskell.org/package/lens-aeson, где у вас есть документы JSON, которые могут иметь различное содержимое.

Примеры показывают, что когда ваш путь и тип не соответствуют структуре/данным, он выдает Nothing вместо Just a.

Объектив не делает ничего, кроме обеспечения поведения звукового геттера/сеттера. Он не выражает особого ожидания относительно того, как выглядят ваши данные, тогда как ассоциативный смысл имеет смысл только с ассоциативными коллекциями с возможно неопределенным содержанием.

Еще одно отличие здесь - чистота и лень против строгой и нечистой семантики. В Haskell, если вы никогда не использовали "более старые" состояния и только самые последние, тогда будет реализовано только это значение.

tl; dr линзы, найденные в Lens, а другие подобные библиотеки более общие, более полезные, безопасные для типов и особенно приятные в ленивых/чистых языках FP.

Ответ 3

assoc-in может быть более универсальным, чем lens в некоторых случаях, поскольку он может создавать уровни в структуре, если они не существуют.

lens предлагает Folds, который разрушает структуру и возвращает сводку содержащихся значений и Traversals, которые изменяют элементы в структуре (возможно, нацеливая сразу несколько элементов, возможно ничего не делая, если целевой элемент (-ых) нет) при сохранении общей "формы" структуры. Но я думаю, что создать промежуточные уровни с помощью lens будет сложно.

Другое отличие, которое я вижу с assoc-in -подобными функциями в Clojure, состоит в том, что они, похоже, касаются только получения и установки значений, в то время как само определение объектива поддерживает "выполнение чего-то со значением", что что-то, возможно, связано с побочными эффектами.

Например, предположим, что у нас есть кортеж (1,Right "ab"). Второй компонент - тип суммы, который может содержать строку. Мы хотим изменить первый символ строки, прочитав ее с консоли. Это можно сделать с помощью линз следующим образом:

(_2._Right._Cons._1) (\_ -> getChar) (1,Right "ab")
-- reads char from console and returns the updated structure

Если строка отсутствует или пуста, ничего не делается:

(_2._Right._Cons._1) (\_ -> getChar) (1,Left 5)
-- nothing read

(_2._Right._Cons._1) (\_ -> getChar) (1,Right "")
-- nothing read

Ответ 4

Этот вопрос несколько аналогичен заданию разницы между Clojure for и монадами Хаскелла. Я буду подражать ответам до сих пор: уверен, что for похож на монаду List, но монады являются гораздо более родовыми и мощными.

Но это немного глупо, не так ли? Монады были реализованы в Clojure. Почему они не используются все время? Clojure имеет в своей основе другую философию о том, как справляться с состоянием, но по-прежнему чувствует себя свободной, чтобы заимствовать хорошие идеи с таких больших языков, как Haskell в своих библиотеках.

Итак, конечно, assoc-in, get-in, update-in и т.д. - это своего рода линзы для ассоциативных структур данных. И есть реализации линз вообще в Clojure там. Почему они не используются все время? Это разница в философии (и, возможно, жуткое чувство, что со всеми сеттерами и геттерами мы будем создавать другую Java внутри Clojure и как-то в конечном итоге жениться на нашей матери). Но, Clojure чувствует себя свободно, чтобы заимствовать хорошие идеи, и вы можете увидеть подходы, основанные на объективе, которые пробиваются в классные проекты, такие как Om и Enliven.

Вы должны быть осторожны, задавая такие вопросы, потому что, как и половинки братьев и сестер, которые занимают какое-то одно и то же пространство Clojure и Haskell, обязательно будут заимствовать друг у друга и немного ссориться о том, кто прав.