Как избежать возвращаемого значения по умолчанию при доступе к несуществующему полю с объективами?

Мне нравится библиотека Lens, и мне нравится, как она работает, но иногда она вводит столько проблем, что я сожалею, что когда-либо начал ее использовать. Давайте рассмотрим этот простой пример:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Data = A { _x :: String, _y :: String }
          | B { _x :: String }

makeLenses ''Data

main = do
    let b = B "x"    
    print $ view y b

выводится:

""

А теперь представьте себе - у нас есть тип данных, и мы реорганизуем его - путем изменения некоторых имен. Вместо того, чтобы получать ошибку (во время выполнения, например, с обычными аксессуарами), что это имя больше не применяется к конкретному конструктору данных, объективы используют mempty из Monoid для создания объекта по умолчанию, поэтому мы получаем странные результаты вместо ошибки. Отладка чего-то подобного почти невозможна. Есть ли способ исправить это поведение? Я знаю, что есть некоторые специальные операторы, чтобы получить поведение, которое я хочу, но все "нормальные" функции от объективов просто ужасны. Должен ли я просто переопределять их с помощью моего настраиваемого модуля или есть ли более удобный метод?

В качестве побочного элемента: я хочу иметь возможность читать и устанавливать аргументы с использованием синтаксиса объектива, но просто удалять поведение автоматического создания результата, когда поле отсутствует.

Ответ 1

Похоже, вы просто хотите восстановить поведение исключения. Я смутно помню, что это то, как однажды разобрался вид. Если это так, я ожидаю, что с изменением будет сделан разумный выбор.

Обычно я заканчиваю работу с (^?) в тех случаях, о которых вы говорите:

> b ^? y
Nothing

Если вы хотите использовать поведение исключения, вы можете использовать ^?!

> b ^?! y
"*** Exception: (^?!): empty Fold

Я предпочитаю использовать ^?, чтобы избежать частичных функций и исключений, подобно тому, как обычно рекомендуется избегать head, last, !! и других частичных функций.

Ответ 2

Да, я тоже немного странно, что view работает для Traversal, объединяя цели. Я думаю, что это из-за экземпляра Monoid m => Applicative (Const m). Вы можете написать собственный эквивалент view, который не имеет такого поведения, написав собственный эквивалент Const, который не имеет этого экземпляра.

Возможно, одним из способов было бы обеспечить подпись типа для y, поэтому знаете точно, что это такое. Если у вас это было, ваше "патологическое" использование view не будет компилироваться.

data Data = A { _x :: String, _y' :: String }
          | B { _x :: String }

makeLenses ''Data

y :: Lens' Data String
y = y'

Ответ 3

Вы можете сделать это, указав свой собственный оператор view1. Он не существует в пакете lens, но его легко определить локально.

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Data = A { _x :: String, _y :: String }
          | B { _x :: String }

makeLenses ''Data

newtype Get a b = Get { unGet :: a }

instance Functor (Get a) where
  fmap _ (Get x) = Get x

view1 :: LensLike' (Get a) s a -> s -> a
view1 l = unGet . l Get

works :: Data -> String
works = view1 x

-- fails :: Data -> String
-- fails = view1 y

-- Bug.hs:23:15:
--     No instance for (Control.Applicative.Applicative (Get String))
--       arising from a use of ‘y’