在Haskell中编写有状态函数

时间:2014-07-18 20:58:48

标签: haskell state

什么是允许组合有状态函数的最简单的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,我们必须手动包装/解包它们的状态。

有更优雅的方式吗?

4 个答案:

答案 0 :(得分:4)

如果我正确理解了您的代码,则您不会在致电computeShortTermEWMAcomputeLongTermEWMA之间分享状态。它们只是两个完全独立的函数,它们碰巧在内部使用状态。在这种情况下,优雅的事情是将runState封装在computeShortTermEWMAcomputeLongTermEWMA的定义中,因为它们是独立的自包含实体:

computeShortTermEWMA start price = runState (computeEWMA 0.2 price) start

所有这一切都是为了使呼叫网站更整洁;我刚刚将runState移到了定义中。这标志着状态是计算EWMA的本地实现细节,这就是它的真实含义。 GoldenCrossStateEWMAState不同的类型强调了这一点。

换句话说,你并没有真正构成有状态的功能;相反,你正在编写碰巧使用内部状态的函数。你可以隐藏这个细节。

更一般地说,我根本没有看到你正在使用国家的东西。我想你会用它来迭代股票价格,维持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中被命名为_3lens,因此,您可以更改几行代码:

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")