Когато манипулирате неизменни структури от данни, каква е разликата между асоциацията на Clojure и лещите на Haskell?

Трябва да манипулирам и модифицирам дълбоко вложени неизменни колекции (карти и списъци) и бих искал да разбера по-добре различните подходи. Тези две библиотеки решават повече или по-малко един и същ проблем, нали? По какво се различават, за какви видове проблеми единият подход е по-подходящ от другия?

assoc-in на Clojure
lens на Haskell


person Dustin Getz    schedule 22.01.2014    source източник


Отговори (4)


assoc-in на Clojure ви позволява да посочите път през вложена структура от данни, като използвате цели числа и ключови думи и да въведете нова стойност в този път. Той има партньори 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"

Можем също така да обобщим за лещи (всъщност 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

Най-общо казано, Lenses обобщават вида базирано на пътя взаимодействие между цели стойности и подчасти от стойности, които фамилията функции Clojure *-in абстрахира. Можете да направите много повече в Haskell, защото Haskell има много по-развито понятие за типове и лещи, като първокласни обекти, широко обобщаващи понятията за получаване и настройка, които са просто представени с *-in функциите.

person J. Abrahamson    schedule 22.01.2014

Говорите за две много различни неща.

Можете да използвате lens за решаване на подобни проблеми като assoc-in, където използвате типове колекции (Data.Map, Data.Vector), които съответстват на семантиката, но има разлики.

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

В Haskell бихте структурирали данните си с помощта на запис и ADT, където докато можете да изразите съдържание, което може или не може да съществува (или да обвиете колекция), по подразбиране е статично известно съдържание.

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

Примерите показват, че когато вашият път и тип не съвпадат със структурата/данните, той извежда Nothing вместо Just a.

Lens не прави нищо друго освен осигуряване на добро поведение при получаване/настройване. Той не изразява конкретни очаквания за това как изглеждат вашите данни, докато assoc-in има смисъл само с асоциативни колекции с евентуално недетерминирано съдържание.

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

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

person bitemyapp    schedule 22.01.2014

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
person danidiaz    schedule 22.01.2014

Този въпрос е донякъде аналогичен на въпроса каква е разликата между for на Clojure и монадите на Haskell. Ще имитирам отговорите досега: със сигурност for е нещо като List монада, но монадите са много по-общи и мощни.

Но това е малко глупаво, нали? Монадите са внедрени в Clojure. Защо не се използват през цялото време? Clojure има в основата си различна философия за това как да се справя със състоянието, но все пак се чувства свободен да заема добри идеи от страхотни езици като Haskell в своите библиотеки.

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

Трябва да внимавате да задавате подобни въпроси, защото като полубратя и сестри, които заемат част от едно и също пространство, Clojure и Haskell са длъжни да заемат един от друг и да се карат малко кой е прав.

person A. Webb    schedule 23.01.2014
comment
по какъв начин Ом използва лещи? Въпросът ми е в по-широкия контекст на използването на React за чисто DOM изобразяване с много сложно състояние, държано в атом в горната част на приложението. - person Dustin Getz; 23.01.2014
comment
@DustinGetz Вижте концептуалния преглед на Om. - person A. Webb; 23.01.2014
comment
Не съм сигурен дали мога да твърдя, че лещите и курсорите са толкова сходни. Те обаче решават подобен проблем. - person J. Abrahamson; 24.01.2014
comment
Лещи и монади без типове и типови класове е лудост. Аз съм опитен програмист на Clojure и не бих се занимавал с това. - person bitemyapp; 24.01.2014
comment
Омекотени до подходи, вдъхновени от обектива, ако желаете. - person A. Webb; 24.01.2014