Могу ли я создать объектив с ограничением Монады?

Контекст: Этот вопрос относится конкретно к Control.Lens (версия 3.9.1 на момент написания этой статьи)

Я использую библиотеку объективов, и очень приятно иметь возможность читать и писать кусок (или фрагменты для обхода) структуры. Затем я понял, можно ли использовать объектив для внешней базы данных. Конечно, мне тогда понадобилось бы выполнить в IO Monad. Итак, чтобы обобщить:

Вопрос:

Для получателя, (s -> m a) и setter (b -> s -> m t), где m является Монадой, можно построить Lens s t a b, где теперь находится Функтор объектива, также являющийся Монадой? Можно ли было бы скомпоновать их с помощью (.) с другими "чисто функциональными" объективами?

Пример:

Могу ли я сделать Lens (MVar a) (MVar b) a b с помощью readMVar и withMVar?

Альтернатива:

Существует ли эквивалент Control.Lens для контейнеров в монаде IO, таком как MVar или IORef (или STDIN)?

Ответ 1

Я думал об этой идее в течение некоторого времени, которую я бы назвал изменяемыми объективами. Пока я не сделал это в пакет, сообщите мне, если вы выиграете от этого.

Сначала позвольте вспомнить обобщенные линзы Ван Лаарховена (после некоторого импорта нам понадобится позже):

{-# LANGUAGE RankNTypes #-}
import qualified Data.ByteString as BS
import           Data.Functor.Constant
import           Data.Functor.Identity
import           Data.Traversable (Traversable)
import qualified Data.Traversable as T
import           Control.Monad
import           Control.Monad.STM
import           Control.Concurrent.STM.TVar

type Lens s t a b = forall f . (Functor f) => (a -> f b) -> (s -> f t)
type Lens' s a = Lens s s a a

мы можем создать такую ​​линзу из "геттера" и "сеттера" как

mkLens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
mkLens g s  f x = fmap (s x) (f (g x))

и получить "getter" / "setter" с объектива обратно как

get :: Lens s t a b -> (s -> a)
get l = getConstant . l Constant

set :: Lens s t a b -> (s -> b -> t)
set l x v = runIdentity $ l (const $ Identity v) x

в качестве примера, следующий объектив обращается к первому элементу пары:

_1 :: Lens' (a, b) a
_1 = mkLens fst (\(x, y) x' -> (x', y))
-- or directly: _1 f (a,c) = (\b -> (b,c)) `fmap` f a

Теперь, как работает изменяемый объектив? Получение некоторого содержимого контейнера предполагает монадическое действие. И установка значения не изменяет контейнер, он остается тем же, как и измененная часть памяти. Таким образом, результат изменчивой линзы должен быть монодичным, и вместо контейнера типа возврата t у нас будет только (). Более того, ограничение Functor недостаточно, так как нам нужно чередовать его с монадическими вычислениями. Поэтому нам понадобится Traversable:

type MutableLensM  m s  a b
    = forall f . (Traversable f) => (a -> f b) -> (s -> m (f ()))
type MutableLensM' m s  a
    = MutableLensM m s a a

(Traversable - это монадические вычисления, что Functor - чистое вычисление).

Опять же, мы создаем вспомогательные функции

mkLensM :: (Monad m) => (s -> m a) -> (s -> b -> m ())
        -> MutableLensM m s a b
mkLensM g s  f x = g x >>= T.mapM (s x) . f


mget :: (Monad m) => MutableLensM m s a b -> s -> m a
mget l s = liftM getConstant $ l Constant s

mset :: (Monad m) => MutableLensM m s a b -> s -> b -> m ()
mset l s v = liftM runIdentity $ l (const $ Identity v) s

В качестве примера позвольте создать изменяемый объектив с TVar внутри STM:

alterTVar :: MutableLensM' STM (TVar a) a
alterTVar = mkLensM readTVar writeTVar

Эти линзы односторонне напрямую могут быть скомбинированы с Lens, например

alterTVar . _1 :: MutableLensM' STM (TVar (a, b)) a

Примечания:

  • Mutable объективы могут быть сделаны более мощными, если мы разрешим, чтобы модифицирующая функция включала эффекты:

    type MutableLensM2  m s  a b
        = (Traversable f) => (a -> m (f b)) -> (s -> m (f ()))
    type MutableLensM2' m s  a
        = MutableLensM2 m s a a
    
    mkLensM2 :: (Monad m) => (s -> m a) -> (s -> b -> m ())
             -> MutableLensM2 m s a b
    mkLensM2 g s  f x = g x >>= f >>= T.mapM (s x)
    

    Однако он имеет два основных недостатка:

    • Он не является составным с чистым Lens.
    • Так как внутреннее действие произвольно, оно позволяет вам стрелять в ногу, изменяя этот (или другой) объектив во время самой операции мутирования.
  • Существуют другие возможности для монадических линз. Например, мы можем создать монадический объектив для копирования на запись, который сохраняет исходный контейнер (как это делает Lens), но где операция включает в себя некоторое монадическое действие:

    type LensCOW m s t a b
        = forall f . (Traversable f) => (a -> f b) -> (s -> m (f t))
    
  • Я сделал jLens - библиотеку Java для изменяемых объективов, но API, конечно, далек от того, хорошо, как линзы Haskell.

Ответ 2

Нет, вы не можете ограничить "Функтор объектива" также Монадой. Тип для Lens требует, чтобы он был совместим со всеми Functor s:

type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t

Это на английском языке что-то вроде: Объектив - это функция, которая для всех типов f, где f является Functor, принимает (a -> f b) и возвращает s -> f t. Ключевой частью этого является то, что он должен обеспечивать такую ​​функцию для каждого Functor f, а не только для некоторого подмножества из них, которые бывают Monad s.


Edit:

Вы можете сделать Lens (MVar a) (MVar b) a b, поскольку ни один из s t a или b не ограничен. Какими были бы типы на геттере и сеттере, необходимые для его построения? Тип получателя будет (MVar a -> a), который, я считаю, может быть реализован только как \_ -> undefined, так как ничего не извлекает значение из MVar, кроме как IO a. Установителем будет (MVar a -> b -> MVar b), который мы также не можем определить, поскольку ничего не делает MVar, кроме как IO (MVar b).

Это говорит о том, что вместо этого мы могли бы сделать тип Lens (MVar a) (IO (MVar b)) (IO a) b. Это был бы интересный способ продолжить работу с некоторым фактическим кодом и компилятором, которого у меня нет сейчас. Чтобы объединить это с другими "чисто функциональными" объективами, нам, вероятно, понадобится какой-то подъем, чтобы поднять объектив в монаду, что-то вроде liftLM :: (Monad m) => Lens s t a b -> Lens s (m t) (m a) b.


Код, который компилирует (2-е редактирование):

Чтобы иметь возможность использовать Lens s t a b как Getter s a, мы должны иметь s ~ t и a ~ b. Это ограничивает наш тип полезных объективов, поднятых над некоторым Monad до самого широкого типа для s и t и самого широкого типа для a и b. Если подставить b ~ a в возможный тип, мы бы имели Lens (MVar a) (IO (MVar a)) (IO a) a, но нам все еще нужны MVar a ~ IO (MVar a) и IO a ~ a. Мы берем широкий из каждого из этих типов и выбираем Lens (IO (MVar a)) (IO (MVar a)) (IO a) (IO a), который Control.Lens.Lens позволяет нам писать как Lens' (IO (MVar a)) (IO a). Следуя этой линии рассуждений, мы можем создать полную систему для объединения "чисто функциональных" линз с линзами на монадические ценности. Операция поднять объектив "чисто функции" liftLensM, затем имеет тип (Monad m) => Lens' s a -> LensF' m s a, где LensF' f s a ~ Lens' (f s) (f a).

{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}

module Main (
    main
) where

import Control.Lens
import Control.Concurrent.MVar

main = do
    -- Using MVar
    putStrLn "Ordinary MVar"
    var <- newMVar 1
    output var
    swapMVar var 2
    output var

    -- Using mvarLens
    putStrLn ""
    putStrLn "MVar accessed through a LensF' IO"
    value <- (return var) ^. mvarLens
    putStrLn $ show value 
    set mvarLens (return 3) (return var)
    output var

    -- Debugging lens
    putStrLn ""
    putStrLn "MVar accessed through a LensF' IO that also debugs"
    value <- readM (debug mvarLens) var
    putStrLn $ show value 
    setM (debug mvarLens) 4 var
    output var 

    -- Debugging crazy box lens
    putStrLn ""
    putStrLn "MVar accessed through a LensF' IO that also debugs through a Box that been lifted to LensF' IO that also debugs"
    value <- readM ((debug mvarLens) . (debug (liftLensM boxLens))) var
    putStrLn $ show value 
    setM ((debug mvarLens) . (debug (liftLensM boxLens))) (Box 5) var
    output var 

    where
        output = \v -> (readMVar v) >>= (putStrLn . show)

-- Types to write higher lenses easily

type LensF f s t a b = Lens (f s) (f t) (f a) (f b)

type LensF' f s a = Lens' (f s) (f a)

type GetterF f s a = Getter (f s) (f a)

type SetterF f s t a b = Setter (f s) (f t) (f a) (f b) 

-- Lenses for MVars

setMVar :: IO (MVar a) -> IO a -> IO (MVar a)
setMVar ioVar ioValue = do
    var <- ioVar
    value <- ioValue
    swapMVar var value
    return var

getMVar :: IO (MVar a) -> IO a
getMVar ioVar = do
    var <- ioVar
    readMVar var
-- (flip (>>=)) readMVar 

mvarLens :: LensF' IO (MVar a) a
mvarLens = lens getMVar setMVar       

-- Lift a Lens' to a Lens' on monadic values           

liftLensM :: (Monad m) => Lens' s a -> LensF' m s a
liftLensM pureLens = lens getM setM
    where
        getM mS = do
            s <- mS
            return (s^.pureLens)
        setM mS mValue = do
            s <- mS
            value <- mValue
            return (set pureLens value s)


-- Output when a Lens' is used in IO 

debug :: (Show a) => LensF' IO s a -> LensF' IO s a 
debug l = lens debugGet debugSet
    where
        debugGet ioS = do
            value <- ioS^.l
            putStrLn $ show $ "Getting " ++ (show value)
            return value
        debugSet ioS ioValue = do
            value <- ioValue
            putStrLn $ show $ "Setting " ++ (show value)
            set l (return value) ioS

-- Easier way to use lenses in a monad (if you don't like writing return for each argument)

readM :: (Monad m) => GetterF m s a -> s -> m a
readM l s = (return s) ^. l

setM :: (Monad m) => SetterF m s t a b -> b -> s -> m t
setM l b s = set l (return b) (return s)

-- Another example lens

newtype Boxed a = Box {
    unBox :: a
} deriving Show

boxLens :: Lens' a (Boxed a) 
boxLens = lens Box (\_ -> unBox)

Этот код производит следующий вывод:

Ordinary MVar
1
2

MVar accessed through a LensF' IO
2
3

MVar accessed through a LensF' IO that also debugs
"Getting 3"
3
"Setting 4"
4

MVar accessed through a LensF' IO that also debugs through a Box that been lifted to LensF' IO that also debugs
"Getting 4"
"Getting Box {unBox = 4}"
Box {unBox = 4}
"Setting Box {unBox = 5}"
"Getting 4"
"Setting 5"
5

Вероятно, лучше написать liftLensM, не прибегая к использованию обозначений Lens, (^.), set и do. Что-то кажется неправильным в отношении создания линз путем извлечения геттера и сеттера и вызова Lens нового приемника и сеттера.

Мне не удалось выяснить, как использовать объектив как геттер, так и сеттер. readM (debug mvarLens) и setM (debug mvarLens) оба работают нормально, но любая конструкция, подобная 'let debugMVarLens = debug mvarLens', теряет либо тот факт, что она работает как Getter, тот факт, что она работает как Setter, либо знание, что Int является экземпляром show, поэтому он может использоваться для debug. Мне бы хотелось увидеть лучший способ написания этой части.

Ответ 3

У меня была та же проблема. Я попробовал методы в ответах Петра и Сирдека, но так и не дошел до того, что хотел. Начал работу над проблемой, и в конце я опубликовал справочную библиотеку по хаке с обобщением линз.

Я следил за идеей библиотеки yall, чтобы параметризовать ссылки с типами монады. В результате в Control.Reference.Predefined имеется ссылка mvar. Это ссылка на IO, поэтому доступ к ссылочному значению выполняется в результате операции ввода-вывода.

Существуют и другие приложения этой библиотеки, они не ограничены IO. Дополнительной особенностью является добавление ссылок (поэтому добавление аксессуаров _1 и _2 дает обход both, который обращается к обоим полям). Он также может использоваться для освобождения ресурсов после доступа к ним, поэтому его можно использовать для безопасного управления файлами.

Использование таково:

test = 
  do result <- newEmptyMVar
     terminator <- newEmptyMVar
     forkIO $ (result ^? mvar) >>= print >> (mvar .= ()) terminator >> return ()
     hello <- newMVar (Just "World")
     forkIO $ ((mvar & just & _tail & _tail) %~= ('_':) $ hello) >> return ()
     forkIO $ ((mvar & just & element 1) .= 'u' $ hello) >> return ()
     forkIO $ ((mvar & just) %~= ("Hello" ++) $ hello) >> return ()

     x <- runMaybeT $ hello ^? (mvar & just) 
     mvar .= x $ result
     terminator ^? mvar

Оператор & объединяет линзы, ^? обобщается для обработки ссылок любой монады, а не только ссылочного значения, которое может не существовать. Оператор %~= представляет собой обновление монадической ссылки с чистой функцией.