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

Другие вопросы по тегам