上下文:此问题具体参考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 b
和readMVar
制作withMVar
吗?
替代:
IO
monad中的容器是否等同于Control.Lens,例如MVar
或IORef
(或STDIN
)?
答案 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)
然而,它有两个主要缺点:
Lens
合成。 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 ~ 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)
和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
如果不使用(^.)
,set
,do
和lens
表示法,可能有更好的方法来编写readM (debug mvarLens)
。通过提取getter和setter并在新的getter和setter上调用setM (debug mvarLens)
来构建镜头似乎有些错误。
我无法弄清楚如何将镜头重新用作吸气剂和固定器。 Getter
和Setter
都可以正常工作,但是任何像'let debugMVarLens = debug mvarLens'这样的构造都失去了它作为Int
工作的事实,它作为{{1}工作的事实或show
是debug
的实例的知识,因此我可以将其用于{{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引用的更新。