[英]Composing Stateful functions in Haskell
What is the simplest Haskell library that allows composition of stateful functions? 什么是最简单的Haskell库,它允许组成有状态函数?
We can use the State
monad to compute a stock's exponentially-weighted moving average as follows: 我们可以使用State
monad来计算股票的指数加权移动平均值,如下所示:
import Control.Monad.State.Lazy
import Data.Functor.Identity
type StockPrice = Double
type EWMAState = Double
type EWMAResult = Double
computeEWMA :: Double -> StockPrice -> State EWMAState EWMAResult
computeEWMA α price = do oldEWMA <- get
let newEWMA = α * oldEWMA + (1.0 - α) * price
put newEWMA
return newEWMA
However, it's complicated to write a function that calls other stateful functions. 但是,编写一个调用其他有状态函数的函数很复杂。 For example, to find all data points where the stock's short-term average crosses its long-term average, we could write: 例如,要找到股票的短期平均值超过其长期平均值的所有数据点,我们可以写:
computeShortTermEWMA = computeEWMA 0.2
computeLongTermEWMA = computeEWMA 0.8
type CrossingState = Bool
type GoldenCrossState = (CrossingState, EWMAState, EWMAState)
checkIfGoldenCross :: StockPrice -> State GoldenCrossState String
checkIfGoldenCross price = do (oldCrossingState, oldShortState, oldLongState) <- get
let (shortEWMA, newShortState) = runState (computeShortTermEWMA price) oldShortState
let (longEWMA, newLongState) = runState (computeLongTermEWMA price) oldLongState
let newCrossingState = (shortEWMA < longEWMA)
put (newCrossingState, newShortState, newLongState)
return (if newCrossingState == oldCrossingState then
"no cross"
else
"golden cross!")
Since checkIfGoldenCross calls computeShortTermEWMA and computeLongTermEWMA, we must manually wrap/unwrap their states. 由于checkIfGoldenCross调用computeShortTermEWMA和computeLongTermEWMA,我们必须手动包装/解包它们的状态。
Is there a more elegant way? 有更优雅的方式吗?
If I understood your code correctly, you don't share state between the call to computeShortTermEWMA
and computeLongTermEWMA
. 如果我正确理解了您的代码,则不会在调用computeShortTermEWMA
和computeLongTermEWMA
之间共享状态。 They're just two entirely independent functions which happen to use state internally themselves. 它们只是两个完全独立的函数,它们碰巧在内部使用状态。 In this case, the elegant thing to do would be to encapsulate runState
in the definitions of computeShortTermEWMA
and computeLongTermEWMA
, since they're separate self-contained entities: 在这种情况下,要做的优雅事情是将runState
封装在computeShortTermEWMA
和computeLongTermEWMA
的定义中,因为它们是独立的自包含实体:
computeShortTermEWMA start price = runState (computeEWMA 0.2 price) start
All this does is make the call site a bit neater though; 所有这一切都是为了使呼叫网站更整洁; I just moved the runState
into the definition. 我刚刚将runState
移动到了定义中。 This marks the state a local implementation detail of computing the EWMA, which is what it really is. 这标志着状态是计算EWMA的本地实现细节,这就是它的真实含义。 This is underscored by the way GoldenCrossState
is a different type from EWMAState
. GoldenCrossState
与EWMAState
不同的方式强调了这EWMAState
。
In other words, you're not really composing stateful functions; 换句话说,你并没有真正构成有状态函数; rather, you're composing functions that happen to use state inside. 相反,你正在编写碰巧使用内部状态的函数。 You can just hide that detail. 你可以隐藏这个细节。
More generally, I don't really see what you're using the state for at all. 更一般地说,我根本没有看到你正在使用国家的东西。 I suppose you would use it to iterate through the stock price, maintaining the EWMA. 我想你会用它来迭代股票价格,维持EWMA。 However, I don't think this is necessarily the best way to do it. 但是,我认为这不一定是最好的方法。 Instead, I would consider writing your EWMA function over a list of stock prices, using something like a scan. 相反,我会考虑使用类似扫描的东西在股票价格列表上编写您的EWMA功能。 This should make your other analysis functions easier to implement, since they'll just be list functions as well. 这应该使您的其他分析函数更容易实现,因为它们也只是列表函数。 (In the future, if you need to deal with IO, you can always switch over to something like Pipes which presents an interface really similar to lists.) (将来,如果你需要处理IO,你可以随时切换到Pipes这样的东西,它提供了一个非常类似于列表的界面。)
In this particular case, you have a y -> (a, y)
and a z -> (b, z)
that you want to use to compose a (x, y, z) -> (c, (x, y, z))
. 在这种特殊情况下,你有一个y -> (a, y)
和一个z -> (b, z)
,你想用来组成一个(x, y, z) -> (c, (x, y, z))
。 Having never used lens
before, this seems like a perfect opportunity. 从未使用过lens
,这似乎是一个绝佳的机会。
In general, we can promote a stateful operations on a sub-state to operate on the whole state like this: 一般来说,我们可以在子状态上提升状态操作,以便在整个状态下运行,如下所示:
promote :: Lens' s s' -> StateT s' m a -> StateT s m a
promote lens act = do
big <- get
let little = view lens big
(res, little') = runState act little
big' = set lens little' big
put big'
return res
-- Feel free to golf and optimize, but this is pretty readable.
Our lens a witness that s'
is a sub-state of s
. 我们的镜头见证s'
是的子状态s
。
I don't know if "promote" is a good name, and I don't recall seeing this function defined elsewhere (but it's probably already in lens
). 我不知道“推广”是否是一个好名字,我不记得看到这个功能在其他地方定义(但它可能已经在lens
)。
The witnesses you need are named _2
and _3
in lens
so, you could change a couple of lines of code to look like: 您需要的证人在lens
中命名为_2
和_3
,因此,您可以更改几行代码,如下所示:
shortEWMA <- promote _2 (computeShortTermEWMA price)
longEWMA <- promote _3 (computeLongTermEWMA price)
If a Lens
allows you to focus on inner values, maybe this combinator should be called blurredBy (for prefix application) or obscures (for infix application). 如果Lens
允许您关注内部值,那么这个组合器可能应该被称为blurBy(用于前缀应用)或模糊(用于中缀应用)。
There is really no need to use any monad at all for these simple functions. 对于这些简单的功能,根本不需要使用任何monad。 You're (ab)using the State
monad to calculate a one-off result in computeEWMA
when there is no state involved. 当没有涉及状态时,你(ab)使用State
monad来计算computeEWMA
的一次性结果。 The only line that is actually important is the formula for EWMA, so let's pull that into it's own function. 实际上唯一重要的是EWMA的公式,所以让我们把它拉进它自己的功能。
ewma :: Double -> Double -> Double -> Double
ewma a price t = a * t + (1 - a) * price
If you inline the definition of State
and ignore the String
values, this next function has almost the exact same signature as your original checkIfGoldenCross
! 如果你内联State
的定义并忽略String
值,那么下一个函数几乎与原始的checkIfGoldenCross
具有完全相同的签名!
type EWMAState = (Bool, Double, Double)
ewmaStep :: Double -> EWMAState -> EWMAState
ewmaStep price (crossing, short, long) =
(crossing == newCrossing, newShort, newLong)
where newCrossing = newShort < newLong
newShort = ewma 0.2 price short
newLong = ewma 0.8 price long
Although it doesn't use the State
monad, we're certainly dealing with state here. 虽然它没有使用State
monad,但我们肯定在这里处理州。 ewmaStep
takes a stock price, the old EWMAState
and returns a new EWMAState
. ewmaStep
获取股票价格,旧的EWMAState
并返回一个新的EWMAState
。
Now putting it all together with scanr :: (a -> b -> b) -> b -> [a] -> [b]
现在将它与scanr :: (a -> b -> b) -> b -> [a] -> [b]
放在一起
-- a list of stock prices
prices = [1.2, 3.7, 2.8, 4.3]
_1 (a, _, _) = a
main = print . map _1 $ scanr ewmaStep (False, 0, 0) prices
-- [False, True, False, True, False]
Because fold*
and scan*
use the cumulative result of previous values to compute each successive one, they are "stateful" enough that they can often be used in cases like this. 因为fold*
和scan*
使用先前值的累积结果来计算每个连续值,所以它们是“有状态的”足以在这种情况下经常使用它们。
With a little type class magic, monad transformers allow you to have nested transformers of the same type. 使用一个小型类魔术,monad变换器允许你拥有相同类型的嵌套变换器。 First, you will need a new instance for MonadState
: 首先,您需要MonadState
的新实例:
{-# LANGUAGE
UndecidableInstances
, OverlappingInstances
#-}
instance (MonadState s m, MonadTrans t, Monad (t m)) => MonadState s (t m) where
state f = lift (state f)
Then you must define your EWMAState
as a newtype, tagged with the type of term (alternatively, it could be two different types - but using a phantom type as a tag has its advantages): 然后你必须将你的EWMAState
定义为一个EWMAState
,用term的类型标记(或者,它可以是两种不同的类型 - 但使用幻像类型作为标记有它的优点):
data Term = ShortTerm | LongTerm
type StockPrice = Double
newtype EWMAState (t :: Term) = EWMAState Double
type EWMAResult = Double
type CrossingState = Bool
Now, computeEWMA
works on an EWMASTate
which is polymorphic in term (the afformentioned example of tagging with phantom types), and in monad: 现在, computeEWMA
适用于EWMASTate
,它在术语上是多态的(用幻像类型标记的上述例子),在monad中:
computeEWMA :: (MonadState (EWMAState t) m) => Double -> StockPrice -> m EWMAResult
computeEWMA a price = do
EWMAState old <- get
let new = a * old + (1.0 - a) * price
put $ EWMAState new
return new
For specific instances, you give them monomorphic type signatures: 对于特定实例,您可以为它们提供单形类型签名:
computeShortTermEWMA :: (MonadState (EWMAState ShortTerm) m) => StockPrice -> m EWMAResult
computeShortTermEWMA = computeEWMA 0.2
computeLongTermEWMA :: (MonadState (EWMAState LongTerm) m) => StockPrice -> m EWMAResult
computeLongTermEWMA = computeEWMA 0.8
Finally, your function: 最后,你的功能:
checkIfGoldenCross ::
( MonadState (EWMAState ShortTerm) m
, MonadState (EWMAState LongTerm) m
, MonadState CrossingState m) =>
StockPrice -> m String
checkIfGoldenCross price = do
oldCrossingState <- get
shortEWMA <- computeShortTermEWMA price
longEWMA <- computeLongTermEWMA price
let newCrossingState = shortEWMA < longEWMA
put newCrossingState
return (if newCrossingState == oldCrossingState then "no cross" else "golden cross!")
The only downside is you have to explicitly give a type signature - in fact, the instance we introduced at the beginning has ruined all hopes of good type errors and type inference for cases where you have multiple copies of the same transformer in a stack. 唯一的缺点是你必须明确地给出一个类型签名 - 事实上,我们在开始时引入的实例已经破坏了良好类型错误和类型推断的所有希望,在这种情况下,堆栈中有相同变换器的多个副本。
Then a small helper function: 然后一个小帮手功能:
runState3 :: StateT a (StateT b (State c)) x -> a -> b -> c -> ((a , b , c) , x)
runState3 sa a b c = ((a' , b', c'), x) where
(((x, a'), b'), c') = runState (runStateT (runStateT sa a) b) c
and: 和:
>runState3 (checkIfGoldenCross 123) (shortTerm 123) (longTerm 123) True
((EWMAState 123.0,EWMAState 123.0,False),"golden cross!")
>runState3 (checkIfGoldenCross 123) (shortTerm 456) (longTerm 789) True
((EWMAState 189.60000000000002,EWMAState 655.8000000000001,True),"no cross")
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.