什么是允许组合有状态函数的最简单的Haskell库?
我们可以使用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
但是,编写一个调用其他有状态函数的函数很复杂。 例如,要查找股票短期平均值超过其长期平均值的所有数据点,我们可以写道:
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!")
由于checkIfGoldenCross调用computeShortTermEWMA和computeLongTermEWMA,我们必须手动包装/解包它们的状态。
有更优雅的方式吗?
答案 0 :(得分:4)
如果我正确理解了您的代码,则您不会在致电computeShortTermEWMA
和computeLongTermEWMA
之间分享状态。它们只是两个完全独立的函数,它们碰巧在内部使用状态。在这种情况下,优雅的事情是将runState
封装在computeShortTermEWMA
和computeLongTermEWMA
的定义中,因为它们是独立的自包含实体:
computeShortTermEWMA start price = runState (computeEWMA 0.2 price) start
所有这一切都是为了使呼叫网站更整洁;我刚刚将runState
移到了定义中。这标志着状态是计算EWMA的本地实现细节,这就是它的真实含义。 GoldenCrossState
与EWMAState
不同的类型强调了这一点。
换句话说,你并没有真正构成有状态的功能;相反,你正在编写碰巧使用内部状态的函数。你可以隐藏这个细节。
更一般地说,我根本没有看到你正在使用国家的东西。我想你会用它来迭代股票价格,维持EWMA。但是,我认为这不一定是最好的方法。相反,我会考虑使用类似扫描的东西在股票价格列表上编写您的EWMA功能。这应该使您的其他分析函数更容易实现,因为它们也只是列表函数。 (将来,如果你需要处理IO,你可以随时切换到Pipes这样的东西,它提供了一个与列表非常相似的界面。)
答案 1 :(得分:2)
在这种特殊情况下,您要使用y -> (a, y)
和z -> (b, z)
来撰写(x, y, z) -> (c, (x, y, z))
。从未使用lens
之前,这似乎是一个绝佳的机会。
一般情况下,我们可以在子状态上推广有状态操作,以便对整个状态进行操作:
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.
我们的镜头证明s'
是s
的子状态。
我不知道&#34;是否推广&#34;是一个好名字,我不记得在其他地方看到过这个功能(但它可能已经在lens
中)。
您需要的证人在_2
中被命名为_3
和lens
,因此,您可以更改几行代码:
shortEWMA <- promote _2 (computeShortTermEWMA price)
longEWMA <- promote _3 (computeLongTermEWMA price)
如果Lens
允许你专注于内部值,那么这个组合子可能应该被称为blurBy(用于前缀应用)或模糊(用于中缀应用)。
答案 2 :(得分:2)
对于这些简单的功能,根本不需要使用任何monad。当没有涉及任何州时,您(ab)使用State
monad计算computeEWMA
中的一次性结果。实际上唯一重要的是EWMA的公式,所以让我们把它拉进它自己的功能。
ewma :: Double -> Double -> Double -> Double
ewma a price t = a * t + (1 - a) * price
如果您列出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
虽然它没有使用State
monad,但我们肯定会在这里处理状态。 ewmaStep
获取股票价格,旧版EWMAState
并返回新的EWMAState
。
现在将所有内容与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]
由于fold*
和scan*
使用先前值的累积结果来计算每个连续值,因此它们是#34;有状态的&#34;足以在这种情况下经常使用它们。
答案 3 :(得分:0)
使用一点类型魔法,monad变换器允许你拥有相同类型的嵌套变换器。首先,您需要MonadState
的新实例:
{-# LANGUAGE
UndecidableInstances
, OverlappingInstances
#-}
instance (MonadState s m, MonadTrans t, Monad (t m)) => MonadState s (t m) where
state f = lift (state f)
然后,您必须将EWMAState
定义为新类型,标记为术语类型(或者,它可以是两种不同的类型 - 但使用幻像类型作为标记有其优势):
data Term = ShortTerm | LongTerm
type StockPrice = Double
newtype EWMAState (t :: Term) = EWMAState Double
type EWMAResult = Double
type CrossingState = Bool
现在,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
对于特定实例,您可以为它们提供单形类型签名:
computeShortTermEWMA :: (MonadState (EWMAState ShortTerm) m) => StockPrice -> m EWMAResult
computeShortTermEWMA = computeEWMA 0.2
computeLongTermEWMA :: (MonadState (EWMAState LongTerm) m) => StockPrice -> m EWMAResult
computeLongTermEWMA = computeEWMA 0.8
最后,你的功能:
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!")
唯一的缺点是你必须明确地给出一个类型签名 - 事实上,我们在开始时引入的实例破坏了所有希望的良好类型错误和类型推断,以防你在同一个变换器中有多个副本叠加。
然后是一个小辅助函数:
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
和
>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")