在Scala中,我该如何将事件驱动编程与函数式编程相结合?

10

为了澄清我的事件驱动的含义,我指的是一个我有

def onTrade(...)

每当某只股票交易时都会调用它。假设我想跟踪每日最高交易价格。对我来说,显而易见的解决方案是:

var dailyHigh = 0

def onTrade(...) {
    if (price > dailyHigh) dailyHigh = price
}

有没有一种方法可以使用val来实现这个功能,而不是var?还要假设我将来可能想添加dailyLow、volumeHigh、volumeLow等。

5个回答

9

这篇弃用观察者模式的论文可能会引起您的兴趣,但我认为它所描述的库尚未可用。


@Didier,我使用那个链接得到了404错误。 - robert
谷歌建议 https://infoscience.epfl.ch/record/176887/files/DeprecatingObservers2012.pdf - Didier Dupont

8

其实没有太大问题。一个完整的解决方案可能会使用Reader、IO和State单子,以及Iteratee和lenses,但这里有一个更简单的版本:

case class State(dailyHigh: Int = 0)

object Main {
  type Event = (State => State)

  def mainLoop(currState: State, events: Stream[Event]): State =
    if (events.nonEmpty) {
      val newState = events.head(currState)
      mainLoop(newState, events.tail)
    } else currState

  def onTrade(price: Int): Event = (s: State) =>
    if (price > s.dailyHigh) s.copy(dailyHigh = price) else s

  def main(args: Array[String]) {
    val events = onTrade(5) #:: onTrade(2) #:: onTrade(10) #:: onTrade(5) #:: Stream.empty
    val finalState = mainLoop(State(), events)
    println(finalState)
  }
}

看,妈妈,没有变量!

状态可能会变得非常复杂,但这就是镜头的用武之地。使用镜头,很容易查询和更改(使用新值进行复制)任意复杂的数据结构。

对于事件来说,使用迭代器是自然而然的——从某种意义上讲,“onTrade”成为一个迭代器,由枚举器(“生成”事件的东西)在每个事件中调用。如果组合自部分函数,则可以将所有函数折叠到单个部分函数中。

另外,状态单子可以与IO单子结合在for-comprehensions。

最后,还有延续的选择。如果某些处理需要接收一系列事件,则每个事件的结果可以是延续,并且延续本身成为状态的一部分。


我刚开始接触函数式编程并试图学习其中的技巧(我的历史是过程化和面向对象的)。我很难理解为什么(或者是否)这种方法更好。我确实理解函数式编程中不可变性的基本原因,但这似乎要复杂得多!这只是因为我对它还很陌生吗? - MPT
2
@MPT 新颖性肯定起到了作用——可能是一个重要的作用。但不仅仅是这样……这些东西是抽象的,因此它们统一了许多在程序员的思维中并不相同的东西,并且隐藏了细节。处理抽象概念比处理具体事物更难。至于更好,它使程序更加可组合和易于测试。但是当你看小例子时,它似乎有点不合适。 - Daniel C. Sobral

2
有时需要自然地使用可变状态,以下是来自《Scala by Example》一书的示例。它也具有一些可变状态(maxBid,maxBidder)。因此,var并不总是一个坏主意。有时它能够正常工作。
   class Auction(seller: Actor, minBid: Int, closing: Date) extends Actor {
   val timeToShutdown = 36000000 // msec
   val bidIncrement = 10
   def act() {
      var maxBid = minBid - bidIncrement
      var maxBidder: Actor = null
      var running = true
      while (running) {
         receiveWithin((closing.getTime() - new Date().getTime())) {
            case Offer(bid, client) =>
               if (bid >= maxBid + bidIncrement) {
                  if (maxBid >= minBid) maxBidder ! BeatenOffer(bid)
                  maxBid = bid; maxBidder = client; client ! BestOffer
               } else {
                  client ! BeatenOffer(maxBid)
               }
            case Inquire(client) =>
               client ! Status(maxBid, closing)
            case TIMEOUT =>
               if (maxBid >= minBid) {
                  val reply = AuctionConcluded(seller, maxBidder)
                  maxBidder ! reply; seller ! reply
               } else {
                  seller ! AuctionFailed
               }
               receiveWithin(timeToShutdown) {
                  case Offer(_, client) => client ! AuctionOver
                  case TIMEOUT          => running = false
               }
         }
      }
   }
}

0

从未真正这样做过,但是你可以在流中创建新实例,而不是修改值。

其他进程可以迭代该流,在到达流的最后一个实例元素时等待。


还可以查看这个链接(http://en.wikipedia.org/wiki/Channel_%28programming%29),它基本上是Jens Schauder建议的相同解决方案。 - agilesteel

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接