Haskell-跟踪记录(初始)状态的更好方法

时间:2019-03-14 15:05:18

标签: haskell record lens

我正在处理一些需要记录并返回经过稍微修改的记录的函数。

例如

import Control.Lens ((%~), (^.), (&))

modifyRecord :: SomeRecord -> SomeRecord -> SomeRecord
modifyRecord baseR currentR = currentR & thisPart %~ (fmap (someFn someValue))
        where someValue = baseR ^. thisPart

函数modifyRecord带有两个相同类型的参数。

currentR是记录的当前状态

baseR是记录的基本状态

(即未应用任何功能,从未更改)


组成这种类型的几个函数意味着我必须组成部分函数,​​并列出它们的一部分

[fn1 baseState , fn2 baseState , fn3 baseState ... fnn baseState]

然后用currentState之类的功能折叠foldl (flip ($))

因此每个fnn baseState本身就是一个具有类型的函数 SomeRecord -> SomeRecord


我要做的是编写这些函数,以便它们仅采用记录的当前状态并自行确定基本状态。

所以

modifyRecord :: SomeRecord -> SomeRecord -> SomeRecord

modifyRecord :: SomeRecord -> SomeRecord

没有实际修改记录本身。

我想避免这样做

data SomeRecord = SomeRecord { value1 :: Float
                             , value1Base :: Float
                             , value2 :: Float
                             , value2Base :: Float
                             ...
                             ...
                             , valueN :: Float
                             , valueNBase :: Float
                             }

记录本身将具有基本值并在其上应用的功能将避免与*Base项进行交互。

有可能吗?

3 个答案:

答案 0 :(得分:3)

听起来像Reader单子的工作。

modifyRecord :: SomeRecord -> Reader SomeRecord SomeRecord
modifyRecord currentR = do
     baseR <- ask
     currentR & thisPart %~ (fmap (someFn someValue))
        where someValue = baseR ^. thisPart

您无需将baseR作为参数显式传递给每个函数,而是将其作为环境的一部分进行访问。

然后您可以编写类似的内容

runReader (foldl (>=>) return [fn1, fn2, ..., fnn] currentR) baseR
  1. foldl (>=>) return [fn1, fn2, ... fnn]将Kleisli箭头列表简化为单个箭头,就像foldl (.) id将普通函数列表组合为单个函数一样。

  2. foldl的结果应用于currentR会产生一个Reader SomeRecord SomeRecord值,该值只需要一个基本记录就可以“启动”对原始当前记录的修改链,并且产生最终结果。

    (步骤1和2概括了固定长度的链,例如return currentR >>= fn1 >>= fn2 >>= fn3。)

  3. runReader通过从Reader值中提取函数并将其应用于baseR来提供基本记录。

答案 1 :(得分:2)

将初始状态和当前状态放入一个元组中,并使用fmap来提升仅关心当前状态的函数:

ghci> :set -XTypeApplications
ghci> fmap @((,) SomeRecord) :: (a -> b) -> (SomeRecord, a) -> (SomeRecord, b)

但是,如果我们以(SomeRecord,SomeRecord) -> SomeRecord的形式给我们两个函数,而我们需要将它们组合起来怎么办?我们可以很容易地定义一个运算符,但是它已经存在吗?

碰巧,类型((,) e)有一个Comonad实例。这是一个非常简单的表象,它可以将值与某些环境配对-在我们的例子中,就是我们想要携带的原始值。

co-kleisli合成运算符=>=可用于链接两个(SomeRecord,SomeRecord) -> SomeRecord函数以及=>>,以将它们应用于初始配对值。

 ghci> import Control.Comonad
 ghci> (1,7) =>> ((\(o,c) -> c * 2) =>= (\(o,c) -> o + c))
 (1,15)

或者我们可以完全使用=>>

 ghci> (1,7) =>> (\(o,c) -> c * 2) =>> (\(o,c) -> o + c)
 (1,15)

使用翻转的fmap运算符<&>,我们甚至可以编写类似

的管道
 ghci> (1,2) =>> (\(x,y) -> y+2) <&> succ =>> (\(x,y) -> x + y)
 (1,6)  

我们还可以使用extract来获取当前值,这可能比snd更好地显示了意图。

答案 2 :(得分:0)

从广义上讲,不行,这是不可能的:函数必须显式声明其所有输入。可能最干净的方法是使用concatM组合您的功能。您将需要翻转他们的参数,以便未修改的记录排在最后,而不是排在第一;完成后,您将拥有

concatM [f1, f2, f3, f4] :: SomeRecord -> SomeRecord -> SomeRecord

根据需要。仅将两个这样的功能组合在一起,就有

(>=>) ::
    (SomeRecord -> SomeRecord -> SomeRecord) ->
    (SomeRecord -> SomeRecord -> SomeRecord) ->
    (SomeRecord -> SomeRecord -> SomeRecord)

in base; f >=> g将首先对f进行修改,然后对g进行修改。如果您更喜欢其他顺序,则为了更接近(.)的行为,还有一个(<=<)