Как в Scala совместить плавные интерфейсы с функциональным стилем?

Я читал о подходе 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?


person Alex Dean    schedule 30.12.2011    source источник
comment
Такой гибкий интерфейс ... не очень дружелюбен по отношению к языкам со статической типизацией. Хотя я уверен, что с помощью некоторого кода оболочки и наследования вы, вероятно, могли бы сделать это с помощью Scala, это будет не так безопасно, как если бы getOrders и getCustomers по отдельности вместо get("orders") и get("customers") с использованием того же метода get.   -  person Dan Burton    schedule 30.12.2011
comment
Спасибо, Дэн, но я бы не стал слишком беспокоиться о синтаксисе get("slug", id) - мой вопрос не в этом. В любом случае в библиотеке есть еще один более безопасный режим, который выглядит как MyTargetApi.orders.get(id)   -  person Alex Dean    schedule 30.12.2011
comment
Лично я считаю, что вам следует предложить более представительный пример некоторого свободного кода и того, какой именно бит, по вашему мнению, не работает. На данный момент из вашего вопроса просто явствует, что вы действительно не знаете, что означает беглость   -  person oxbow_lakes    schedule 30.12.2011
comment
Почему бы не использовать Either? stackoverflow.com/ вопросы / 1193333 /   -  person earldouglas    schedule 30.12.2011
comment
Спасибо, Джеймс - Either - интересная идея. Это то, что я буду использовать где-нибудь в моей библиотеке, потому что orders.get() ответ после сортировки будет Either[ErrorRepresentation, OrderRepresentation]   -  person Alex Dean    schedule 31.12.2011
comment
Вы также можете посмотреть презентации Тони Морриса на Reader и Writer   -  person oxbow_lakes    schedule 31.12.2011
comment
@DanBurton Нет ничего плохого в свободном интерфейсе и статически типизированных языках. Это просто отстойный пример его реализации.   -  person Daniel C. Sobral    schedule 02.01.2012


Ответы (3)


Кажется, вы имеете дело с клиентским 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, то вы также сможете объединить два вычисления из двух разных запросов.

person huynhjl    schedule 30.12.2011
comment
Ух ты, огромное спасибо, huynhjl, за плохую формулировку моего вопроса и составление этого ответа. Это очень полезно - оно показывает, как определить свободный интерфейс, который может возвращать мой RestfulResponse тип или через map может применить дальнейшие вычисления и вернуть их. Могу ли я подтвердить, что исходный query() метод, который вы упомянули, является частью MyTargetApi и просто возвращает объект new Query()? Также было бы слишком много просить увидеть определение flatMap? Еще раз спасибо! - person Alex Dean; 31.12.2011
comment
@AlexDean, я добавил flatMap. Да, query() будет частью вашего исходного MyTargetApi и вернет новый Query объект. Пожалуйста, используйте мой ответ только для некоторых идей. Я также приглашаю вас посмотреть engineering.foursquare.com/2011/01/21/ и вообще любой ORM и интерфейс или оболочка nosql, написанные для Scala для большего вдохновения. - person huynhjl; 01.01.2012
comment
Большое спасибо, huynhjl. Я использовал исходный код Squeryl для вдохновения, но я обязательно проверю Rogue и некоторые другие инструменты ORM / NoSQL ... - person Alex Dean; 02.01.2012

Вы можете попробовать заставить 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 и наоборот. Об этом стоит подумать.

person Destin    schedule 30.12.2011
comment
И в каком смысле это тот функциональный стиль, о котором просил ОП? Вы выкидываете ошибки и выполняете ввод-вывод - person oxbow_lakes; 30.12.2011
comment
@oxbox_lakes Вы правы; это не функционально, но в большинстве практических программ приходится идти на компромиссы с точки зрения функционализма. Я думаю, что это лучшее решение для того, о чем он спрашивает. Лично я бы порекомендовал изменить API, но это не мой призыв. Если он хочет выдавать ошибки и выполнять ввод-вывод, кто я такой, чтобы говорить, что он не должен? - person Destin; 30.12.2011
comment
Но это был именно его вопрос: как сочетаются беглый и функциональный язык? На что вы никоим образом не ответили. - person oxbow_lakes; 30.12.2011
comment
Я думаю, что вы, возможно, зашли слишком далеко в какой-то (возможно) плохой выбор слов со стороны спрашивающего - а может быть, я. Исходя из моей интерпретации, он спрашивает, как он может вернуть кортеж и выполнять вызовы функций для этого кортежа, согласно этой цитате: я действительно не хочу жертвовать функциональной простотой возвращаемого значения кортежа, но я хотел бы расширить библиотеку с различными вызовами «беглых» методов. - person Destin; 30.12.2011
comment
Привет, Дестин! Большое спасибо за то, что нашли время собрать это воедино. Думаю, ваш ответ начался с моей конкретной болевой точки, тогда как oxbow_lakes начался с моей (очень плохой) формулировки. Итак, вы оба были правы :-) Я смутно имел в виду неявное преобразование RestfulResponse в более многофункциональный RestfulWrapper, поэтому было полезно увидеть, как это предлагается в вашем ответе. Я согласен, что выдача ошибок и выполнение операций ввода-вывода не особенно функционально, но тогда моя библиотека взаимодействует со сторонней веб-службой через HTTP, поэтому изначально она изначально неидемпотентна. - person Alex Dean; 31.12.2011

person    schedule
comment
Привет, oxbow_lakes! Большое спасибо за то, что нашли время составить этот ответ. Вы были правы - мой вопрос сформулирован очень неудачно, извиняюсь за неразбериху. Я согласен с тем, что функциональная и свободная части полностью совместимы - и спасибо за примеры, касающиеся построителей с сохранением состояния и без сохранения состояния. Что касается нетипизированных строк для get()ing ресурсов HTTP - я согласен, это была плохая идея, я собираюсь удалить эту возможность из API (или, по крайней мере, отметить ее unsafe, в стиле Haskell). - person Alex Dean; 31.12.2011