Haskell 透镜的用途是什么?

Haskell 透镜的用途是什么?,haskell,data-structures,types,lenses,Haskell,Data Structures,Types,Lenses,在实际例子中,我似乎找不到任何关于透镜用途的解释。这是我发现的最接近黑客页面的一段: 此模块提供了访问和更新结构元素的方便方法。它与Data.Accessors非常相似,但更通用一些,依赖性更少。我特别喜欢它如何干净地处理状态monad中的嵌套结构 那么,它们是用来干什么的?与其他方法相比,它们有哪些优点和缺点?为什么需要它们?镜头以统一、组合的方式提供了编辑数据结构的方便方法 许多程序围绕以下操作构建: 查看(可能是嵌套的)数据结构的组件 正在更新(可能是嵌套的)数据结构的字段 镜头以确保

在实际例子中,我似乎找不到任何关于透镜用途的解释。这是我发现的最接近黑客页面的一段:

此模块提供了访问和更新结构元素的方便方法。它与Data.Accessors非常相似,但更通用一些,依赖性更少。我特别喜欢它如何干净地处理状态monad中的嵌套结构


那么,它们是用来干什么的?与其他方法相比,它们有哪些优点和缺点?为什么需要它们?

镜头以统一、组合的方式提供了编辑数据结构的方便方法

许多程序围绕以下操作构建:

  • 查看(可能是嵌套的)数据结构的组件
  • 正在更新(可能是嵌套的)数据结构的字段
镜头以确保编辑一致的方式为查看和编辑结构提供语言支持;编辑可以很容易地组合;同一代码可用于查看结构的各个部分,也可用于更新结构的各个部分

因此,透镜使得从视图到结构编写程序变得容易;并从结构返回到这些结构的视图(和编辑器)。他们清理了大量杂乱无章的记录存取器和设置器

Pierce等人,哈斯凯尔的普及透镜(例如,在其应用中)和实现现在被广泛使用(例如,和数据存取器)

对于具体用例,请考虑:

  • 图形用户界面,其中用户以结构化方式编辑信息
  • 解析器和漂亮的打印机
  • 编译程序
  • 同步更新数据结构
  • 数据库和模式

在许多其他情况下,您拥有世界的数据结构模型和数据的可编辑视图。

它们为数据更新提供了清晰的抽象,并且从来都不是真正的“需要”。它们只是让您以不同的方式对问题进行推理

在一些命令式/面向对象的编程语言(如C)中,您熟悉一些值集合(让我们称它们为“结构”)的概念,以及标记集合中每个值的方法(标签通常称为“字段”)。这导致了这样一个定义:

let vec  = Vec2 2 3
    -- Reading the components of vec
    foo  = vecX vec
    -- Creating a new vector with some component changed.
    vec2 = vec { vecY = foo }

    mat = Mat2 vec2 vec2
typedef struct{/*定义新的结构类型*/
浮点x;/*字段*/
浮动y;/*字段*/
}Vec2;
类型定义结构{
Vec2 col1;/*嵌套结构*/
Vec2-col2;
}Mat2;
然后,您可以创建此新定义类型的值,如下所示:

vec2vec={2.0f,3.0f};
/*读取vec的组件*/
float foo=vec.x;
/*写入vec的组件*/
向量y=foo;
Mat2 mat={vec,vec};
/*更改矩阵中的嵌套字段*/
材料col2.x=4.0f;
类似地,在Haskell中,我们有数据类型:

data Vec2 =
  Vec2
  { vecX :: Float
  , vecY :: Float
  }

data Mat2 =
  Mat2
  { matCol1 :: Vec2
  , matCol2 :: Vec2
  }
该数据类型的使用方式如下:

let vec  = Vec2 2 3
    -- Reading the components of vec
    foo  = vecX vec
    -- Creating a new vector with some component changed.
    vec2 = vec { vecY = foo }

    mat = Mat2 vec2 vec2
但是,在Haskell中,没有简单的方法可以更改数据结构中的嵌套字段。这是因为您需要围绕正在更改的值重新创建所有包装对象,因为Haskell值是不可变的。如果Haskell中有一个如上所述的矩阵,并且想要更改矩阵中右上角的单元格,则必须编写以下内容:

    mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
它能工作,但看起来很笨拙。所以,有人提出的基本上是这样的:如果你把两个东西组合在一起:一个值的“getter”(如上面的
vecX
matCol2
)与一个相应的函数,给定getter所属的数据结构,可以创建一个新的数据结构,并更改该值,你能做很多整洁的事情。例如:

data Data = Data { member :: Int }

-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d

-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }

memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)
有许多实现透镜的方法;对于本文,假设透镜与上面的一样:

type Lens a b = (a -> b, a -> b -> a)
也就是说,它是某种类型
a
的getter和setter的组合,其字段类型为
b
,因此上面的
memberLens
将是
Lens Data Int
。这让我们做什么

首先,让我们制作两个简单的函数,从镜头中提取getter和setter:

getL :: Lens a b -> a -> b
getL (getter, setter) = getter

setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter
现在,我们可以开始抽象东西了。让我们再次考虑上面的情况,我们想要修改一个“两层楼深”的值。我们添加了一个数据结构和另一个镜头:

data Foo = Foo { subData :: Data }

subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
现在,让我们添加一个由两个镜头组成的函数:

(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
    (getter2 . getter1, combinedSetter)
    where
      combinedSetter a x =
        let oldInner = getter1 a
            newInner = setter2 oldInner x
        in setter1 a newInner
代码编写得有点快,但我认为它的作用是很清楚的:getter只是简单地组合起来的;获取内部数据值,然后读取其字段。当setter要用新的内部字段值
x
更改某个值
a
时,它首先检索旧的内部数据结构,设置其内部字段,然后用新的内部数据结构更新外部数据结构

现在,让我们做一个函数,简单地增加镜头的值:

increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
如果我们有此代码,它的作用就会变得很清楚:

d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
现在,因为我们可以合成镜头,我们也可以这样做:

f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
所有的镜头包基本上都是将镜头的概念——一个“setter”和一个“getter”组合成一个整洁的包,使它们易于使用。在特定的镜头实现中,可以编写:

with (Foo (Data 5)) $ do
  subDataLens . memberLens $= 7
因此,您非常接近C版本的代码;修改数据结构树中的嵌套值变得非常容易

镜头就是这样:一种修改部分数据的简单方法。因为对某些概念进行推理变得非常容易,所以它们在有大量数据结构的情况下有着广泛的用途,这些数据结构必须以各种方式相互交互


有关镜头的优点和缺点,请参见。

作为补充说明,镜头实现了一个非常通用的“现场访问和更新”概念,这一点经常被忽略。透镜可以用于各种各样的东西,包括功能类似的物体。它需要一些抽象的思维来理解
polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag   :: (RealFloat a) => Lens (Complex a) a