Как я могу объединить свободные интерфейсы с функциональным стилем в Scala? - PullRequest
6 голосов
/ 30 декабря 2011

Я читал о подходе OO «плавный интерфейс» в Java , JavaScript и Scala , и мне нравится его внешний вид, но яя изо всех сил пытался понять, как это согласовать с более функциональным подходом в Scala на основе типов.

Чтобы привести очень конкретный пример того, что я имею в виду: я написал клиент API, который можно вызывать следующим образом.:

val response = MyTargetApi.get("orders", 24)

Возвращаемое значение из get() - это тип Tuple3, называемый RestfulResponse, как определено в моем объекте пакета :

// 1. Return code
// 2. Response headers
// 2. Response body (Option)
type RestfulResponse = (Int, List[String], Option[String])

Это прекрасно работает - и я не хочу жертвовать функциональной простотой возвращаемого значения кортежа - но я хотел бы расширить библиотеку с помощью различных «быстрых» вызовов методов, возможно, что-то вроде этого:

val response = MyTargetApi.get("customers", 55).throwIfError()
// Or perhaps:
MyTargetApi.get("orders", 24).debugPrint(verbose=true)

Как я могу объединить функциональную простоту get(), возвращая типизированный кортеж (или аналогичный) с возможностью добавления более «плавных» возможностей в мой API?

Ответы [ 3 ]

7 голосов
/ 30 декабря 2011

Похоже, вы имеете дело с клиентским API для общения в стиле отдыха. Ваш get метод, по-видимому, запускает фактический цикл запрос / ответ. Похоже, вам придется иметь дело с этим:

  • свойства транспорта (такие как учетные данные, уровень отладки, обработка ошибок)
  • предоставление данных для ввода (ваш id и тип записи (заказ или заказчик)
  • что-то делает с результатами

Я думаю, что для свойств транспорта вы можете поместить некоторые из них в конструктор объекта MyTargetApi, но вы также можете создать объект query , который будет хранить их для одного запроса. и может быть установлен беглый способ, используя query() метод:

MyTargetApi.query().debugPrint(verbose=true).throwIfError()

Это вернуло бы некоторый объект Query с состоянием, который хранит значение для уровня журнала, обработки ошибок. Для предоставления данных для ввода вы также можете использовать объект запроса для установки этих значений, но вместо того, чтобы возвращать свой ответ, возвращайте QueryResult:

class Query {
  def debugPrint(verbose: Boolean): this.type = { _verbose = verbose; this }
  def throwIfError(): this.type = { ... }
  def get(tpe: String, id: Int): QueryResult[RestfulResponse] =
    new QueryResult[RestfulResponse] {
       def run(): RestfulResponse = // code to make rest call goes here
    }
}

trait QueryResult[A] { self =>
  def map[B](f: (A) => B): QueryResult[B] = new QueryResult[B] {
    def run(): B = f(self.run())
  }
  def flatMap[B](f: (A) => QueryResult[B]) = new QueryResult[B] {
    def run(): B = f(self.run()).run()
  }
  def run(): A
}

Затем, чтобы в итоге получить результаты, вы звоните run. Поэтому в конце дня вы можете назвать это так:

MyTargetApi.query()
  .debugPrint(verbose=true)
  .throwIfError()
  .get("customers", 22)
  .map(resp => resp._3.map(_.length)) // body
  .run()

Это должен быть подробный запрос с ошибкой при выдаче, поиск клиентов с идентификатором 22, сохранение тела и получение его длины в виде Option[Int].

Идея состоит в том, что вы можете использовать map, чтобы определить вычисления для результата, которого у вас еще нет. Если мы добавим flatMap к нему, то вы также можете объединить два вычисления из двух разных запросов.

3 голосов
/ 30 декабря 2011

Честно говоря, я думаю, что это звучит так, как будто вам нужно немного поближе познакомиться, потому что пример не явно функциональный и не очень беглый. Кажется, вы можете смешивать беглость с неидемпотентным в том смысле, что ваш debugPrint метод, вероятно, выполняет ввод-вывод, а throwIfError вызывает исключения. Это то, что вы имеете в виду?

Если вы имеете в виду, функционален ли Stateful Builder , ответ будет «не в чистом смысле». Однако обратите внимание, что сборщик не обязательно должен быть с состоянием.

case class Person(name: String, age: Int)

Во-первых; это может быть создано с использованием именованных параметров:

Person(name="Oxbow", age=36)

Или строитель без гражданства:

object Person {
  def withName(name: String) 
    = new { def andAge(age: Int) = new Person(name, age) } 
}

Привет, до:

scala> Person withName "Oxbow" andAge 36

Что касается использования нетипизированных строк для определения запроса, который вы делаете; это плохая форма в статически типизированном языке. Более того, нет необходимости:

sealed trait Query
case object orders extends Query

def get(query: Query): Result

Привет, preto:

api get orders

Хотя я думаю, что это плохая идея - у вас не должно быть единственного метода, который мог бы дать вам условно совершенно разные типы результатов

<Ч />

В заключение: лично я думаю, что нет никакой причины, по которой беглость и функционал не могут смешиваться, поскольку функционал просто указывает на отсутствие изменяемого состояния и сильное предпочтение идемпотентных функций для выполнения вашей логики.

Вот один для вас:

args.map(_.toInt)

args map toInt

Я бы сказал, что второе более свободно. Это возможно, если вы определите:

val toInt = (_ : String).toInt

То есть; если вы определите функцию. Я нахожу функции и беглость речи очень хорошо в Scala.

0 голосов
/ 30 декабря 2011

Вы можете попробовать заставить get () вернуть объект-обертку, который может выглядеть примерно так

type RestfulResponse = (Int, List[String], Option[String])

class ResponseWrapper(private rr: RestfulResponse /* and maybe some flags as additional arguments, or something? */) {

    def get : RestfulResponse = rr

    def throwIfError : RestfulResponse = {
        // Throw your exception if you detect an error
        rr    // And return the response if you didn't detect an error
    }

    def debugPrint(verbose: Boolean, /* whatever other parameters you had in mind */) {
        // All of your debugging printing logic
    }

    // Any and all other methods that you want this API response to be able to execute

}

По сути, это позволяет вам поместить ваш ответ в контейнер, содержащий все эти приятные методы, которые вы хотите, и, если вы просто хотите получить упакованный ответ, вы можете просто вызвать метод get () оболочки.

Конечно, недостатком этого является то, что вам нужно будет немного изменить свой API, если это вас вообще беспокоит. Ну ... вы, вероятно, могли бы избежать необходимости менять свой API, фактически, если бы вы вместо этого создали неявное преобразование из RestfulResponse в ResponseWrapper и наоборот. Это стоит рассмотреть.

...