我正在研究一些记录并返回略微修改记录的函数。
例如
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
项目交互。
这可能吗?
听起来像Reader
monad的工作。
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
foldl (>=>) return [fn1, fn2, ... fnn]
将Kleisli箭头列表缩减为单个箭头,就像foldl (.) id
将普通函数列表组合成单个函数一样。foldl
的结果应用于currentR
会产生一个Reader SomeRecord SomeRecord
值,只需要一个基本记录来“启动”对原始当前记录的修改链并产生最终结果。
(步骤1和2概括了像return currentR >>= fn1 >>= fn2 >>= fn3
这样的固定长度链。)runReader
通过从Reader
值中提取函数并将其应用于baseR
来提供该基本记录。将初始状态和当前状态放在元组中,并使用fmap
来提升仅关注当前状态的函数:
ghci> :set -XTypeApplications
ghci> fmap @((,) SomeRecord) :: (a -> b) -> (SomeRecord, a) -> (SomeRecord, b)
但是如果我们以(SomeRecord,SomeRecord) -> SomeRecord
的形式给出两个函数并且我们需要编写它们呢?我们可以很容易地定义一个运算符,但它已经存在于某个地方吗?
碰巧,类型((,) e)
有一个Comonad
实例。这是一个非常简单的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
更好地显示意图。
从广义上讲,不,这是不可能的:函数必须明确声明所有输入。可能最干净的方法是使用concatM
来组合你的功能。您将需要翻转他们的参数,以便未修改的记录最后,而不是第一个;一旦你这样做,你就会有
concatM [f1, f2, f3, f4] :: SomeRecord -> SomeRecord -> SomeRecord
按要求。对于仅仅组合两个这样的功能,有
(>=>) ::
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord)
qazxsw poi; qazxsw poi将首先执行qazxsw poi的修改,然后执行in base
的修改。如果您更喜欢其他订单,更接近f >=> g
的行为,还有一个f
。