为了尝试简化这个问题,我已经定义了这些箭头函数:
splitA :: (Arrow arr) => arr a b -> arr a (b,a)
splitA ar = ar &&& (arr (\a -> id a))
recordArrow
:: (Arrow arr)
=> (d -> r)
-> (d -> r -> d)
-> (r -> r)
-> arr d d
recordArrow g s f = splitA (arr g >>^ f) >>^ \(r,d) -> s d r
然后让我做这样的事情:
unarrow :: ((->) b c) -> (b -> c) -- unneeded as pointed out to me in the comments
unarrow g = g
data Testdata = Testdata { record1::Int,record2::Int,record3::Int }
testRecord = unarrow $
recordArrow record1 (\d r -> d { record1 = r }) id
>>> recordArrow record2 (\d r -> d { record2 = r }) id
>>> recordArrow record3 (\d r -> d { record3 = r }) id
正如您所看到的,这并没有很好地利用DRY。
我希望可能有某种语言扩展可以帮助简化这个过程。因此,上述内容可以简单地重写为:
testRecord' = unarrow $
recordArrow' record1 id
>>> recordArrow' record2 id
>>> recordArrow' record3 id
为清晰起见而更新:澄清一点。我知道我可以这样做:
foo d = d { record1 = id (record1 d), record2 = id (record2 d) }
但这会忽略任何执行顺序和任何状态。假设record2
的更新函数依赖于record1
的更新值。或者,我可能想要创建一个如下所示的不同箭头:arr d (d,x)
然后我想构建一个[x]
列表,其顺序取决于记录的评估顺序。
我发现我经常想要执行某些功能,然后更新记录。我可以通过像这样穿线状态来做到这一点
g :: d -> r -> d
foo d = let d' = d { record1 = (g d) (record1 d) } in d' { record2 = (g d') (record2 d') }
但我认为箭头符号更整洁,我也可以[arr d d]
并按顺序将它们链接在一起。另外,如果r
或d
是Monads,则会创建更整洁的代码。或者,如果它们都是Monads,那么我就可以执行分层绑定,而无需使用Monad Transformer。在ST s x
的情况下,让我以有序的方式绕过状态s
。
我不想解决一个特定的问题。我只是想找到一种更新记录语法的抽象方法,而不必明确定义某种“getter”和“setter”。
评论中回答了以下问题 -
旁注:我必须定义一个函数unarrow
,用于将函数( - >)箭头转换回函数。否则,如果我有someArrow b
箭头arr b c
,我无法获得价值c
。使用unarrow
函数,我可以编写unarrow someArrow b
,它可以正常工作。我觉得我必须在这里做错事,因为我对unarrow的定义只是unarrow g = g
。
答案 0 :(得分:7)
您正在寻找的抽象被称为镜头,Hackage上的lens
包可能是目前使用的最普遍的实现。使用lens
包,您可以将recordArrow'
定义为
{-# LANGUAGE RankNTypes #-}
import Control.Arrow
import Control.Lens
recordArrow' :: Arrow arr => Lens' d r -> (r -> r) -> arr d d
recordArrow' field f = arr $ field %~ f
%~
是一个更新运算符,它使用给定镜头的函数更新较大数据结构中的值。
现在的问题是你没有自动获取记录字段的镜头,但你可以手动定义它们或使用Template Haskell自动生成它们。例如
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Testdata = Testdata { _record1::Int, _record2::Int, _record3::Int }
makeLenses ''Testdata
请注意,原始记录访问者以下划线为前缀,以便我们可以使用镜头的原始名称。
testRecord :: Testdata -> Testdata
testRecord =
recordArrow' record1 id
>>> recordArrow' record2 id
>>> recordArrow' record3 id
不需要unarrow
功能。最好通过简单的类型签名强制将泛型类型赋予具体类型。
请注意,如果您只是想要一种更好的链接记录操作的语法,并且没有任何其他箭头用途,您可能更喜欢使用带有镜头的State
monad。例如:
import Control.Monad.State (execState)
testRecord' :: Testdata -> Testdata
testRecord' = execState $ do
record1 .= 3
record2 %= (+5)
record3 += 2