我一直在考虑这个想法,我称之为可变镜头。到目前为止,我还没有把它做成一个包,如果你能从中受益,请告诉我。
首先让我们回顾一下广义的 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
例如,让我们从TVar
within创建一个可变镜头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)
但是,它有两个主要缺点:
- 它不能与 pure 组合
Lens
。
- 由于内部动作是任意的,它允许您通过在变异操作本身期间变异这个(或其他)镜头来射击自己。
一元透镜还有其他可能性。例如,我们可以创建一个 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 镜头那么好。