如何在 Scala 中将流畅的界面与函数式风格结合起来?

发布于 2024-12-23 09:48:14 字数 1205 浏览 2 评论 0原文

我一直在阅读JavaJavaScriptScala 我喜欢它的外观,但是一直在努力了解如何将其与 Scala 中更加基于类型/功能的方法相协调。

举一个非常具体的例子来说明我的意思:我编写了一个 API 客户端,可以像这样调用:

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

get() 的返回值是一个 Tuple3 类型称为 RestfulResponse,如我的 包object

// 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 添加更多“流畅”功能的能力结合起来吗?

I've been reading about the OO 'fluent interface' approach in Java, JavaScript and Scala and I like the look of it, but have been struggling to see how to reconcile it with a more type-based/functional approach in Scala.

To give a very specific example of what I mean: I've written an API client which can be invoked like this:

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

The return value from get() is a Tuple3 type called RestfulResponse, as defined in my package object:

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

This works fine - and I don't really want to sacrifice the functional simplicity of a tuple return value - but I would like to extend the library with various 'fluent' method calls, perhaps something like this:

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

How can I combine the functional simplicity of get() returning a typed tuple (or similar) with the ability to add more 'fluent' capabilities to my API?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

小帐篷 2024-12-30 09:48:14

看来您正在处理休息风格通信的客户端 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
}

然后最终获取您调用 的结果。所以在一天结束时你可以这样调用它

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

:应该是一个详细的请求,在出现问题时会出错,检索 id 22 的客户,保留正文并以 Option[Int] 形式获取其长度。

这个想法是,您可以使用 map 来定义对您尚未获得的结果的计算。如果我们向其中添加 flatMap,那么您还可以组合来自两个不同查询的两个计算。

It seems you are dealing with a client side API of a rest style communication. Your get method seems to be what triggers the actual request/response cycle. It looks like you'd have to deal with this:

  • properties of the transport (like credentials, debug level, error handling)
  • providing data for the input (your id and type of record (order or customer)
  • doing something with the results

I think for the properties of the transport, you can put some of it into the constructor of the MyTargetApi object, but you can also create a query object that will store those for a single query and can be set in a fluent way using a query() method:

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

This would return some stateful Query object that stores the value for log level, error handling. For providing the data for the input, you can also use the query object to set those values but instead of returning your response return a 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
}

Then to eventually get the results you call run. So at the end of the day you can call it like this:

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

Which should be a verbose request that will error out on issue, retrieve the customers with id 22, keep the body and get its length as an Option[Int].

The idea is that you can use map to define computations on a result you do not yet have. If we add flatMap to it, then you could also combine two computations from two different queries.

慕烟庭风 2024-12-30 09:48:14

老实说,我认为听起来您需要多摸索一下,因为该示例的功能不明显,也不是特别流畅。看来您可能将流畅性与非幂等混淆了,因为您的debugPrint方法可能正在执行I/O,而throwIfError是抛出异常。你是这个意思吗?

如果您指的是有状态构建器是否有效,答案是“不是最纯粹的意义上的”。但请注意,构建器不必是有状态的。

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

嘿,急:

api get orders

虽然,我认为这是一个坏主意 - 你不应该有一个单一的方法可以给你返回概念上完全不同类型的结果


总结:我个人认为没有不管出于什么原因,流畅性和函数式不能混合,因为函数式只是表明缺乏可变状态以及对幂等函数的强烈偏好来执行您的逻辑。

这是给您的一个:

args.map(_.toInt)

args map toInt

我认为第二个更流畅。如果你定义:

val toInt = (_ : String).toInt

那就是可能的;如果你定义了一个函数。我发现 Scala 中函数和流畅性融合得很好。

To be honest, I think it sounds like you need to feel your way around a little more because the example is not obviously functional, nor particularly fluent. It seems you might be mixing up fluency with not-idempotent in the sense that your debugPrint method is presumably performing I/O and the throwIfError is throwing exceptions. Is that what you mean?

If you are referring to whether a stateful builder is functional, the answer is "not in the purest sense". However, note that a builder does not have to be stateful.

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

Firstly; this can be created using named parameters:

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

Or, a stateless builder:

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

Hey presto:

scala> Person withName "Oxbow" andAge 36

As to your use of untyped strings to define the query you are making; this is poor form in a statically-typed language. What is more, there is no need:

sealed trait Query
case object orders extends Query

def get(query: Query): Result

Hey presto:

api get orders

Although, I think this is a bad idea - you shouldn't have a single method which can give you back notionally completely different types of results


To conclude: I personally think there is no reason whatsoever that fluency and functional cannot mix, since functional just indicates the lack of mutable state and the strong preference for idempotent functions to perform your logic in.

Here's one for you:

args.map(_.toInt)

args map toInt

I would argue that the second is more fluent. It's possible if you define:

val toInt = (_ : String).toInt

That is; if you define a function. I find functions and fluency mix very well in Scala.

久伴你 2024-12-30 09:48:14

您可以尝试让 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。好吧...实际上,如果您创建了从 RestfulResponse 到 ResponseWrapper 的隐式转换,反之亦然,您可能可以避免更改 API。这是值得考虑的事情。

You could try having get() return a wrapper object that might look something like this

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

}

Basically, this allows you to put your response into a contain that has all of these nice methods that you want, and, if you simply want to get the wrapped response, you can just call the wrapper's get() method.

Of course, the downside of this is that you will need to change your API a bit, if that's worrisome to you at all. Well... you could probably avoid needing to change your API, actually, if you, instead, created an implicit conversion from RestfulResponse to ResponseWrapper and vice versa. That's something worth considering.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文