Haskell использует линзы первого уровня для создания сложных линз
Допустим, у меня есть объект с двумя полями:
data Example = Example { _position :: Int
, _storage :: [Int]}
как мне построить линзу, которая фокусируется на position
элемент внутри storage
?
Кроме того, будет ли возможно ограничить position
значения, изменяемые с помощью линз, в диапазоне, основанном на storage
размер?
Кажется, что рядом можно было бы использовать как-то, так как Example
изоморфен кортежу, но я не могу понять, как это сделать.
Я не уверен, как сформулировать вопрос, поэтому я не смог найти много соответствующей информации.
3 ответа
Изменить: я неправильно понял проблему, оригинальный ответ следует в конце.
Я не знаю ни одного комбинатора, который делает то, что вы хотите, поэтому я написал один.
(^>>=) :: Lens' s a -> (a -> Lens' s b) -> Lens' s b
-- Lens' s a -> (a -> (b -> f b) -> s -> f s) -> (b -> f b) -> s -> f s
-- (That previous line disregards a forall and the Functor constraints)
(x ^>>= f) btofb s = f (s ^. x) btofb s
Если вы оставите сигнатуру типа и не запросите ghci, это даст нам наиболее общий пример, так что вот так:
:t (^>>=)
Getting a s a -> (a -> t1 -> s -> t) -> t1 -> s -> t
Документ для получения: "Когда вы видите это в сигнатуре типа, это означает, что вы можете передать функцию Lens, Getter, Traversal, Fold, Prism, Iso или один из проиндексированных вариантов, и он просто" сделает правильную вещь " ".
Правая сторона аналогична общей, позволяя обходы / призмы / и т.д..
Обратите внимание, что это создает законные линзы, только если указатель не на себя.
Теперь, чтобы применить этот комбинатор - состав, который вы хотели это:
position ^>>= \p -> storage . ix p
Это получается Обход, см. Оригинальный ответ.
Или, используя другой комбинатор, который мне нравится:
let (f .: g) x = f . g x in position ^>>= (storage .: ix)
Любой с некоторыми объявлениями инфиксов вы могли бы даже избавиться от этих скобок.
(Этот оригинальный ответ предполагает position :: Int
локально затеняет положение линзы.)
Мы не знаем, имеет ли список значение в этой позиции, так что это не "Объектив", а "Обход", что означает "обход любого количества значений", а не "линзирование на одно значение".
storage . ix position :: Traversal' Example Int
(^?) вернет первое пройденное значение, если оно есть, и, таким образом, этот термин даст вам Int, если эта позиция верна, или Nothing, если это не так.
(^? storage . ix position) :: Example -> Maybe Int
Эта частичная версия предполагает, что позиция действительна, и сбой, если это не так.
(^?! storage . ix position) :: Example -> Int
(% ~), который применяет функцию справа ко всему пройденному слева, работает не только для линз, но и для всех обходов. (Каждая Линза является Обходом с помощью хитрой обманчивой хитрости и может быть вставлена в любое место, куда может пойти Обход.)
storage . ix position %~ (+1) :: Example -> Example
И если вам абсолютно необходимо работать с объективом, любой из этих частичных условий потерпит крах, если вы попытаетесь применить их в недопустимых положениях.
singular $ storage . ix position :: Lens' Example Int
storage . singular (ix position) :: Lens' Example Int
PS: Ваша запись выглядит так, как будто вы хотите использовать вместо этого застежки-молнии: если вы будете двигаться вперед / назад только постепенно, вы будете делать менее вонючие (!!) вещи, если будете отслеживать список значений слева от вашей текущей позиции, значение в вашей текущей позиции и список значений справа от вашей текущей позиции, а не список всех значений и вашей позиции в нем. Для большего удовольствия, посмотрите Control.Lens.Zipper, но они оптимизированы для изящного вложения нескольких уровней застежки-молнии.
Кажется, что самый простой способ добиться этого - написать геттер и сеттер с использованием линз, а затем составить линзу:
at_position :: Functor f => (Int -> f Int) -> Example -> f Example
at_position = lens get set
where
get :: Example -> Int
get e = fromJust $ e ^? storage . ix (e^.position)
set :: Example -> Int -> Example
set e v = e & storage . ix (e^.position) .~ v
хотя это может быть улучшено, но код достаточно ясен и не ограничивается структурой объекта.
Я думаю, что призмы должны быть лучше для этой ситуации, так как pos
может быть целым числом, не превышающим длину вашего списка, или отрицательным.
Я думаю, что вы могли бы использовать что-то вроде документа для призм предоставить
nat :: Prism' Integer Natural nat = prism toInteger $ \ i -> if i < 0 then Left i else Right (fromInteger i)
storageAtPos Prism' Example Int
storageAtPos = prism $ aux
where aux (Example p s) | p < 0 || p >= length s = Nothing
| otherwise = Just (s !! p)
примечание: я не запускал этот код - просто сделал аналог к документу (сейчас у меня нет ghc)
ОБНОВИТЬ
Может быть что-то вроде
storageAtPos = \p -> (p^.storage)^?(ix $ p^.pos)
работает, но опять же - сейчас у меня нет ghc для тестирования - как @Gurkenglas указал, что это не Prism