繁体   English   中英

在Haskell中编写有状态函数

[英]Composing Stateful functions in Haskell

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

有更优雅的方式吗?

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

computeShortTermEWMA start price = runState (computeEWMA 0.2 price) start

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

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

更一般地说,我根本没有看到你正在使用国家的东西。 我想你会用它来迭代股票价格,维持EWMA。 但是,我认为这不一定是最好的方法。 相反,我会考虑使用类似扫描的东西在股票价格列表上编写您的EWMA功能。 这应该使您的其他分析函数更容易实现,因为它们也只是列表函数。 (将来,如果你需要处理IO,你可以随时切换到Pipes这样的东西,它提供了一个非常类似于列表的界面。)

在这种特殊情况下,你有一个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

我不知道“推广”是否是一个好名字,我不记得看到这个功能在其他地方定义(但它可能已经在lens )。

您需要的证人在lens中命名为_2_3 ,因此,您可以更改几行代码,如下所示:

shortEWMA <- promote _2 (computeShortTermEWMA price)
longEWMA <- promote _3 (computeLongTermEWMA price)

如果Lens允许您关注内部值,那么这个组合器可能应该被称为blurBy(用于前缀应用)或模糊(用于中缀应用)。

对于这些简单的功能,根本不需要使用任何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*使用先前值的累积结果来计算每个连续值,所以它们是“有状态的”足以在这种情况下经常使用它们。

使用一个小型类魔术,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定义为一个EWMAState ,用term的类型标记(或者,它可以是两种不同的类型 - 但使用幻像类型作为标记有它的优点):

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM