Haskell - 跟踪记录(初始)状态的更好方法
Haskell - A better way of keeping track of the (initial) state of a record
我正在开发一些记录和 return 稍微修改记录的功能。
例如
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
项交互。
这可能吗?
从广义上讲,不,那是不可能的:函数必须显式声明它们的所有输入。可能最简洁的方法是使用 concatM
来组合您的功能。您将需要翻转他们的论点,以便未修改的记录排在最后,而不是第一个;一旦你这样做了,那么你将拥有
concatM [f1, f2, f3, f4] :: SomeRecord -> SomeRecord -> SomeRecord
根据需要。对于仅组合两个这样的函数,有
(>=>) ::
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord)
in base
; f >=> g
将首先执行 f
的修改,然后是 g
的修改。如果你更喜欢其他顺序,更接近 (.)
的行为,还有一个 (<=<)
.
听起来像是 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 组合运算符 =>=
can be used to chain two (SomeRecord,SomeRecord) -> SomeRecord
functions, along with =>>
将它们应用于初始配对值。
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
更能体现意图。
我正在开发一些记录和 return 稍微修改记录的功能。
例如
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
项交互。
这可能吗?
从广义上讲,不,那是不可能的:函数必须显式声明它们的所有输入。可能最简洁的方法是使用 concatM
来组合您的功能。您将需要翻转他们的论点,以便未修改的记录排在最后,而不是第一个;一旦你这样做了,那么你将拥有
concatM [f1, f2, f3, f4] :: SomeRecord -> SomeRecord -> SomeRecord
根据需要。对于仅组合两个这样的函数,有
(>=>) ::
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord) ->
(SomeRecord -> SomeRecord -> SomeRecord)
in base
; f >=> g
将首先执行 f
的修改,然后是 g
的修改。如果你更喜欢其他顺序,更接近 (.)
的行为,还有一个 (<=<)
.
听起来像是 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 组合运算符 =>=
can be used to chain two (SomeRecord,SomeRecord) -> SomeRecord
functions, along with =>>
将它们应用于初始配对值。
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
更能体现意图。