记录中一种简便的方式,用于分配单个字段并复制其余字段?

144

假设我有以下的记录ADT:

data Foo = Bar { a :: Integer, b :: String, c :: String }

我希望有一个函数,它接受一条记录并返回一条记录(相同类型的)其中除了一个字段与传递的参数具有相同的值以外,其余所有字段都具有相同的值,就像这样:

walkDuck x = Bar { a = a x, b = b x, c = lemonadeStand (a x) (b x) }
上述方法可行,但对于具有更多字段(比如说10个字段)的记录,创建这样的函数将需要大量的打字,我认为这是相当不必要的。
有没有更简便的方式来实现相同的功能?

4
现有的更新记录语法虽然存在,但很快就会变得笨重。可以查看lenses来代替。 - Cat Plus Plus
3个回答

190

是的,有一种不错的更新记录字段的方法。在GHCi中,你可以这样做 --

> data Foo = Foo { a :: Int, b :: Int, c :: String }  -- define a Foo
> let foo = Foo { a = 1, b = 2, c = "Hello" }         -- create a Foo
> let updateFoo x = x { c = "Goodbye" }               -- function to update Foos
> updateFoo foo                                       -- update the Foo
Foo {a = 1, b = 2, c = "Goodbye" }

12
RecordWildCards扩展有时很方便,可以在一个作用域中“展开”字段。但对于更新操作来说,它并不是很方便:incrementA x@Foo{..} = x { a = succ a } - Jon Purdy
2
顺便提一下,在 Frege(JVM 上的 Haskell)中,您可以将函数定义为updateFoo x = x.{ c = "Goodbye" }(请注意 . 运算符)。 - 0dB
顺便说一句,视频不错 https://www.youtube.com/watch?v=YScIPA8RbVE - developer_hatch
谢谢。很遗憾,我已经很久没有写 Haskell 了! - Chris Taylor

40

这是使用lens的好工作:

data Foo = Foo { a :: Int, b :: Int , c :: String }

test = Foo 1 2 "Hello"

然后:

setL c "Goodbye" test

将 'test' 的字段 'c' 更新为您提供的字符串。


5
镜头式的包通常会定义运算符,除了获取和设置字段的功能之外。例如,test $ c .~ "Goodbye"lens 包中的一种方式(如果我没记错的话)。我并不是说这很直观,但一旦您知道了这些运算符,它可能就像 $ 一样容易使用。 - Thomas M. DuBuisson
3
你知道_setL_去哪了吗?我正在导入_Control.Lens_,但是ghc报告说_setL_未定义。 - dbanas
1
使用 set 而不是 setL。 - Subhod I
这是可怕的过程式思维。在Haskell中,不要说它做了什么,而是说它什么。这就是函数式编程的重点。编写test { c =“Goodbye”}是一种更自然的方式,表示“test,但c“Goodbye””。设计setL函数的人错过了函数式编程的全部意义。如果记录语法存在缺陷(它确实存在),则记录语法本身需要修复。不是将Haskell有效地转换回类似C的语言。 - anon
@Evi1M4chine,你看过镜头在底层是如何工作的吗?表面上看起来是命令式的,但实际上它是最精妙的范畴论类型魔术。请查看:https://www.fpcomplete.com/haskell/tutorial/lens/ - Niklas Gruhn
显示剩余2条评论

27

你不需要定义辅助函数或使用镜头。标准的Haskell已经具备了你所需的。我们来看一下Don Stewart的例子:

data Foo = Foo { a :: Int, b :: Int , c :: String }

test = Foo 1 2 "Hello"

那么你只需要说test { c = "Goodbye" }来获取更新后的记录。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接