什么是使用/有用的镜头?

Pub*_*bby 34 haskell types data-structures lenses

我似乎无法找到在实际例子中使用什么镜头的任何解释.Hackage页面中的这一短段是我发现的最接近的段落:

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

那么,它们用于什么?他们对其他方法有什么好处和坏处?他们为什么需要?

dfl*_*str 48

它们提供了对数据更新的清晰抽象,并且从未真正"需要".他们只是让你以不同的方式解决问题.

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

typedef struct { /* defining a new struct type */
  float x; /* field */
  float y; /* field */
} Vec2;

typedef struct {
  Vec2 col1; /* nested structs */
  Vec2 col2;
} Mat2;
Run Code Online (Sandbox Code Playgroud)

然后,您可以创建此新定义类型的值,如下所示:

Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;

Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;
Run Code Online (Sandbox Code Playgroud)

类似地,在Haskell中,我们有数据类型:

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

data Mat2 =
  Mat2
  { matCol1 :: Vec2
  , matCol2 :: Vec2
  }
Run Code Online (Sandbox Code Playgroud)

然后使用此数据类型,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

但是,在Haskell中,没有简单的方法来更改数据结构中的嵌套字段.这是因为您需要围绕要更改的值重新创建所有包装对象,因为Haskell值是不可变的.如果在Haskell中有如上所述的矩阵,并且想要更改矩阵中的右上角单元格,则必须写入:

    mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
Run Code Online (Sandbox Code Playgroud)

它有效,但看起来很笨拙.那么,是什么人想出了,基本上是这样的:如果你组的两个东西放在一起:该值的"吸气"(如vecXmatCol2以上)具有相应的功能,给定吸气剂属于数据结构,可以创建一个新的数据结构改变了这个值,你可以做很多干净的东西.例如:

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)
Run Code Online (Sandbox Code Playgroud)

有许多方法可以实现镜头; 对于这个文本,让我们说镜头就像上面那样:

type Lens a b = (a -> b, a -> b -> a)
Run Code Online (Sandbox Code Playgroud)

即它是某种类型的getter和setter的组合,a它有一个类型的字段b,所以memberLens上面就是a 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
Run Code Online (Sandbox Code Playgroud)

现在,我们可以开始抽象.让我们再次考虑上面的情况,我们想要修改一个"两层楼深"的值.我们用另一个镜头添加数据结构:

data Foo = Foo { subData :: Data }

subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
Run Code Online (Sandbox Code Playgroud)

现在,让我们添加一个组成两个镜头的功能:

(#) :: 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
Run Code Online (Sandbox Code Playgroud)

代码有点快速编写,但我认为它的作用很明确:getter只是简单编写; 你得到内部数据值,然后你读取它的字段.当setter a用新的内部字段值改变某个值时x,首先检索旧的内部数据结构,设置其内部字段,然后用新的内部数据结构更新外部数据结构.

现在,让我们创建一个简单增加镜头值的功能:

increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
Run Code Online (Sandbox Code Playgroud)

如果我们有这个代码,它就会变得清晰:

d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
Run Code Online (Sandbox Code Playgroud)

现在,因为我们可以组成镜头,我们也可以这样做:

f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
Run Code Online (Sandbox Code Playgroud)

所有镜头包装的功能基本上都是将这种镜头概念包裹起来 - 将"定位器"和"吸气器"组合成一个整洁的包装,使其易于使用.在特定的镜头实现中,人们可以写:

with (Foo (Data 5)) $ do
  subDataLens . memberLens $= 7
Run Code Online (Sandbox Code Playgroud)

所以,你非常接近代码的C版本; 在数据结构树中修改嵌套值变得非常容易.

镜头只不过是:修改部分数据的简单方法.因为由于它们而更容易推理某些概念,所以它们在你拥有大量数据结构的情况下会被广泛使用,这些数据结构必须以各种方式相互交互.

有关镜头的优缺点,请参阅此处的最新问题.

  • 你的答案缺失的一个重点是镜头是*头等*,所以你可以从它们构建其他抽象.在这方面,内置记录语法失败. (2认同)
  • 另外,我写了一篇关于可能对OP有用的镜头的博客文章:http://www.haskellforall.com/2012/01/haskell-for-mainstream-programmers_28.html (2认同)

Don*_*art 13

镜头以统一的,组合的方式提供编辑数据结构的便捷方式.

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

  • 查看(可能是嵌套的)数据结构的组件
  • 更新(可能是嵌套的)数据结构的字段

镜头以确保编辑一致的方式为查看和编辑结构提供语言支持; 编辑可以轻松编写; 并且相同的代码可用于查看结构的部分,以及更新结构的部分.

因此,镜头可以轻松地将视图中的程序编写到结构上; 从结构返回到那些结构的视图(和编辑器).他们清理了很多记录访问者和制定者.

皮尔斯等人.普及镜头,例如在他们的Quotient Lenses论文中,Haskell的实现现在被广泛使用(例如fclabels和数据访问器).

对于具体用例,请考虑:

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

以及许多其他情况,您拥有世界的数据结构模型,以及对该数据的可编辑视图.


小智 6

另外需要注意的是,镜头通常会忽略"现场访问和更新"的非常通用的概念.镜头可以用于各种事物,包括类似功能的物体.这需要一些抽象的思考来欣赏这一点,所以让我向你展示镜头的力量的一个例子:

at :: (Eq a) => a -> Lens (a -> b) b
Run Code Online (Sandbox Code Playgroud)

使用at您可以实际访问和操作具有多个参数的函数,具体取决于先前的参数.请记住,这Lens是一个类别.这是一个非常有用的习惯用于本地调整功能或其他东西.

您还可以按属性或备用表示形式访问数据:

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag   :: (RealFloat a) => Lens (Complex a) a
Run Code Online (Sandbox Code Playgroud)

您可以进一步编写镜头以访问傅里叶变换信号的各个频段等等.