haskell - 镜头的用途/用途是什么?

标签 haskell data-structures types lenses

我似乎找不到任何关于在实际示例中使用什么镜头的解释。 Hackage 页面中的这个简短段落是我找到的最接近的:

This modules provides a convienient way to access and update the elements of a structure. It is very similar to Data.Accessors, but a bit more generic and has fewer dependencies. I particularly like how cleanly it handles nested structures in state monads.



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

最佳答案

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

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

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

typedef struct {
  Vec2 col1; /* nested structs */
  Vec2 col2;
} Mat2;

然后,您可以像这样创建这个新定义的类型的值:

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;

同样在 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”(如上面的 vecXmatCol2)和一个相应的函数,给定 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)

IE。它是某种类型的 getter 和 setter 的组合 a它有一个类型为 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 ,当它应该改变一些值时 a新的内部字段值为 x ,首先检索旧的内部数据结构,设置其内部字段,然后用新的内部数据结构更新外部数据结构。

现在,让我们创建一个简单地增加镜头值的函数:
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 版本的代码;在数据结构树中修改嵌套值变得非常容易。

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

镜片的优缺点见a recent question here on SO .

关于haskell - 镜头的用途/用途是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10788261/

相关文章:

c - 返回类型是否节省空间或时间

haskell - haskell中的递归实例

haskell - 为什么引入关联类型会扼杀我的表现?

haskell - 过滤功能免点

java - 有没有提供排序,没有重复元素,subList(index, index)方法的集合?

c++ - 尝试在双向链表的中间插入新节点时我是否连接错误?

python - 如何允许用户输入各种各样的内容并让它们在 numpy 数组中工作?

haskell - 为什么 GHC 在这里推断出单态类型,即使禁用了 MonomorphismRestriction?

c++ - 如何在此自定义堆栈实现中正确分配更多内存?

types - 当使用剩余字段定义记录时,类型缩小不起作用