Статьи

Набрал попросить Акку

Akka — отличный инструмент для написания распределенных приложений. Однако меня всегда удивляло то, что, будучи базирующимся на Scala, который является языком, очень безопасным для типов, элементарная конструкция в Akka — актер — на самом деле не является безопасной для типов. Вы можете отправить любое сообщение любому актеру и получить в ответ любой объект.

Предстоящий выпуск 2.2 Akka будет содержать экспериментальную реализацию типизированных каналов , которые используют макросы для обеспечения безопасности типов при взаимодействии акторов; но, прежде чем это будет окончательно, вот более простой и менее мощный подход, чтобы добавить некоторую типизацию в шаблон запроса .

Вы можете посмотреть на шаблон запроса как на вид асинхронного вызова метода: вы

отправить сообщение (его класс соответствует имени метода) с некоторыми аргументами и ожидать ответа (результат метода). Точнее говоря, мы возвращаем Future , которое в конечном итоге будет содержать ответ (если есть). Обратите внимание, что ответ также может иметь тип Unit , соответствующий методам, не возвращающим результат. Зная, когда такие «методы» завершены, все же может быть полезно.

Очень простой пример использования базового шаблона запроса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
import akka.pattern.ask
 
case class LookupUser(id: Int)
 
// the actor impl should send back a message to the sender
val userFuture = actor ? LookupUser(10)
 
// userFuture: Future[Any]
 
userFuture onSuccess {
   case result => {
      // do something with the result
      // result has type Any
   }
}

Не очень хорошая вещь здесь, что возвращаемый тип ? (см. реализацию AskSupport ) является Future[Any] , поскольку субъект может ответить любым сообщением. Однако в идеале при отправке LookupUser мы хотели бы получить Future[Option[User]] , при отправке UserCount a Future[Int] и так далее.

Это на самом деле довольно легко реализовать. Прежде всего, мы должны как-то вставить ожидаемый тип ответа в сообщение. Для этого мы можем использовать признак, который принимает тип ожидаемого ответа в качестве параметра типа:

1
trait Replyable[T]

Это можно использовать в сообщениях, которые мы отправляем актеру:

1
2
case class LookupUser(id: Int) extends Replyable[Option[User]]
case class UserCount() extends Replyable[Int]

Теперь нам нужен вариант ? который возвращает будущее с правильным параметром типа:

01
02
03
04
05
06
07
08
09
10
trait ReplySupport {
  implicit class ReplyActorRef(actorRef: ActorRef) {
    def ?[T](message: Replyable[T])
            (implicit timeout: Timeout, tag: ClassTag[T]): Future[T] = {
      akka.pattern.ask(actorRef, message).mapTo[T]
    }
  }
}
 
package object reply extends ReplySupport

Вы можете видеть, что мы просто повторно используем существующую реализацию Ask и сопоставляем получающееся будущее с правильным типом. ClassTag аут является неявным параметром ask и ClassTag mapTo , поэтому мы должны также включить их в сигнатуру.

Использование довольно простое, фактически оно почти такое же, как и раньше, за исключением импорта и того, что будущее имеет правильный тип:

01
02
03
04
05
06
07
08
09
10
11
12
import reply._
 
val userFuture = actor ? LookupUser(10)
 
// userFuture: Future[Option[User]]
 
userFuture onSuccess {
   case result => {
      // do something with the result
      // result has type Option[User]
   }
}

Это сторона актера-пользователя. А как насчет самого актера? Как гарантировать, что если актер получит сообщение типа Replyable[T] , он действительно ответит с помощью T ? Как обычно в Scala, ответом снова является черта, которую можно смешать в актера:

01
02
03
04
05
06
07
08
09
10
11
12
13
trait ReplyingActor extends Actor {
  def receive = {
    case m: Replyable[_] if receiveReplyable.isDefinedAt(m) => {
      try {
        sender ! receiveReplyable(m)
      } catch {
        case e: Exception => sender ! Failure(e)
      }
    }
  }
 
  def receiveReplyable[T]: PartialFunction[Replyable[T], T]
}

И пример использования:

1
2
3
4
5
6
class UserActor extends ReplyingActor {
   def receiveReplyable[T] = {
      case LookupUser(id) => Some(User(...))
      case UserCount() => 512
   }
}

Теперь все это хорошо проверено. Если бы мы попытались вернуть String в ветке UserCount , мы получили бы ошибку времени компиляции.