Haskell Language
объектив
Поиск…
Вступление
Объект представляет собой библиотеку для Haskell, которая предоставляет объективы, изоморфизмы, складки, обходы, геттеры и сеттеры, которые предоставляют единый интерфейс для запросов и манипулирования произвольными структурами, в отличие от концепций аксессуаров Java и мутаторов.
замечания
Что такое объектив?
Объективы (и другая оптика) позволяют нам разделить описание того, как мы хотим получить доступ к некоторым данным из того, что мы хотим с ним делать. Важно различать абстрактное понятие объектива и конкретную реализацию. Понимание абстрактно делает программирование с lens
намного проще в долгосрочной перспективе. Существует много изоморфных представлений линз, поэтому для этого обсуждения мы избегаем какого-либо конкретного обсуждения реализации и вместо этого дадим обзор концепций на высоком уровне.
фокусирование
Важной концепцией в абстрактном понимании является понятие фокусировки . Важная оптика фокусируется на определенной части большей структуры данных, не забывая о более широком контексте. Например, объектив _1
фокусируется на первом элементе кортежа, но не забывает о том, что было во втором поле.
После того, как мы сосредоточимся, мы можем поговорить о том, какие операции нам позволяют выполнять с объективом. Учитывая объект Lens sa
который при заданном типе типа s
фокусируется на конкретном a
, мы можем либо
- Извлеките
a
, забыв о дополнительном контексте или - Замените
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
может добавлять дополнительные экземпляры в существующий класс и распространять ваши типы через несколько модулей. Но если вы снова используете его в другом модуле, где вы не импортировали класс, он будет создавать новый класс (с тем же именем), и у вас будет две отдельные перегруженные объективы, которые несовместимы.