Functional programming: how to transfer context for a chain of validation rules

I have a set of functions (rules) for checking, which take the context as a parameter and either return "Okay" or "Error" with the message. Basically they can return type Maybe(Haskell) / Optional(Java).

In the following case, I would like to check the properties Fruit(context) and return an error message if the check fails, otherwise "Okay" / Nothing.

Note: I would prefer a solution that is purely functional style and stagnant / immutable. Actually this is a little Kata.

In my experiments I used Kotlin, but the main problem also applies to any language that supports higher order functions (such as Java and Haskell). You can find the link to the full source code here and the same thing at the very bottom.

Given the Fruit class with color and weight, as well as some sample rules:

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

fun theFruitIsRed(fruit: Fruit) : Optional<String> =
        if (fruit.color == "red") Optional.empty() else Optional.of("Fruit not red")
fun fruitNotTooHeavy(fruit: Fruit) : Optional<String> =
            if (fruit.weight < 500) Optional.empty() else Optional.of("Too heavy")

Now I would like to bind the rule score using the link to the corresponding function, without specifying the context as an argument with FruitRuleProcessor. When rule processing fails, it should not evaluate any other rules.

Example:

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 red": 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("red","1000")) 
 }

I don’t care where it failed, only about the result. Also, when a function returns an error, others should not be processed. Again, this is pretty much like OptionalMonad.

, - Fruit check check.

- Result, RuleError(context:Fruit, message:String) Okay(context). Optional , Fruit ( 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)
}

, /, return , a Fruit Result or, bind. Scala Haskell, , .

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 == "red") Result.Okay(fruit) else Result.RuleError(fruit, "Fruit not red")

, :

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

//:

: Fruit , .

, :

  • Fruit
  • ( : )
  • , .

? , ?

, Kotlin Java 8 ( ), (, Scala Haskell) . ( , :))

this fiddle.

+4
6

/ Optional/Maybe, First Haskell, , , .

, Haskell :

import Data.Foldable (foldMap)
import Data.Monoid (First(First, getFirst))

data Fruit = Fruit { color :: String, weight :: Int }

theFruitIsRed :: Fruit -> Maybe String
theFruitIsRed (Fruit "red" _) = Nothing
theFruitIsRed _               = Just "Fruit not red"

theFruitIsNotTooHeavy :: Fruit -> Maybe String
theFruitIsNotTooHeavy (Fruit _ w)
    | w < 500   = Nothing
    | otherwise = Just "Too heavy"

checkRules :: Fruit -> Maybe String
checkRules = getFirst . foldMap (First .)
    [ theFruitIsRed
    , theFruitIsNotTooHeavy
    ]

, Monoid:

Monoid b => Monoid (a -> b)
+3

( ), ( - ). Validator a err = a -> [err]. , [] ( ). , mzero = const [] mappend f g x = f x `mappend` g x. Haskell instance Monoid b => Monoid (a -> b)

: , , . @4castle , Maybe err [err]. .

// 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 == "red") Seq() else Seq("Not red")
}

val compositeRule: FruitValidator = notTooHeavy |+| isRed

, Validator compositeRule(Fruit("green", 700)), 2 .

, - , , ,

type Validator = ReaderT Fruit (Either String) Fruit
ruleA :: Validator
ruleA = ReaderT $ \fruit ->
  if color fruit /= "red" then Left "Not red"
  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

ruleA ruleB greenHeavy, runReaderT ruleC greenHeavy . : , , , , .

, "" :

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.
+2

, ?

, Monad .

fun theFruitIsRed (: ): <String>

. Monad , . " foo, , , ".

, , Validator<T>. T, Optional<String>.

, . T, , , , . ( , , , .)

, T, . , Java Comparator, .

, , , , ( ). , , . " " Applicative Haskell. , Monad, Applicative, Applicative Monad.


, T. , " " A B Validator<B>, Validator<A>, "" . ( mapping Java Collectors.)

, , . ( Haskell Divisible.)

+1

, - , .

... ...

M, M, () "" ?

Haskell ReaderT monad transformer. , ​​ Maybe, , " " .

Haskell:

data Fruit = Fruit {colour::String, weight::Int}

theFruitIsRed :: Fruit -> Either String ()
theFruitIsRed fruit
  | colour fruit == "red"  = Right ()
  | otherwise              = Left "Fruit not red"

fruitNotTooHeavy :: Fruit -> Either String ()
fruitNotTooHeavy fruit
  | weight fruit < 500     = Right ()
  | otherwise              = Left "Too heavy"

, Either String () Maybe String, , String " ", Maybe "" .

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

checks = runReaderT $ do
    ReaderT theFruitIsRed
    ReaderT fruitNotTooHeavy

Result ReaderT. , .

+1

, monad. Maybe (aka Option), .

Haskell Either, .

type MyError a = Either String a

Data.E > , , Either e Monad, - . :

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

monad, fail, , , Left "Too heavy" Right (). , - .

, , , (). , . , Either, .

+1

Haskell, .

Data-Object:

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

, , :

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>()
}

FruitRule:

typealias FruitRule = (Fruit) -> String?

FruitRule - , Fruit-Instance null, .

, , , FruitRule . Type, FruitRule Fruit

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

-, ComposableFruitRule 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 , , Result.Ok. , FruitRule Fruit Result.Error, null.

ComposableFruitRule :

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
        }
    }
}

This infix function makes ComposableFruitRuleup with FruitRule, which means that the inner one is called first FruitRule. If there is no error, it is called FruitRuleprovided as a parameter.

So now we can put FruitRulestogether, and then just provide Fruitand verify the rules.

fun colorIsRed(fruit: Fruit): String? {
    return if(fruit.color == "red")
        null
    else
        "Color is not red"
}

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
    //e.g. ruleChecker composeRules ::fruitTooOld composeRules ::fruitNotTooLight

    val fruit1 = Fruit("blue", 300)
    val result1 = ruleChecker(Result.Ok(fruit1))
    println(result1)

    val fruit2 = Fruit("red", 700)
    val result2 = ruleChecker(Result.Ok(fruit2))
    println(result2)

    val fruit3 = Fruit("red", 350)
    val result3 = ruleChecker(Result.Ok(fruit3))
    println(result3)
}

The result of this main:

Error(e=Color is not red)
Error(e=Fruit too heavy)
Ok(o=Fruit@65b54208)
+1
source

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