Progtwigción funcional: cómo continuar el context para una cadena de reglas de validation

Tengo un set de funciones (reglas) para la validation que toman un context como parámetro y devuelven "Bien" o un "Error" con un post. Básicamente, estos podrían devolver un tipo Maybe (Haskell) / Optional (Java).

A continuación, me gustaría validar las properties de una Fruit (el context) y devolver un post de error si la validation falló, de lo contrario "está bien" / Nada.

Nota: Preferiría una solución que sea de estilo puramente funcional y sin estado / inmutable. Es un poco de un Kata, en realidad.

Para mis experimentos, utilicé Kotlin, pero el problema principal también se aplica a cualquier lenguaje que admita funciones de order superior (como Java y Haskell). Puede encontrar un enlace al código fuente completo aquí y el mismo al final.

Dada una class de fruta con color y peso, además de algunas reglas de ejemplo:

 data class Fruit(val color:String, val weight:Int) fun theFruitIsRed(fruit: Fruit) : Optional<String> = if (fruit.color == "networking") Optional.empty() else Optional.of("Fruit not networking") fun fruitNotTooHeavy(fruit: Fruit) : Optional<String> = if (fruit.weight < 500) Optional.empty() else Optional.of("Too heavy") 

Ahora me gustaría encadenar la evaluación de reglas usando una reference a la function respectiva, sin especificar el context como un argumento usando un FruitRuleProcessor . Cuando se procesa una regla falla, no debe evaluar ninguna de las otras reglas.

Por ejemplo:

 fun checkRules(fruit:Fruit) { var res = FruitRuleProcessor(fruit).check(::theFruitIsNotRed).check(::notAnApple).getResult() if (!res.isEmpty()) println(res.get()) } def main(args:Array<String) { // "Fruit not networking": The fruit has the wrong color and the weight check is thus skipped checkRules(Fruit("green","200")) // Prints "Fruit too heavy": Color is correct, checked weight (too heavy) checkRules(Fruit("networking","1000")) } 

No me importa dónde falló, solo sobre el resultado. Además, cuando una function devuelve un error, las otras no deben procesarse. Nuevamente, esto suena como una Mónada Optional .

Ahora el problema es que de alguna manera tengo que llevar el context de la fruit de la check a la llamada de check .

Una solución que probé es implementar una class Result que toma un context como valor y tiene dos subclasss RuleError(context:Fruit, message:String) y Okay(context) . La diferencia con Optional es que ahora puedo envolver el context de Fruit (piense T = Fruit )

 // T: Type of the context. I tried to generify this a bit. sealed class Result<T>(private val context:T) { fun isError () = this is RuleError fun isOkay() = this is Okay // bind infix fun check(f: (T) -> Result<T>) : Result<T> { return if (isError()) this else f(context) } class RuleError<T>(context: T, val message: String) : Result<T>(context) class Okay<T>(context: T) : Result<T>(context) } 

Creo que esto parece un monoide / Mónada, con return en el constructor levantando una Fruit en un Result y or siendo el bind . Aunque probé con Scala y Haskell, no tengo tanta experiencia en eso.

Ahora podemos cambiar las reglas a

 fun theFruitIsNotTooHeavy(fruit: Fruit) : Result<Fruit> = if (fruit.weight < 500) Result.Okay(fruit) else Result.RuleError(fruit, "Too heavy") fun theFruitIsRed(fruit: Fruit) : Result<Fruit> = if (fruit.color == "networking") Result.Okay(fruit) else Result.RuleError(fruit, "Fruit not networking") 

que permite encadenar comprobaciones como se pretendía:

 fun checkRules(fruit:Fruit) { val res = Result.Okay(fruit).check(::theFruitIsRed).check(::theFruitIsNotTooHeavy) if (res.isError()) println((res as Result.RuleError).message) } 

// Imprime: fruta no roja demasiado pesada

Sin embargo, esto tiene un inconveniente importante: el context de Fruit ahora se convierte en parte del resultado de la validation, aunque no es estrictamente necesario allí.

Entonces, para concluir: estoy buscando un path

  • para llevar el context de la fruit al invocar las funciones
  • para que pueda encadenar (básicamente: componer) múltiples verificaciones en una fila usando el mismo método
  • junto con los resultados de las funciones de reglas sin cambiar la interfaz de estos.
  • sin efectos secundarios

¿Qué patrones de functional programming podrían resolver este problema? ¿Son Mónadas porque mi instinto me dice eso?

Preferiría una solución que se puede hacer en Kotlin o Java 8 (para puntos de bonificación), pero las respuestas en otros idiomas (por ejemplo, Scala o Haskell) también podrían ser útiles. (Se trata del concepto, no del idioma :))

Puede encontrar el código fuente completo de esta pregunta en este violín .

Puede usar / crear un contenedor monoide de su tipo Optional / Maybe como First en Haskell que combina valores al devolver el primer valor que no sea Nada.

No conozco a Kotlin, pero en Haskell se vería así:

 import Data.Foldable (foldMap) import Data.Monoid (First(First, getFirst)) data Fruit = Fruit { color :: String, weight :: Int } theFruitIsRed :: Fruit -> Maybe String theFruitIsRed (Fruit "networking" _) = Nothing theFruitIsRed _ = Just "Fruit not networking" theFruitIsNotTooHeavy :: Fruit -> Maybe String theFruitIsNotTooHeavy (Fruit _ w) | w < 500 = Nothing | otherwise = Just "Too heavy" checkRules :: Fruit -> Maybe String checkRules = getFirst . foldMap (First .) [ theFruitIsRed , theFruitIsNotTooHeavy ] 

Ideone Demo

Tenga en count que estoy aprovechando la instancia de funciones de Monoid aquí:

 Monoid b => Monoid (a -> b) 

Como el tipo de object que se está validando no puede cambiar (ya que el object en sí no debería cambiar), no usaría una mónada (ni ningún tipo de functor). Tendría un tipo Validator a err = a -> [err] . Si un validador tiene éxito, muestra [] (sin error). Esto forma un monoide, donde mzero = const [] y mappend fgx = fx `mappend` gx . Haskell tiene esto incorporado como instance Monoid b => Monoid (a -> b)

EDIT : parece haber leído mal la pregunta. La respuesta de @ 4castle es casi exactamente esta, pero utiliza Maybe err lugar de [err] . Usa eso.

 // Scala, because I'm familiar with it, but it should translate to Kotlin case class Validator[-A, +Err](check: A => Seq[Err]) { def apply(a: A): Err = check(a) def |+|[AA >: A](that: Validator[A, Err]): Validator[AA, Err] = Validator { a => this(a) ++ that(a) } } object Validator { def success[A, E]: Validator[A, E] = Validator { _ => Seq() } } type FruitValidator = Validator[Fruit, String] val notTooHeavy: FruitValidator = Validator { fruit => if(fruit.weight < 500) Seq() else Seq("Too heavy") // Maybe make a helper method for this logic } val isRed: FruitValidator = Validator { fruit => if (fruit.color == "networking") Seq() else Seq("Not networking") } val compositeRule: FruitValidator = notTooHeavy |+| isRed 

Para usar, simplemente llame a un Validator como compositeRule(Fruit("green", 700)) , que devuelve 2 errores en este caso.

Para ver por qué la mónada del lector no es apropiada aquí, considere lo que sucede si

 type Validator = ReaderT Fruit (Either String) Fruit ruleA :: Validator ruleA = ReaderT $ \fruit -> if color fruit /= "networking" then Left "Not networking" else Right fruit ruleB :: Validator ruleB = ReaderT $ \fruit -> if weight fruit >= 500 then Left "Too heavy" else Right fruit ruleC = ruleA >> ruleB greenHeavy = Fruit "green" 700 

Tanto la ruleA como la ruleB fallan para greenHeavy , pero al ejecutar runReaderT ruleC greenHeavy solo produce el primer error. Esto no es deseable: es probable que desee tantos errores como sea posible revelado por ejecución.

Además, puedes "secuestrar" la validation:

 bogusRule :: ReaderT Fruit (Either String) Int bogusRule = return 42 ruleD = do ruleA ruleB bogusRule -- Validates just fine... then throws away the Fruit so you can't validate further. 

Para responder en general a la pregunta

Ahora el problema es que de alguna manera tengo que llevar el context de la fruta de la verificación a la llamada de verificación.

… expresado como …

Dada alguna mónada M , ¿cómo encadenar algunas acciones M mientras que (implícitamente) le doy el mismo object de "context" a cada una?

La respuesta de Haskell sería usar el transformador de mónada ReaderT . Toma cualquier mónada, como Maybe , y te da otra mónada que implícitamente pasa una "constante global" a cada acción.

Déjame reescribir tus fichas en Haskell:

 data Fruit = Fruit {colour::String, weight::Int} theFruitIsRed :: Fruit -> Either String () theFruitIsRed fruit | colour fruit == "networking" = Right () | otherwise = Left "Fruit not networking" fruitNotTooHeavy :: Fruit -> Either String () fruitNotTooHeavy fruit | weight fruit < 500 = Right () | otherwise = Left "Too heavy" 

Tenga en count que he usado Either String () lugar de Maybe String porque quiero que String sea ​​el "caso abortar", mientras que en la mónada Maybe podría ser el caso "continuar".

Ahora, en lugar de hacer

 checks :: Fruit -> Either String () checks fruit = do theFruitIsRed fruit fruitNotTooHeavy fruit 

puedo hacer

 checks = runReaderT $ do ReaderT theFruitIsRed ReaderT fruitNotTooHeavy 

Su class de Result parece ser esencialmente una instancia especial del transformador ReaderT . No estoy seguro si podría implementar lo mismo en Kotlin también.

Parece que estás buscando una mónada de error. Es como la mónada Maybe (aka Option ), pero el caso de error contiene un post.

En Haskell solo es el tipo de Either , con el primer argumento siendo el tipo del valor de error.

 type MyError a = Either String a 

Si revisa los datos. En cualquiera de los documentos, verá que Either e ya es una instancia de Monad, por lo que no necesita hacer nada más. Puedes escribir:

 notTooHeavy :: Fruit -> MyError () notTooHeavy fruit = when (weight fruit > 500) $ fail "Too heavy" 

Lo que hace la instancia de mónada es detener el cálculo en el primer fail , por lo que se obtiene, por ejemplo, Left "Too heavy" o Right () . Si quieres acumular errores, entonces tienes que hacer algo más complicado.

Otros carteles han sugerido que no necesita mónadas porque su código de ejemplo tiene todas las funciones return () . Si bien esto puede ser cierto para tus ejemplos, soy reacio a generalizar tan rápido. Además, dado que obtienes la instancia monádica automáticamente con Either tiene sentido simplemente usarla.

¿Son Mónadas porque mi instinto me dice eso?

Creo que Monad es un requisito demasiado fuerte en tu caso. Sus funciones de validation

diversión theFruitIsRed (fruta: fruta): Opcional <String>

no devuelva un valor utilizable cuando validen con éxito. Y una característica definitoria de Monad es poder decidir qué cálculos futuros ejecutar en function de un resultado anterior. "Si el primer validador tiene éxito volviendo a foo, valide este campo, si tiene éxito al regresar a la barra, valide este otro campo en su lugar".

No conozco Kotlin, pero creo que podrías tener una class Validator<T> . Básicamente, envolvería una única function de validation para un tipo T que devolvió un Optional<String> .

Luego, podría escribir un método que combinara dos validadores en un validador compuesto. La function interna del validador compuesto recibiría una T, ejecuta el primer validador, devuelve el error si falla, si no ejecuta el segundo validador. (Si sus validadores arrojaron algún resultado útil en la validation exitosa, como advertencias no fatales, deberá proporcionar una function adicional para combinar estos resultados).

La idea es que primero compones los validadores, y solo luego proporciones la T real para get el resultado final. Este enfoque de compilation previa a la ejecución es utilizado por Java's Comparator , por ejemplo.

Tenga en count que en esta solución, incluso si sus funciones arrojaban algún resultado en una validation exitosa, esos valores no se usarían para seleccionar qué validaciones hacer a continuación (aunque un error detendría la cadena). Puede combinar resultados usando una function pero eso es todo. Este estilo de progtwigción "más rígido" se denomina Applicative en Haskell. Todos los types compatibles con una interfaz Monad se pueden usar de forma Applicative , pero algunos types son compatibles con Applicative sin compatibilidad con Monad .


Otro aspecto interesante de los validadores es que son contravariantes en su tipo de input T Esto significa que puede "preaplicar" una function de A a B a un Validator<B> , lo que da como resultado un Validator<A> cuyo tipo fue "hacia atrás" en comparación con la dirección de la function. (La function de mapping de la class de Collectors de Java funciona de esta manera).

Y puede ir más allá en esta ruta al tener funciones que crean un validador para un compuesto de validadores para sus partes individuales. (Lo que en Haskell se llama Divisible ).

Hay un par de implementaciones de Haskell, así que intentemos resolverlo con Kotlin.

Primero, comenzamos con el object de datos:

 class Fruit(val color: String, val weight: Int) 

Y necesitamos un tipo que represente una fruta y si se produjo un error:

 sealed class Result<out E, out O> { data class Error<E>(val e: E) : Result<E, Nothing>() data class Ok<O>(val o: O): Result<Nothing, O>() } 

Ahora vamos a definir el tipo de una FruitRule:

 typealias FruitRule = (Fruit) -> String? 

FruitRule es una function que recibe una instancia de fruta y devuelve null si se pasa la regla o el post de error.

El problema que tenemos aquí es que FruitRule sí no es FruitRule . Entonces, necesitamos un Tipo que sea composable y ejecute una FruitRule en una Fruit

 typealias ComposableFruitRule = (Result<String, Fruit>) -> Result<String, Fruit> 

Primero, necesitamos una forma de crear una ComposableFruitRule partir de una FruitRule

 fun createComposableRule(f: FruitRule): ComposableFruitRule { return { result: Result<String, Fruit> -> if(result is Result.Ok<Fruit>) { val temporaryResult = f(result.o) if(temporaryResult is String) Result.Error(temporaryResult) else //We know that the rule passed, //so we can return Result.Ok<Fruit> we received back result } else { result } } } 

createComposableFruitRule devuelve una lambda que primero verifica si el resultado proporcionado es Result.Ok . En caso afirmativo, ejecuta el FruitRule proporcionado en la Fruit dada y devuelve Result.Error si el post de error no es nulo.

Ahora hagamos ComposableFruitRule composable:

 infix fun ComposableFruitRule.composeRules(f: FruitRule): ComposableFruitRule { return { result: Result<String, Fruit> -> val temporaryResult = this(result) if(temporaryResult is Result.Ok<Fruit>) { createComposableRule(f)(temporaryResult) } else { temporaryResult } } } 

Esta function de infijo compone una ComposableFruitRule junto con una FruitRule , lo que significa que primero se invoca la FruitRule interna. Si no hay ningún error, se FruitRule como parámetro.

Así que ahora podemos componer FruitRules juntos y luego solo proporcionar una Fruit y verificar las reglas.

 fun colorIsRed(fruit: Fruit): String? { return if(fruit.color == "networking") null else "Color is not networking" } fun notTooHeavy(fruit: Fruit): String? { return if(fruit.weight < 500) null else "Fruit too heavy" } fun main(args: Array<String>) { val ruleChecker = createComposableRule(::colorIsRed) composeRules ::notTooHeavy //We can compose as many rules as we want //eg ruleChecker composeRules ::fruitTooOld composeRules ::fruitNotTooLight val fruit1 = Fruit("blue", 300) val result1 = ruleChecker(Result.Ok(fruit1)) println(result1) val fruit2 = Fruit("networking", 700) val result2 = ruleChecker(Result.Ok(fruit2)) println(result2) val fruit3 = Fruit("networking", 350) val result3 = ruleChecker(Result.Ok(fruit3)) println(result3) } 

La salida de ese main es:

 Error(e=Color is not networking) Error(e=Fruit too heavy) Ok(o=Fruit@65b54208) 
  • Companion se beneficia de la posibilidad de implementar interfaces
  • La creación de Gradle falló con Kotlin, Scala y Java
  • Biblioteca Headless de una fuente para JVM y JavaScript
  • Forma equivalente de Scala de Rango a class personalizada
  • ¿Cómo funcionan las funciones de extensión de Kotlin?
  • Tipo de function con receptor en Scala
  • Kotlin VS Scala: Implementar methods con parameters constructor primarios
  • Hay alguna biblioteca para trabajar con mónadas en kotlin?
  • Kotlin zipToda alternativa
  • Convierta la function de Scala a la function de Kotlin
  • @uncheckedVariance en Kotlin?