gpt4 book ai didi

validation - 是否有必要以递归方式编写 Haskell 泛型?

转载 作者:行者123 更新时间:2023-12-05 00:15:05 25 4
gpt4 key购买 nike

Haskell 泛型的大多数示例都围绕 :+: 递归地进行少量计算。和 :*:类型/构造函数。我似乎正在解决一个可能无法解决的问题。

我正在尝试编写一个通用验证函数,该函数采用具有相同形状的任意两条记录,并根据 recordB 中定义的验证函数验证 recordA 中的每个字段,以返回 的错误记录。同 形状或记录A本身。

例子:

-- Some type synonyms for better readability
type Name = Text
type Age = Int
type Email = Text
type GeneralError = Text
type FieldError = Text

-- a polymorphic record to help preserve the shape of various records
data User n a e = User {name :: n, age :: a, email :: e}

-- the incoming value which has been parsed into the correct type
-- but still needs various values to be validated, eg length, format, etc
type UserInput = User Name Age Email

-- specifies the exact errors for each field
type UserError = User [FieldError] [FieldError] [FieldError]

-- specifies how to validate each field. the validator is being passed
-- the complete record along with the specific field to allow
-- validations that depends on the value of another field
type UserValidator = User
(UserInput -> Name -> Either ([GeneralError], [FieldError]) Name)
(UserInput -> Age -> Either ([GeneralError], [FieldError]) Age)
(UserInput -> Email -> Either ([GeneralError], [FieldError]) Email)

let (validationResult :: Either ([GeneralError], UserError) UserInput)
= genericValidation (i :: UserInput) (v :: UserValidator)

现在,为什么要在 :*: 周围递归地执行此操作可能行不通的是,需要查看每个验证函数的结果,然后决定返回值是否应该是 Left ([GeneralError], UserError)Right UserInput .我们无法评估为 Left失败的第一个验证函数上的值。

有什么办法可以写这个 genericValidation函数使用 Haskell 泛型?

最佳答案

Now, the reason why doing this recursively around :*: might not work is, that one needs to look at the result of every validation function and then decide if the return value should be a Left ([GeneralError], UserError) or a Right UserInput. We cannot evaluate to a Left value on the first validation function that fails.



标准 Applicative Either 的行为不是这种类型的唯一合理行为!正如您所说,例如,当您验证表单时,您希望返回发生的所有错误的集合,而不仅仅是第一个错误。所以这里的类型与 Either 结构相同但有一个不同的 Applicative实例。
newtype Validation e a = Validation (Either e a) deriving Functor

instance Semigroup e => Applicative (Validation e) where
pure = Validation . pure
Validation (Right f) <*> Validation (Right x) = Validation (Right $ f x)
Validation (Left e1) <*> Validation (Left e2) = Validation (Left $ e1 <> e2)
Validation (Left e) <*> _ = Validation (Left e)
_ <*> Validation (Left e) = Validation (Left e)

当两个计算都失败时,组合计算也失败,返回使用它们的 Semigroup 组合的两个错误。实例 - 两个错误,对于两者的一些合适的概念。如果两个计算都成功,或者只有一个失败,那么 Validation表现得像 Either .所以它有点像 Either 的科学怪人混搭和 Writer应用程序。

此实例确实满足 Applicative法律,但我会把证据留给你。哦,还有 Validation不能成为合法的 Monad .

请原谅我冒昧地重新排列您的类型。我正在使用一个常见的技巧来重用各种不同类型的记录结构:通过类型构造函数参数化记录。您可以通过将模板应用于 Identity 来恢复原始记录。仿函数。
data UserTemplate f = UserTemplate {
name :: f Name,
age :: f Age,
email :: f Email
}
type User = UserTemplate Identity

一个有用的新类型: Validator是一个接受 a 的函数并返回 a或错误的幺半群总结。
newtype Validator e a = Validator { runValidator :: a -> Validation e a }

一个有用的类: HTraversable就像 Traversable但是对于从类型构造函数类别到 Hask 的仿函数。 (在 a previous question of mine 中了解更多信息。)
class HFunctor t where
hmap :: (forall x. f x -> g x) -> t f -> t g
class HFunctor t => HTraversable t where
htraverse :: Applicative a => (forall x. f x -> Compose a g x) -> t f -> a (t g)
htraverse f = hsequence . hmap f
hsequence :: Applicative a => t (Compose a g) -> a (t g)
hsequence = htraverse id

为什么是 HTraversable相关的? Traversable Classic™ 允许您进行排序 Applicative效果如 Validation在同类容器(如列表)上。但是记录更像是一个异构容器:记录“包含”一堆字段,但每个字段都有自己的类型。 HTraversable正是您需要对 Applicative 进行排序时使用的类对多态容器的操作。

另一个有用的类概括 zipWith到这些异构容器。
class HZip t where
hzip :: (forall x. f x -> g x -> h x) -> t f -> t g -> t h

UserTemplate 的方式构建的记录是可遍历和可压缩的。 (事实上​​,它们通常是 HRepresentable——一个类似的高阶概念 Representable ——这是一个非常有用的属性,尽管我不会在这里详述。)
instance HFunctor UserTemplate where
hmap f (UserTemplate n a e) = UserTemplate (f n) (f a) (f e)

instance HTraversable UserTemplate where
htraverse f (UserTemplate n a e) = UserTemplate <$>
getCompose (f n) <*>
getCompose (f a) <*>
getCompose (f e)

instance HZip UserTemplate where
hzip f (UserTemplate n1 a1 e1) (UserTemplate n2 a2 e2) = UserTemplate (f n1 n2) (f a1 a2) (f e1 e2)

希望应该很容易看到 Generic或模板 Haskell 实现 HTraversableHZip对于适合此模式的任意记录。

所以,计划是:写 Validator s 代表每个字段,然后 hzip这些 Validator s 沿着要验证的对象。那么你可以 htraverse结果得到一个 Validation包含经过验证的对象。根据您的问题,此模式适用于逐字段验证。如果您需要查看多个字段来验证您的记录,则不能使用 hzip (但当然你也不能使用 Generic )。
type Validatable t = (HZip t, HTraversable t)
validate :: (Semigroup e, Validatable t) => t (Validator e) -> Validator e (t Identity)
validate t = Validator $ htraverse (Compose . fmap Identity) . hzip val t
where val v = runValidator v . runIdentity

类型的特定验证器,例如 User基本上涉及选择幺半群错误并返回验证函数的记录。我在这里定义了一个 MonoidUserError提升幺半群 e逐点遍历记录的每个字段。
type UserError e = UserTemplate (Const e)

instance Semigroup e => Semigroup (UserError e) where
x <> y = hzip (<>) x y

现在您可以定义验证器函数的记录。
type UserValidator = Validator ([GeneralError], UserError [FieldError])

validateEmail :: UserInput -> UserValidator Email
validateEmail i = Validator v
where v e
| '@' `elem` toString e = pure e
| otherwise = Validation $ Left ([], UserTemplate [] [] [FieldError "missing @"])

validateName :: UserInput -> UserValidator Name
validateName = ...
validateAge :: UserInput -> UserValidator Age
validateAge = ...

userValidator :: UserInput -> UserValidator User
userValidator input = validate $ UserTemplate {
name = validateName input,
age = validateAge input,
email = validateEmail input
}

您可以更轻松地组合较小的验证器 - 这样每个验证器都不需要知道整个错误结构 - 使用镜头。

关于validation - 是否有必要以递归方式编写 Haskell 泛型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45510332/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com