如何在纯粹功能的同时通过隐藏/内部状态强制执行合同?

时间:2017-08-27 06:45:56

标签: scala io functional-programming state

用例

网站的访问者可以通过提供他们的电子邮件地址和消息向我发送电子邮件。为了避免垃圾邮件,在限制速率之前,每分钟只允许发送2封电子邮件(任意)。

请注意,这是一个让我更习惯于函数式编程实践的学习练习,所以虽然这看起来有些过分,但我可以将它扩展到更复杂的系统。

实施

为此,我到目前为止的ContactMailer类暴露了一种方法send,该方法根据提供的信息发送电子邮件。它处理所有准备邮件/电子邮件,执行速率限制合同,并实际发送电子邮件(使用courier):

import courier._, Defaults._
import scala.collection.mutable.Queue
import scala.concurrent.Future

case class ContactMailer(host: String, port: Int, username: String, password: String) {
  private val mailer = Mailer(host, port).auth(true).as(username, password).startTtls(true)()
  private val envelope = Envelope.from("no-reply" `@` "example.com").to("astorije" `@` "example.com")
  private val queue = Queue[Long]()

  def send(from: String, subject: String, content: String): Future[String] = {
    val now = System.currentTimeMillis()

    // If queue is full and oldest known message is < 1 minute ago, rate
    // limit, otherwise send email, drop oldest known timestamp and enqueue
    // the new one
    if (queue.length == 2) {
      val oldest = queue.head

      if (now - oldest < 60 * 1000) return Future("message rate limited")
      else queue.dequeue()
    }
    queue.enqueue(now)

    mailer(envelope.replyTo(from.addr).subject(subject).content(Text(content)))
      .map(_ => "message delivered")
      .recover { case ex => s"message failed: ${ex}" }
  }
}

消费者(应用程序的另一部分)将其称为:

scala> val mailer = ContactMailer("smtp.example.com", 25, "username", "password")
mailer: ContactMailer = ContactMailer(smtp.example.com,25,username,password)

scala> mailer.send("foo@bar.com", "One", "...").foreach(println(_))
message delivered

scala> mailer.send("foo@bar.com", "Two", "...").foreach(println(_))
message delivered

scala> mailer.send("foo@bar.com", "Three", "...").foreach(println(_))
message rate limited

问题

完美,它有效。但是因为到目前为止我已经学会并专门使用OOP,所以它具有以下所有特征:可变状态,副作用,没有参考透明度,因为使用相同的输入多次调用会导致不同的输出等。

  • 如何将此速率限制保持为纯功能编程风格?
  • 如果我在邮件程序之外提取此内部状态(时间戳queue)并请求消费者提供它,例如def send(previousQueue: Queue[Long], from: String, ...): (Queue[Long], Future[String]),我该如何确保消费者始终遵守此速率限制并且不发送空队列永远不会限速?
  • 有没有办法让ContactMailer专注于它应该做什么(发送电子邮件),并将此速率限制提取到一个不太专业的层(一般速率限制器,无论它试图限制什么)?首先是它的好主意吗?
  • 我为此阅读了很多通用方法,但仍然不知道该选择什么:IO monad?国家monad?免费monad?演员系统?在我看来,最后一个只会改变问题而在这种有限的背景下是不合适的。
  • 一般来说,FP方式对这个用例有什么好的结构?

总的来说,我不知道如何处理这个问题。那里有很多资源,但它们要么过于简单化,要么解释在现实世界中难以应用的基础知识,或者对我将其转化为这个例子的小经验过于抽象和理论化。

由于我自己控制整个操作序列,所以我显然可以完全自由地更新类(s)/函数的签名。

我希望这不会被标记为基于意见的。我明白为什么会有这种感觉,但我实际上仍然坚持如何更好,更具体的实施。 :)

1 个答案:

答案 0 :(得分:1)

您的系统与外界有两次互动,根据时间发送电子邮件和限速操作。这两种相互作用都有副作用。

最简单的建模方法是外部服务使用IO monad。

// Assuming sending email cannot fail, IO[Either[EmailError, Unit]] otherwise
def sendEmail(email: Email): IO[Unit]

def rateLimit(): IO[Boolean]

您的实施现在只需&#34;结合&#34;这两个:

case object RateLimitReached

def mySend(email: Email): IO[Either[RateLimitReached.type, Unit]] =
  for {
    token  <- rateLimit()
    result <- if (token)
                IO(Left(RateLimitReached))
              else
                sendEmail(email).map(Right)
  } yield result

如果您立即致电runAsync(假设您使用cats.effect.IO),则可以使用mySend代替Future,这两者在所有意图和目的上都是等同的。

  

如何将这个速率限制保持在纯函数编程风格中?

速率限制是读取时间(副作用)并改变本地计数器。你可以用状态monad来做后者,但这样会在Scala中过度设计IMO。

  

我为此阅读了很多通用方法,但仍然不知道可以选择什么:IO monad?国家monad?免费monad?演员系统?在我看来,最后一个只会改变问题而在这种有限的背景下是不合适的。

IO是默认答案。演员基于IO函数类型,所以如果你喜欢纯度和类型安全,你就不会成为演员的朋友。 Any => Unit vs Free的主要好处是能够获得更精确的类型签名。返回IO意味着您可以在内部执行任何操作,IO[Result]您可以尽可能精确地使用:

Free

但是,额外的间接水平带来了一些复杂性和许多样板,因为最终所有东西仍然需要被解释为def sendEmail(email: Email): Free[EmailEffect, Unit] def rateLimit(): Free[ReadTheTime, Boolean] // | | // list of side effect | // return type type MyEffect[X] = Either[EmailEffect[X], ReadTheTime[X]] def mySend(email: Email): Free[MyEffect, Either[RateLimitReached.type, Unit]] =