简体   繁体   English

在Haskell中编写有状态函数

[英]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 . 如果我正确理解了您的代码,则不会在调用computeShortTermEWMAcomputeLongTermEWMA之间共享状态。 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封装在computeShortTermEWMAcomputeLongTermEWMA的定义中,因为它们是独立的自包含实体:

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 . GoldenCrossStateEWMAState不同的方式强调了这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.

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