Поиск…


Вступление

Объект представляет собой библиотеку для Haskell, которая предоставляет объективы, изоморфизмы, складки, обходы, геттеры и сеттеры, которые предоставляют единый интерфейс для запросов и манипулирования произвольными структурами, в отличие от концепций аксессуаров Java и мутаторов.

замечания

Что такое объектив?

Объективы (и другая оптика) позволяют нам разделить описание того, как мы хотим получить доступ к некоторым данным из того, что мы хотим с ним делать. Важно различать абстрактное понятие объектива и конкретную реализацию. Понимание абстрактно делает программирование с lens намного проще в долгосрочной перспективе. Существует много изоморфных представлений линз, поэтому для этого обсуждения мы избегаем какого-либо конкретного обсуждения реализации и вместо этого дадим обзор концепций на высоком уровне.

фокусирование

Важной концепцией в абстрактном понимании является понятие фокусировки . Важная оптика фокусируется на определенной части большей структуры данных, не забывая о более широком контексте. Например, объектив _1 фокусируется на первом элементе кортежа, но не забывает о том, что было во втором поле.

После того, как мы сосредоточимся, мы можем поговорить о том, какие операции нам позволяют выполнять с объективом. Учитывая объект Lens sa который при заданном типе типа s фокусируется на конкретном a , мы можем либо

  1. Извлеките a , забыв о дополнительном контексте или
  2. Замените a , предоставив новое значение

Они соответствуют хорошо известным операциям get и set которые обычно используются для характеристики объектива.

Другие оптика

Аналогичным образом мы можем говорить о других оптиках.

оптический Фокусируется на...
объектив Одна часть продукта
призма Одна часть суммы
пересечение Нулевые или более части структуры данных
изоморфизм ...

Каждая оптика фокусируется по-другому, как таковая, в зависимости от того, какой тип оптики у нас есть, мы можем выполнять разные операции.

Состав

Более того, мы можем составить любую из двух опций, которые мы так обсуждали, чтобы определить сложные обращения к данным. Четыре типа оптики, которые мы обсуждали, образуют решетку, результатом объединения двух оптических элементов является их верхняя граница.

введите описание изображения здесь

Например, если мы собрали вместе линзу и призму, мы получим обход. Причиной этого является то, что по их (вертикальной) композиции мы сначала фокусируемся на одной части продукта, а затем на одной части суммы. Результатом является оптическая система, которая фокусируется на нулевой или одной части наших данных, которая является частным случаем обхода. (Это также иногда называют аффинным обходом).

В Хаскелле

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

Манипулирование кортежами с помощью объектива

Получение

("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1

настройка

("a", 1) & _1 .~ "b" -- returns ("b", 1)

Изменение

("a", 1) & _2 %~ (+1) -- returns ("a", 2)

both обхода

(1, 2) & both *~ 2 -- returns (2, 4)

Линзы для записей

Простая запись

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data Point = Point {
    _x :: Float,
    _y :: Float
}
makeLenses ''Point

Создаются объективы x и y .

let p = Point 5.0 6.0 
p ^. x     -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }

Управление записями с именами повторяющихся полей

data Person = Person { _personName :: String }
makeFields ''Person

Создает класс HasName класса, name объектива для Person и делает Person экземпляром HasName . Последующие записи также будут добавлены в класс:

data Entity = Entity { _entityName :: String }
makeFields ''Entity

Расширение шаблона Haskell требуется для работы makeFields . Технически, вполне возможно создать линзы, сделанные таким образом с помощью других средств, например, вручную.

Головные линзы

Операторы объектива имеют полезные варианты, которые работают в контекстах состояния. Они получаются заменой ~ на = в имени оператора.

(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()

Примечание. Варианты с сохранением состояния не должны изменять тип, поэтому у них есть подписи Lens' или Simple Lens' .

Избавление от & цепочек

Если операции с объективом необходимо сковать, он часто выглядит так:

change :: A -> A
change a = a & lensA %~ operationA
             & lensB %~ operationB
             & lensC %~ operationC

Это работает благодаря ассоциативности & . Однако версия с сохранением состояния более ясна.

change a = flip execState a $ do
    lensA %= operationA
    lensB %= operationB
    lensC %= operationC

Если lensX фактически является id , вся операция, конечно, может выполняться напрямую, просто поднимите ее с modify .

Императивный код со структурированным состоянием

Предположим, что этот пример:

data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }

makeLenses ''Point
makeLenses ''Entity
makeLenses ''World

Мы можем написать код, похожий на классические императивные языки, но при этом позволяя нам использовать преимущества Haskell:

updateWorld :: MonadState World m => m ()
updateWorld = do
    -- move the first entity
    entities . ix 0 . position . x += 1

    -- do some operation on all of them
    entities . traversed . position %= \p -> p `pointAdd` ...

    -- or only on a subset
    entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...

Написание объектива без шаблона Haskell

Чтобы демистифицировать шаблон Haskell, предположим, что у вас есть

data Example a = Example { _foo :: Int, _bar :: a }

затем

makeLenses 'Example

производит (более или менее)

foo :: Lens' (Example a) Int
bar :: Lens (Example a) (Example b) a b

Тем не менее, ничего особенного не происходит. Вы можете написать их сами:

foo :: Lens' (Example a) Int
--  :: Functor f => (Int -> f Int) -> (Example a -> f (Example a))    ;; expand the alias
foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo)

bar :: Lens (Example a) (Example b) a b
--  :: Functor f => (a -> f b) -> (Example a -> f (Example b))    ;; expand the alias
bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)

По сути, вы хотите «посетить» ваш объектив «фокус» с помощью функции wrap а затем перестроить «весь» тип.

Объективы и призмы

Lens' sa означает, что вы всегда можете найти a в любом s . А Prism' sa означает , что иногда вы можете обнаружить , что s на самом деле просто но иногда это что - то другое. a

Чтобы быть более понятным, мы имеем _1 :: Lens' (a, b) a потому что у любого набора всегда есть первый элемент. У нас есть _Just :: Prism' (Maybe a) a , Maybe a a Just Nothing _Just :: Prism' (Maybe a) a , потому что иногда Maybe a на самом деле является значение , завернутые в Just , но иногда это не Nothing .

С этой интуицией некоторые стандартные комбинаторы можно интерпретировать параллельно друг другу

  • view :: Lens' sa -> (s -> a) "получает" a из s
  • set :: Lens' sa -> (a -> s -> s) "наборы" в a слот s
  • review :: Prism' sa -> (a -> s) «понимает», что a может быть s
  • preview :: Prism' sa -> (s -> Maybe a) «пытается» превратить s в a .

Другой способ подумать о том, что значение типа Lens' sa показывает, что s имеет ту же структуру, что и (r, a) для некоторого неизвестного r . С другой стороны, Prism' sa показывает, что s имеет ту же структуру, что и Either ra для некоторого r . Мы можем написать эти четыре функции выше с этими знаниями:

-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)`

view :: (r, a) -> a
view (r, a) = a

set :: a -> (r, a) -> (r, a)
set a (r, _) = (r, a)

-- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a`

review :: a -> Either r a
review a = Right a

preview :: Either r a -> Maybe a
preview (Left _) = Nothing
preview (Right a) = Just a

обходов

Traversal' sa показывает, что s имеет 0-to-many a s внутри него.

toListOf :: Traversal' s a -> (s -> [a])

Любой тип t который является Traversable автоматически имеет этот traverse :: Traversal (ta) a .

Мы можем использовать Traversal , чтобы установить или отобразить по всем из них a значения

> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]

> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]

f :: Lens' sa говорит , что именно один внутри a s . A g :: Prism' ab говорит, что в a есть 0 или 1 b s. Составление f . g дает нам Traversal' sb потому что после f а затем g показывает, как там есть 0-to-1 b s в s .

Линзы составляют

Если у вас есть f :: Lens' ab и g :: Lens' bc то f . g - это Lens' ac полученная следующим образом f а затем g . В частности:

  • Линзы представляют собой функции (на самом деле они просто являются функциями)
  • Если вы думаете о view функциональности Lens , похоже , потоки данных «слева направо» -за может чувствовать назад к нормальной интуиции для композиции функций. С другой стороны, это должно быть естественно, если вы думаете . -нотация, как это происходит на языках OO.

Более чем просто компоновка Lens с Lens (.) Можно использовать для создания почти любого типа типа « Lens ». Не всегда легко понять, что результат, так как тип становится более жестким, но вы можете использовать диаграмму lens чтобы понять это. Композиция x . y имеет тип наименьшей-верхней границы типов x и y в этой диаграмме.

Классные линзы

В дополнение к стандартной функции makeLenses для генерации Lens es Control.Lens.TH также предлагает функцию makeClassy . makeClassy имеет тот же тип и работает по существу так же, как makeLenses , с одним ключевым отличием. В дополнение к созданию стандартных объективов и обходов, если тип не имеет аргументов, он также создаст класс, описывающий все типы данных, которые обладают типом в качестве поля. Например

data Foo = Foo { _fooX, _fooY :: Int }
  makeClassy ''Foo

создаст

class HasFoo t where
   foo :: Simple Lens t Foo

instance HasFoo Foo where foo = id

fooX, fooY :: HasFoo t => Simple Lens t Int

Поля с makeFields

(Этот пример скопирован из этого ответа StackOverflow )

Допустим, у вас есть несколько разных типов данных, которые должны иметь объектив с таким же названием, в этом случае capacity . makeFields создаст класс, который выполнит это без конфликтов пространства имен.

{-# LANGUAGE FunctionalDependencies
           , MultiParamTypeClasses
           , TemplateHaskell
  #-}

module Foo
where

import Control.Lens

data Foo
  = Foo { fooCapacity :: Int }
  deriving (Eq, Show)
$(makeFields ''Foo)

data Bar
  = Bar { barCapacity :: Double }
  deriving (Eq, Show)
$(makeFields ''Bar)

Тогда в ghci:

*Foo
λ let f = Foo 3
|     b = Bar 7
| 
b :: Bar
f :: Foo

*Foo
λ fooCapacity f
3
it :: Int

*Foo
λ barCapacity b
7.0
it :: Double

*Foo
λ f ^. capacity
3
it :: Int

*Foo
λ b ^. capacity
7.0
it :: Double

λ :info HasCapacity 
class HasCapacity s a | s -> a where
  capacity :: Lens' s a
    -- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3

Итак, что на самом деле сделано, объявлен класс HasCapacity sa , где емкость - это Lens' от s до a ( a фиксируется, как только s известен). Он определил имя «емкость», сняв с поля (с нижней) имя типа данных из поля; Мне приятно не использовать знак подчеркивания ни имени поля, ни имени объектива, поскольку иногда синтаксис записи на самом деле является тем, что вы хотите. Вы можете использовать makeFieldsWith и различные lensRules, чтобы иметь несколько разных опций для расчета имен объективов.

В случае, если это помогает, используя ghci -ddump-сращивания Foo.hs:

[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
    makeFields ''Foo
  ======>
    class HasCapacity s a | s -> a where
      capacity :: Lens' s a
    instance HasCapacity Foo Int where
      {-# INLINE capacity #-}
      capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
    makeFields ''Bar
  ======>
    instance HasCapacity Bar Double where
      {-# INLINE capacity #-}
      capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.

Итак, первый сплайс сделал класс HasCapcity и добавил экземпляр для Foo; второй использовал существующий класс и сделал экземпляр для Bar.

Это также работает, если вы импортируете класс HasCapcity из другого модуля; makeFields может добавлять дополнительные экземпляры в существующий класс и распространять ваши типы через несколько модулей. Но если вы снова используете его в другом модуле, где вы не импортировали класс, он будет создавать новый класс (с тем же именем), и у вас будет две отдельные перегруженные объективы, которые несовместимы.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow