现在请做好准备,让我们来介绍一个想象力非凡的概念:透镜组Lens。顾名思义,透镜组的作用是让你更清楚地观察。而在光学上,透镜组可以相互组合,构成新的透镜组,从而满足各种观察要求。对于Haskell来说,透镜组的作用就是让你透过它,操作复杂数据结构中的一小部分,它最重要的特点就是可以组合!

先来看看它如何定义:

type Lens b a = Functor f => (a -> f a) -> b -> f b

Lens b a是一个类型别名,它指的是类型是(a -> f a) -> b -> f b的函数,这个函数里面的a类型是数据中需要操作的那部分的类型,而b类型则是数据本身的类型。所以。对于Position类型来说,我们可以定义出两个透镜组:

xLens :: Functor f => (Double -> f Double) -> Position -> f Position
yLens :: Functor f => (Double -> f Double) -> Position -> f Position

由于类型别名中并没有包含函子的类型变量f,所以想要编译器接收这个类型,需要打开高阶类型的扩展,这里暂时先不理会这个限制,只需关心怎么写出具体的透镜组。我们以xLens为例,它会接收到一个Double -> f Double类型的函数,然后把这个函数变成Position -> f Position类型的函数。在没有其他任何信息的情况下,唯一线索是Lens类型别名中的一个约束Functor f =>,这要求f一定要是一个函子类型,而我们手上现在掌握的函数中,下面两个能够反映Double类型的横坐标和Position类型的值的关系:

positionX :: Position -> Double
setPositionX :: Double -> Position -> Position

此外,还有一个可以把Double -> Position类型升格成f Double -> f Position类型的函数fmap。这个fmap的具体定义我们并不关心,这和f的具体类型相关,但是却可以利用这个函数定义出需要的Functor f => (Double -> f Double) -> Position -> f Position类型的函数:

xLens :: Functor f => (Double -> f Double) -> Position -> f Position
-- 也可以使用类型别名。注意类型别名中,大类型在前,小类型在后
-- xLens :: Lens Position Double

xLens f p = fmap (\x' -> setPositionX x' p) $ f (positionX p)
  where
    setPositionX :: Double -> Position -> Position
    setPositionX x' p = p { positionX = x' }

-- inline写法
xLens f p = fmap (\x' -> p { positionX = x' }) $ f (positionX p)

这里需要注意xLens做的事情。

 在接收的参数中,f是一个Double -> f Double类型的函数,p是Position类型的数据。

 通过positionX p把p中的横坐标提取出来,并交给f得到一个包裹在函子中的值f (positionX p) :: f Double。

 通过构造匿名函数\x' -> setPositionX x' p得到一个Double -> Position类型的函数,这个函数接收一个x'并把它当成新的横坐标设置给p。

 最后通过fmap把刚刚构造出来的Double -> Position类型的函数升格为f Double -> f Position类型的函数,并把第二步得到的包裹在函子中的值f (positionX p)交给这个函数。注意$的使用。

 我们成功地得到了函数类型说明中要求得到的f Position类型的值。

你可能在想这个函子类型f有什么作用?下面就来看看:

Prelude> data Position = Position { positionX :: Double, positionY :: Double } deriving Show
Prelude> data Line = Line { lineStart :: Position, lineEnd :: Position } deriving Show
Prelude> let setPositionX x' p = p { positionX = x' }
Prelude> let let xLens f p = fmap (\x' -> setPositionX x' p) $ f (positionX p)
Prelude> xLens (\x -> Just (x+1)) (Position 3 4)
Just (Position {positionX = 4.0, positionY = 4.0})
Prelude> xLens (\x -> Nothing) (Position 3 4)
Nothing
Prelude> xLens (\x -> [x+1, x+2, x+3]) (Position 3 4)
[ Position {positionX = 4.0, positionY = 4.0}
, Position {positionX = 5.0, positionY = 4.0}
, Position {positionX = 6.0, positionY = 4.0} ]

使用deriving Show可以自动生成数据类型的Show实例,从而在GHCi中方便地显示出来。我们给xLens传递Double -> Maybe Double类型的函数,代表在更新坐标地时计算可能会失败,结果我们得到了Maybe Position类型的值,而不是包含Maybe Double的Position。当丢给xLens的函数是Double -> [Double]类型时,代表我们可能在一次操作中产生若干个新的横坐标,我们得到了每一个新的横坐标对应的新坐标!

通过这个例子可以看到透镜组xLens的威力,它允许你使用函子来包裹你的数据操作,从而获得各种各样的计算语义。其中的核心是不同的函子类型实例声明中不同的fmap实现。下面问题来了,我们能否通过透镜组获得getter和setter?

评论

本文目前还没有评论……

我要评论

需要登录后才能发言
登录未成功,请修改提交。

× 453
× 1756
× 2438
× 960
× 1
× 1
× 1198
× 0
× 1
× 0
× 2
× 1
× 3
× 4
× 2755
× 818
× 1108