13

背景:这个问题专门参考Control.Lens(撰写本文时的版本 3.9.1)

我一直在使用镜头库,能够读取和写入结构的一部分(或遍历的部分)非常好。然后,我考虑了是否可以将镜头用于外部数据库。当然,然后我需要在IO Monad. 所以概括一下:

问题:

给定一个 getter和一个在哪里是 Monad(s -> m a)的 setter ,是否有可能构造现在包含镜头的 Functor 的位置也是一个 Monad?是否仍然可以将这些与其他“纯功能”镜头组合在一起?(b -> s -> m t)mLens s t a b(.)

例子:

我可以Lens (MVar a) (MVar b) a b 使用readMVarandwithMVar吗?

选择:

monad中的容器是否有等效于 Control.Lens 的容器,IO例如MVaror IORef(or STDIN)?

4

3 回答 3

7

我一直在考虑这个想法,我称之为可变镜头。到目前为止,我还没有把它做成一个包,如果你能从中受益,请告诉我。

首先让我们回顾一下广义的 van Laarhoven Lenses(在我们稍后需要一些导入之后):

{-# 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

我们可以从“getter”和“setter”创建这样一个镜头

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

例如,让我们从TVarwithin创建一个可变镜头STM

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

这些镜头单方面可直接组合Lens,例如

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

笔记:

  • 如果我们允许修改函数包含效果,则可变镜头可以变得更强大:

    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)
    

    但是,它有两个主要缺点:

    1. 它不能与 pure 组合Lens
    2. 由于内部动作是任意的,它允许您通过在变异操作本身期间变异这个(或其他)镜头来射击自己。
  • 一元透镜还有其他可能性。例如,我们可以创建一个 monadic copy-on-write 镜头,它保留原始容器(就像这样Lens做一样),但是操作涉及一些 monadic 动作:

    type LensCOW m s t a b
        = forall f . (Traversable f) => (a -> f b) -> (s -> m (f t))
    
  • 我已经制作了jLens——一个可变镜头的 Java 库,但它的 API 当然远没有 Haskell 镜头那么好。

于 2013-09-14T10:30:37.183 回答
6

不,您不能将“镜头函子”限制为单子。a 的类型Lens要求它与所有Functors 兼容:

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

这用英语读起来是这样的:A Lens 是一个函数,对于所有类型fwhere fis a Functor,它接受 an(a -> f b)并返回 an s -> f t。其中的关键部分是它必须为每个 s 提供这样的功能Functor f,而不仅仅是其中一些恰好是Monads 的子集。


编辑:

您可以制作Lens (MVar a) (MVar b) a b, 因为没有s t a, 或b受到约束。那么构建它所需的 getter 和 setter 上的类型是什么?getter 的类型是(MVar a -> a),我相信它只能实现为\_ -> undefined,因为除了 as 之外没有任何东西可以从 MVar 中提取值IO a。setter 将是(MVar a -> b -> MVar b),我们也无法定义它,因为MVar除了 as 之外没有任何东西IO (MVar b)

这表明我们可以改为创建 type Lens (MVar a) (IO (MVar b)) (IO a) b。这将是进一步探索一些实际代码和编译器的有趣途径,而我现在还没有。要将其与其他“纯功能”镜头结合起来,我们可能需要某种提升来将镜头提升为单子,例如liftLM :: (Monad m) => Lens s t a b -> Lens s (m t) (m a) b.


编译的代码(第二次编辑):

为了能够使用Lens s t a bas aGetter s a我们必须拥有s ~ tand a ~ b。这将我们的有用镜头类型限制Monad为 和 的最宽类型s以及和t的最宽类型。如果我们替换成可能的类型,我们将有,但我们仍然需要and 。我们取每种类型的宽度,并选择Control.Lens.Lens 让我们写成的。按照这种推理,我们可以制作一个完整的系统,将“纯功能”镜片与单值镜片结合起来。提起“纯功能”镜头的操作,则有类型,其中。abb ~ aLens (MVar a) (IO (MVar a)) (IO a) aMVar a ~ IO (MVar a)IO a ~ aLens (IO (MVar a)) (IO (MVar a)) (IO a) (IO a)Lens' (IO (MVar a)) (IO a)liftLensM(Monad m) => Lens' s a -> LensF' m s aLensF' 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's 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's 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、和符号。通过提取 getter 和 setter 并调用新的 getter 和 setter 来构建镜头似乎有些错误。(^.)setdolens

我无法弄清楚如何将镜头同时用作吸气剂和设置剂。readM (debug mvarLens)并且setM (debug mvarLens)两者都工作得很好,但是任何像'let debugMVarLens = debug mvarLens'这样的构造都会失去它作为a工作Getter的事实,它作为a工作的事实,或者作为一个实例Setter的知识,所以它可以用于. 我很想看到一个更好的方式来写这部分。Intshowdebug

于 2013-09-13T21:52:19.603 回答
0

我有同样的问题。我尝试了 Petr 和 Cirdec 的答案中的方法,但从未达到我想要的程度。开始解决这个问题,最后,我发布了关于 hackage 的参考库,其中包含镜头的概括。

我遵循yall库的想法,用 monad 类型参数化引用。因此,mvarControl.Reference.Predefined. 它是一个 IO 引用,因此对引用值的访问是在 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

运算符&组合镜头,^?泛化为处理任何 monad 的引用,而不仅仅是可能不存在的引用值。%~=运算符是具有纯函数的一元引用的更新。

于 2014-07-23T12:13:36.157 回答