2

我正在阅读有关镜头的教程,在介绍中,作者lens通过展示我们如何使用标准 Haskell 实现 OOP 风格的“setter”/“getter”的一些示例来激发这个概念。我对以下示例感到困惑。

假设我们User按照图 1(下图)定义了一个代数数据类型。本教程(正确地)指出,我们可以通过NaiveLens数据类型和nameLens函数实现“setter”功能(也在图 1 中)。图 2 给出了一个示例用法。

我很困惑为什么我们需要如此复杂的构造(即NaiveLens数据类型和nameLens函数)来实现“setter”功能,而以下(有些明显的)函数似乎同样能很好地完成这项工作:set' a s = s {name = a}.

但是,鉴于我的“明显”函数就是 lambda 函数的一部分nameLens,我怀疑使用下面的构造确实有优势,但是我太密集了,看不出那个优势是什么。我希望其中一位 Haskell 向导可以帮助我理解。

图 1(定义):

data User = User { name :: String
                 , age :: Int
                 } deriving Show

data NaiveLens s a = NaiveLens { view :: s -> a
                               , set :: a -> s -> s
                               }

nameLens :: NaiveLens User String
nameLens = NaiveLens name (\a s -> s {name = a})

图 2(示例用法):

λ: let john = User {name="John",age=30}
john :: User

λ: set nameLens "Bob" john
User {name = "Bob", age = 30}
it :: User
4

2 回答 2

6

镜头的主要优点是它们可以组合,因此它们可以用于访问和更新嵌套记录中的字段。使用记录更新语法手动编写这种嵌套更新很快就会变得乏味。

于 2014-11-19T19:59:17.117 回答
2

假设您添加了一个Email数据类型:

data Email = Email
    { _handle :: String
    , _domain :: String
    } deriving (Eq, Show)

handle :: NaiveLens Email String
handle = NaiveLens _handle (\h e -> e { _handle = h })

并将其作为字段添加到您的User类型中:

data User = User
    { _name :: String
    , _age :: Int
    , _userEmail :: Email
    } deriving (Eq, Show)

email :: NaiveLens User Email
email = NaiveLens _userEmail (\e u -> u { _userEmail = e })

镜头的真正力量来自于能够构图,但这是一个有点棘手的步骤。我们想要一些看起来像的函数

(...) :: NaiveLens s b -> NaiveLens b a -> NaiveLens s a
NaiveLens viewA setA ... NaiveLens viewB setB
    = NaiveLens (viewB . viewA) (\c a -> setA (setB c (viewA a)) a)

为了解释这是如何写的,我会推迟到这篇文章,我无耻地把它从那里摘下来。这个新镜头的结果set字段可以认为是取一个新值和一个顶级记录,查找较低的记录并将其值设置为c,然后将新记录设置为顶级记录。

现在我们有了一个方便的函数来组合我们的镜头:

> let bob = User "Bob" 30 (Email "bob" "gmail")
> view (email...handle) bob
"bob"
> set (email...handle) "NOTBOB" bob
User {_name = "Bob", _age = 30, _userEmail = Email {_handle = "NOTBOB", _domain = "gmail"}}

我在...这里用作组合运算符是因为我认为它很容易键入并且仍然与.运算符相似。现在,这为我们提供了一种深入研究结构、相当任意地获取和设置值的方法。如果我们有一个domain类似的镜头,我们可以以几乎相同的方式获取和设置该值。这就是它看起来像是 OOP 成员访问的原因,即使它只是花哨的函数组合。

如果您查看镜头库(我对镜头的选择),您会得到一些不错的工具来使用模板 haskell 为您自动构建镜头,并且在幕后还有一些额外的东西可以让您使用普通的函数合成运算符.来代替一个定制的。

于 2014-11-19T20:54:45.353 回答