How can I combine free interfaces with functional style in Scala?

I read about the "free interface" of OO in Java , JavaScript, and Scala , and I like its appearance, but he struggled to figure out how to put up with a more type-based / functional approach in Scala.

To give a very concrete example of what I mean: I wrote an API client that can be called as follows:

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

The return value from the get() - this type Tuple3 , called RestfulResponse , as defined in the facility:

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

This works great, and I really don't want to sacrifice the functional simplicity of the return value of the tuple, but I would like to expand the library with various β€œquick” 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 β€œfree” features to my API?

+6
source share
3 answers

It seems you are dealing with a holiday-style client-side API. It seems your get method starts the actual request / response cycle. It looks like you have to deal with this:

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

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

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

This will return some stateful Query object that will save the value for the log level, error handling. To provide input data, you can also use a query object to set these values, but instead of returning a response, reply 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 end up with 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() 

What should be a detailed request that will fail, get clients with identifier 22, save the body and get its length as Option[Int] .

The idea is that you can use map to determine the calculations for the result that you have not yet received. If we add to it flatMap , you can also combine the two calculations of two different queries.

+7
source

Honestly, I think it sounds like you need to think a little, because the example is clearly not functional and not particularly fluent. It seems you can mix fluency with non-idempotency in the sense that your debugPrint method seems to do I / O and throwIfError throws exceptions. Is that what you mean?

If you mean whether the constructor works with state, the answer is "not in the pure sense." However, note that the builder does not have to be workable.

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

At first; this can be created using named parameters:

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

Or, stateless creator:

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

Hi Preko:

 scala> Person withName "Oxbow" andAge 36 

Regarding the use of untyped strings to define the query you are making; it is a bad form in a statically typed language. Moreover, there is no need:

 sealed trait Query case object orders extends Query def get(query: Query): Result 

Hi Preko:

 api get orders 

Although, I think this is a bad idea - you should not have a single method that can give you completely different types of results


In conclusion: I personally think that there is no reason that fluency and functionality cannot be mixed, because the functional simply indicates the absence of a volatile state and a strong preference for idempotent functions to fulfill your logic.

Here is one for you:

 args.map(_.toInt) args map toInt 

I would say that the second one is more fluent. This is possible if you define:

 val toInt = (_ : String).toInt 

I.e; if you define a function. I believe that function and fluency work well in Scala.

+3
source

You can try to get () the return of 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 answer in a file containing all these good methods, and if you just want a wrapped answer, you can just call the wrapper get () method.

Of course, the disadvantage of this is that you will need to change your API a bit, if that bothers you at all. Well ... you probably won't want to change your API, in fact, if instead you created an implicit conversion from RestfulResponse to ResponseWrapper and vice versa. This is something worth noting.

0
source

Source: https://habr.com/ru/post/904758/


All Articles