2011-12-30 11 views
6

He estado leyendo acerca del enfoque OO de 'interfaz fluida' en Java, JavaScript y Scala y me gusta su aspecto, pero he estado luchando para ver cómo conciliarlo con un enfoque más basado en tipo/funcional en Scala .¿Cómo puedo combinar interfaces fluidas con un estilo funcional en Scala?

Para dar un ejemplo muy concreto de lo que quiero decir: He escrito un cliente de API que puede ser invocada como esto:

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

El valor de retorno de get() es un tipo llamado Tuple3RestfulResponse, como se define en mi package object:

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

Esto funciona bien - y yo realmente no quieren sacrificar la simplicidad funcional de un valor de retorno tupla - pero me gustaría extender la biblioteca con varios f' uentes' llamadas a métodos, tal vez algo como esto:

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

¿Cómo puedo combinar la simplicidad funcional de get() devolver una tupla escrita (o similar) con la posibilidad de añadir más capacidades fluidas '' a mi API?

+0

Este enfoque de "interfaz fluida" ... no se ve muy amigable con los lenguajes estáticos. Aunque estoy seguro de que con algún código de ajuste y herencia probablemente puedas hacerlo con Scala, simplemente no será tan seguro como tener 'getOrders' y' getCustomers' por separado, en lugar de 'get (" orders ")' y 'get (" clientes ")' utilizando el mismo método 'get'. –

+0

Gracias Dan, pero no me preocuparía demasiado acerca de la sintaxis 'get (" slug ", id)' - de esto no se trata realmente mi pregunta. En cualquier caso, hay otro modo más seguro en la biblioteca que se parece a 'MyTargetApi.orders.get (id)' –

+0

Personalmente, creo que debería ofrecer un ejemplo más representativo de algún código fluido y exactamente qué bit cree que no es funcional. Por el momento, solo surge de su pregunta que realmente no sabe lo que significa –

Respuesta

7

Parece que está tratando con una API del lado del cliente de una comunicación de estilo de descanso. Su método get parece ser lo que desencadena el ciclo de solicitud/respuesta real. Parece que lo tienes que lidiar con esto:

  • propiedades del transporte (como credenciales, el nivel de depuración, tratamiento de errores)
  • que proporcionan datos para la entrada (su identificación y tipo de registro (orden o cliente)
  • hacer algo con los resultados

Creo que para las propiedades del transporte, se puede poner un poco de ella en el constructor de la MyTargetApi obj etc., pero también se puede crear un objeto consulta que almacenará los de una sola consulta y se puede fijar en una fluidez manera usando un método query():

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

Esto volvería alguna Query objeto con estado que almacena el valor para el nivel de registro, manejo de errores.Para proporcionar los datos para la entrada, también puede utilizar el objeto de consulta para establecer esos valores, pero en lugar de volver a su respuesta devolver un 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 
} 

luego a que con el tiempo los resultados que llaman run. Así que al final del día se le puede llamar así:

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

que debe ser una petición detallado que el error fuera en cuestión, recuperar los clientes con id 22, mantener el cuerpo y obtener su longitud como una Option[Int].

La idea es que puede usar map para definir cálculos sobre un resultado que aún no tiene. Si le agregamos flatMap, entonces también puede combinar dos cálculos de dos consultas diferentes.

+0

Guau, gracias enormemente ** huynhjl ** por leer el pobre fraseo de mi pregunta y armar esta respuesta. Es muy útil: muestra cómo definir una interfaz fluida que puede devolver mi tipo 'RestfulResponse' o mediante' map' puede aplicar un cálculo posterior y devolverlo. ¿Puedo confirmar que el método 'query()' original que menciona es parte de 'MyTargetApi' y simplemente devuelve un' nuevo objeto Query() '? ¿Sería demasiado pedir ver la definición 'flatMap'? ¡Gracias de nuevo! –

+1

@AlexDean, agregué 'flatMap'. Sí 'query()' sería parte de su 'MyTargetApi' original y devolvería un nuevo objeto' Query'. Por favor, use mi respuesta solo para algunas ideas. También los invito a consultar http://engineering.foursquare.com/2011/01/21/rogue-a-type-safe-scala-dsl-for-querying-mongodb/ y, en general, cualquier interfaz ORM y nosql o envoltorio escrito para Scala para obtener más inspiración. – huynhjl

+0

Muchas gracias ** huynhjl **. He estado usando [fuente de Squeryl] (https://github.com/max-l/Squeryl/tree/master/src/main/scala/org/squeryl) en busca de inspiración, pero definitivamente voy a echar un vistazo a Rogue y algunas de las otras herramientas ORMs/NoSQL también ... –

3

Para ser honesto, creo que es necesario que te sientas a gusto un poco más porque el ejemplo no es obviamente funcional, ni particularmente fluido. Parece que podría estar mezclando fluidez con no idempotente en el sentido de que su método debugPrint presumiblemente está realizando E/S y el throwIfError está lanzando excepciones. ¿Es eso lo que quieres decir?

Si se refiere a si un stateful builder es funcional, la respuesta es "no en el sentido más puro". Sin embargo, tenga en cuenta que un constructor no tiene que ser con estado.

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

Primero; esto puede ser creado usando parámetros con nombre:

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

O, un constructor sin estado:

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

¡listo:

scala> Person withName "Oxbow" andAge 36 

En cuanto a su uso de cadenas sin tipo para definir la consulta que están haciendo; esta es una forma pobre en un lenguaje de tipo estático.Lo que es más, no hay necesidad:

sealed trait Query 
case object orders extends Query 

def get(query: Query): Result 

¡listo:

api get orders 

Aunque, creo que esto es una mala idea - que no debería tener un único método que puede darle vuelta teóricamente completamente diferentes tipos de resultados


en conclusión: yo personalmente creo que no hay razón alguna de que la fluidez y funcional no se pueden mezclar, ya que simplemente funcional indica la falta de un estado mutable nd la fuerte preferencia por las funciones idempotente para llevar a cabo su lógica en

Aquí está uno para usted:.

args.map(_.toInt) 

args map toInt 

Yo diría que la segunda es más fluida. Es posible si define:

val toInt = (_ : String).toInt 

Eso es; si defines una función Las funciones y la fluidez se mezclan muy bien en Scala.

+0

Hola ** oxbow_lakes **: muchas muchas gracias por tomarte el tiempo para armar esta respuesta. Tenías razón, el fraseo de mi pregunta era muy pobre, disculpas por toda la confusión. Estoy de acuerdo en que los recursos funcionales y fluidos son completamente compatibles, y gracias por los ejemplos sobre constructores con estado y apátridas. En las cadenas no tipeadas para 'get()' ing recursos HTTP - estoy de acuerdo, esa fue una mala idea, voy a eliminar esa capacidad de la API (o al menos marcarlo como 'inseguro', estilo Haskell). –

0

Usted podría intentar tener get() devuelve un objeto de contenedor que podría ser algo como esto

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 

} 

Básicamente, esto le permite poner su respuesta en un contener de que tiene todos estos métodos agradables que desee y, si simplemente desea obtener la respuesta envuelta, puede simplemente llamar al método get() del contenedor.

Por supuesto, la desventaja de esto es que tendrá que cambiar su API un poco, si eso le preocupa en absoluto. Bueno ... probablemente podría evitar tener que cambiar su API, en realidad, si creó una conversión implícita de RestfulResponse a ResponseWrapper y viceversa. Eso es algo que vale la pena considerar.

+0

¿Y de qué manera es este el "estilo funcional" que el OP estaba pidiendo? Estás lanzando errores y realizando E/S –

+1

@oxbox_lakes Estás en lo correcto; no es funcional, pero la programación más práctica tiene que hacer concesiones en lo que respecta al funcionalismo. Creo que esta es la mejor solución para hacer lo que pregunta. Personalmente, recomendaría cambiar la API, pero esa no es mi decisión. Si quiere lanzar errores y realizar E/S, ¿quién soy yo para decir que no debería? – Destin

+0

Pero fue específicamente su pregunta: "¿cómo funciona la mezcla fluida y funcional?". Que de ninguna manera has respondido. –

Cuestiones relacionadas