我可以使用Monad约束制作镜头吗?

时间:2013-09-13 20:30:47

标签: haskell monads lens

上下文:此问题具体参考Control.Lens(撰写本文时的第3.9.1版)

我一直在使用镜头库,能够读取和写入结构的一块(或遍历的碎片)是非常好的。然后我讨论了是否可以使用镜头来对抗外部数据库。当然,我需要在IO Monad中执行。所以概括:

问题:

给定一个吸气剂,(s -> m a)和一个(b -> s -> m t),其中m是一个Monad,可以构建Lens s t a b,其中镜头的Functor现在也包含在其中成为Monad?是否仍然可以将(.)与其他"纯功能"组合在一起。镜片

示例:

我可以使用Lens (MVar a) (MVar b) a breadMVar制作withMVar吗?

替代:

IO monad中的容器是否等同于Control.Lens,例如MVarIORef(或STDIN)?

3 个答案:

答案 0 :(得分:7)

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

首先让我们回想一下通用的van Laarhoven镜头(在我们稍后需要进口一些产品之后):

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

现在可变镜头应如何工作?获取某些容器的内容涉及一个monadic动作。设置一个值不会改变容器,它保持不变,就像一块可变的内存一样。因此,可变镜头的结果必须是monadic,而不是返回类型容器t,我们只有()。此外,Functor约束是不够的,因为我们需要将它与monadic计算交错。因此,我们需要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是monadic计算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

备注:

  • 如果允许修改功能包括效果,可变镜头可以变得更强大:

    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. 它不能与纯Lens合成。
    2. 由于内部动作是任意的,它允许你在变异操作过程中通过改变这个(或其他)镜头来射击自己。
  • monadic镜片还有其他可能性。例如,我们可以创建一个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镜头那么好。

答案 1 :(得分:5)

不,你不能将“镜头的Functor”限制为Monad。 Lens的类型要求它与所有Functor s兼容:

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

这用英文读取的内容如下:A Lens是一个函数,对于f f Functor所有类型(a -> f b),它取s -> f t并返回Functor。关键部分是它必须为每个f Monad提供这样的功能,而不仅仅是恰好是Lens (MVar a) (MVar b) a b s的某些子集。


修改

您可以制作s,因为t a b(MVar a -> a)都不受限制。那么构造它所需的getter和setter上的类型是什么? getter的类型为\_ -> undefined,我认为它只能实现为IO a,因为除了(MVar a -> b -> MVar b)之外,没有什么可以从MVar中提取值。 setter将是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类型。这将是一个有趣的途径,进一步追求一些实际的代码和编译器,我现在没有。为了将其与其他“纯功能”镜头结合起来,我们可能需要某种升力将镜头升为单体,例如Lens s t a b


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

为了能够将Getter s a用作s ~ t,我们必须拥有a ~ bMonad。这会将s ta的最宽类型bb ~ 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)Lens' (IO (MVar a)) (IO a)。我们采用每种类型的广泛选择,并选择liftLensM,Control.Lens.Lens允许我们将其写为(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'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

如果不使用(^.)setdolens表示法,可能有更好的方法来编写readM (debug mvarLens)。通过提取getter和setter并在新的getter和setter上调用setM (debug mvarLens)来构建镜头似乎有些错误。

我无法弄清楚如何将镜头重新用作吸气剂和固定器。 GetterSetter都可以正常工作,但是任何像'let debugMVarLens = debug mvarLens'这样的构造都失去了它作为Int工作的事实,它作为{{1}工作的事实或showdebug的实例的知识,因此我可以将其用于{{1}}。我很想看到写这部分的更好方法。

答案 2 :(得分:0)

我遇到了同样的问题。我在Petr和Cirdec的答案中尝试了这些方法,但从未达到我想要的程度。开始研究这个问题,最后,我发表了关于镜片概括的references library hackage。

我遵循yall库的想法来参数化monad类型的引用。因此,mvar中有Control.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的引用,而不仅仅是可能不存在的引用值。 %~=运算符是具有纯函数的monadic引用的更新。