函数式编程:如何继承一系列验证规则的上下文
我有一套用于验证的函数(规则),以上下文为参数,并返回“Okay”或“Error”。 基本上这些可能会返回一个Maybe
(Haskell)/ Optional
(Java)类型。
在下面,我想验证一个Fruit
(上下文)的属性,并返回一个错误消息,如果验证失败,否则“好”/无。
注意:我更喜欢纯粹的功能风格和无状态/不可变的解决方案。 实际上这是一个卡塔。
对于我的实验,我使用Kotlin,但核心问题也适用于支持更高阶函数(如Java和Haskell)的任何语言。 你可以在这里找到完整源代码的链接,在最底部也是一样。
给定一个水果类的颜色和重量,再加上一些示例规则:
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")
现在我想使用对各个函数的引用来链接规则评估,而不使用FruitRuleProcessor
将上下文指定为参数。 处理规则失败时,不应该评估任何其他规则。
例如:
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")) }
我不在乎它失败的地方,只关于结果。 另外,当一个函数返回一个错误,其他的不应该被处理。 再次,这听起来像一个Optional
Monad。
现在的问题是,我不得不把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) }
我认为这看起来像一个monoid / Monad,并且在构造函数中return
一个将Fruit
放入Result
or
成为bind
。 虽然我尝试了一些斯卡拉和哈斯克尔,但我并不那么有经验。
现在我们可以改变规则
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
上下文 - 这样我就可以使用相同的方法连锁(基本上是:编写)连续的多个检查
- 以及规则函数的结果, 而不改变这些的接口 。
- 没有副作用
什么功能编程模式可以解决这个问题? 我的直觉是否会试图告诉我这是否是Monad?
我更喜欢可以在Kotlin或Java 8中完成的解决方案(对于奖励积分),但其他语言(例如Scala或Haskell)的答案也可能会有所帮助。 (这是关于概念,而不是语言:))
你可以在这个小提琴中找到这个问题的完整源代码。
你可以使用/创建一个Optional
/ Maybe
类型的monoid包装器,例如Haskell中的First
,它通过返回第一个非Nothing值来组合值。
我不认识Kotlin,但是在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 ]
Ideone演示
请注意,我在这里利用了Monoid
实例的功能:
Monoid b => Monoid (a -> b)
由于被验证的对象的类型不能改变(因为对象本身不应该改变),所以我不会使用monad(或任何类型的函子)。 我会有一个类型Validator a err = a -> [err]
。 如果验证成功,则输出[]
(无错误)。 这形成一个monoid,其中mzero = const []
和mappend fgx = fx `mappend` gx
。 哈斯克尔有内置的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
要使用,只需调用一个像compositeRule(Fruit("green", 700))
的Validator
,在这种情况下返回2个错误。
要明白为什么读者monad在这里不合适,请考虑如果发生什么情况
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.
一般回答这个问题
现在的问题是,我不得不把水果上下文从检查到检查呼叫。
…表达为…
给定一些monad
M
,我怎样连锁一些M
行为,同时(隐式地)给每个相同的“context”对象?
Haskell的答案是使用ReaderT
monad变换器 。 它需要任何monad,比如Maybe
,并且给你另一个monad,它隐式地将“全局常量”传递给每个动作。
让我在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
monad中,它将成为“继续”的情况。
现在,而不是做
checks :: Fruit -> Either String () checks fruit = do theFruitIsRed fruit fruitNotTooHeavy fruit
我可以
checks = runReaderT $ do ReaderT theFruitIsRed ReaderT fruitNotTooHeavy
您的Result
类似乎本质上是一个ReaderT
转换器的特殊实例。 不知道你是否可以在Kotlin中实现确切的东西。
这听起来像你正在寻找一个错误monad。 它就像Maybe
(aka Option
)monad,但是错误的情况下会传递一个消息。
在Haskell中它只是Either
类型,第一个参数是错误值的类型。
type MyError a = Either String a
如果你检查Data.Either文档,你会看到Either e
是Either e
已经是Monad的一个实例,所以你不需要做任何事情。 你可以写:
notTooHeavy :: Fruit -> MyError () notTooHeavy fruit = when (weight fruit > 500) $ fail "Too heavy"
monad实例所做的是在第一次fail
停止计算,所以你得到例如Left "Too heavy"
或Right ()
。 如果你想积累错误,那么你必须做更复杂的事情。
其他海报建议你不需要单子,因为你的示例代码具有返回()
所有函数。 虽然这可能是你的例子,但我不愿意这样做。 另外,由于您自动获取monadic实例,所以只需使用它即可。
我的直觉是否会试图告诉我这是否是Monad?
我认为Monad
在你的情况下是太强大了。 您的验证功能
fun theFruitIsRed(fruit:Fruit):可选<String>
验证成功时不返回可用的值。 而Monad
一个决定性特征就是能够根据以前的结果决定执行未来的计算。 “如果第一个验证器成功返回foo,验证该字段,如果成功返回bar,则改为验证这个其他字段”。
我不了解Kotlin,但我想你可以有一个Validator<T>
类。 它基本上会为返回一个Optional<String>
的类型T
包装一个验证函数。
然后你可以编写一个方法将两个验证器组合成一个复合验证器。 复合验证器的内部函数会收到一个T,运行第一个验证器,如果失败则返回错误,如果没有运行第二个验证器。 (如果你的验证器在成功验证时返回了一些有用的结果,比如说非致命的警告,你需要提供一个额外的函数来结合这些结果。)
这个想法,你将首先组成验证,然后才提供实际的T来获得最终结果。 例如,Java的比较器使用这种组合运行之前的方法。
注意,在这个解决方案中,即使你的函数在成功验证的时候返回了一些结果,这些值也不会被用来选择下一步做什么验证 (尽管错误会阻止链)。 你可以结合使用功能的结果,但就是这样。 这种“更严格”的编程风格在Haskell中被称为Applicative
。 支持Monad
接口的所有类型都可以以Applicative
方式使用,但某些类型支持Applicative
而不支持Monad
。
验证器的另一个有趣的方面是它们在输入类型T
上是相反的。 这意味着您可以将“从A
到B
”的函数“预先应用”到Validator<B>
,从而生成一个Validator<A>
其类型与函数的方向相比向后“倒退”。 (Java的Collectors
类的mapping
功能就是这样工作的。)
而且,您可以通过具有为各个部分的验证程序构建验证程序的函数来进一步实现此路线。 (Haskell中的什么被称为Divisible
)
有几个Haskell的实现,所以让我们试着用Kotlin来解决它。
首先,我们从数据对象开始:
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,并在Fruit
上运行一个FruitRule
typealias ComposableFruitRule = (Result<String, Fruit>) -> Result<String, Fruit>
首先,我们需要一种从FruitRule
创建一个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
返回一个lambda,首先检查提供的Result是否为Result.Ok
。 如果是,则在给定Fruit
上运行提供的FruitRule
,如果错误消息不为空,则返回Result.Error
。
现在,让我们的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 } } }
这个中缀函数和一个FruitRule
一起组成一个ComposableFruitRule
,这意味着首先调用内部的FruitRule
。 如果没有错误,则调用作为参数提供的FruitRule
。
所以现在我们可以把FruitRules
组合在一起,然后提供一个Fruit
并检查规则。
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 //eg 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) }
该main
的输出是:
Error(e=Color is not red) Error(e=Fruit too heavy) Ok(o=Fruit@65b54208)