高数据

是的,是的,您没有做梦,您没听错-非常好。 Kind是类型理论术语,从本质上讲是类型[数据]的类型。

但是首先,一些歌词。

在Habré上发表了几篇文章,详细描述了功能语言中的数据验证方法。

本文是我炒作的五分钱。 我们将考虑在Haskell中进行数据验证。

类型验证


以前考虑过使用类型验证的验证技术示例:

type EmailContactInfo = String type PostalContactInfo = String data ContactInfo = EmailOnly EmailContactInfo | PostOnly PostalContactInfo | EmailAndPost (EmailContactInfo, PostalContactInfo) data Person = Person { pName :: String, , pContactInfo :: ContactInfo, } 

使用这种方法,根本不可能创建不正确的数据。 但是,尽管这样的验证非常容易创建和读取,但使用它会迫使您编写大量例程并对代码进行大量更改。 这意味着这种方法的使用仅限于真正重要的数据。

高数据验证




在本文中,我们将介绍一种不同的验证方法-使用高质量数据。

假设我们有一个数据类型:

 data Person = Person { pName :: String , pAge :: Int } 

并且只有在记录的所有字段均有效的情况下,我们才会验证数据。
由于Haskell在功能上优于大多数功能语言,因此可以轻松摆脱大多数例程。

在这里是可能的,因此该方法在Haskell的库作者中被广泛使用。

出于讨论目的,让我们想象一下,我们希望用户通过Web表单或其他方式填写个人信息。 换句话说,它们有可能破坏部分信息的填充,而不一定会使其余的数据结构无效。 如果他们成功填充了整个结构,我们希望获得一个填充的Person笔刷。

一种建模方法是使用第二种数据类型:

 data MaybePerson = MaybePerson  { mpName :: Maybe String  , mpAge :: Maybe Int  } 

我记得在哪里使用了可选类型:

 -- already in Prelude data Maybe a = Nothing | Just a 

从这里开始,验证功能非常简单:

 validate :: MaybePerson -> Maybe Person validate (MaybePerson name age) =  Person <$> name <*> age 

有关功能(<$>)和(<*>)的更多详细信息
函数(<$>)只是fmap Functor的中缀同义词

 -- already in Prelude fmap :: Functor f => (a -> b) -> fa -> fb (<$>) :: Functor f => (a -> b) -> fa -> fb (<$>) = fmap 

AND (<*>)是应用应用函子的功能

 -- already in Prelude (<*>) :: Applicative f => f (a -> b) -> fa -> fb 

对于可选类型,这些函数具有以下定义

 -- already in Prelude (<$>) :: (a -> b) -> Maybe a -> Maybe b _ <$> Nothing = Nothing f <$> (Just a) = Just (fa) (<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b (Just f) <*> m = f <$> m Nothing <*> _ = Nothing 


我们的验证有效,但是手工编写其他例程代码很烦人,因为这是完全机械地完成的。 此外,重复这些工作意味着将来我们将需要动脑筋来确保所有三个定义保持同步。 如果编译器可以处理这个问题,那会很好吗?

惊喜! 他可以! 一个高大的家庭会帮助我们!

在Haskell中,有一个属,它是一种 ,最简单,最准确的解释是,属是一种类型[数据]。 最广泛使用的属是* ,可以称为“最终”

 ghci> :k Int Int :: * ghci> :k String String :: * ghci> :k Maybe Int Maybe Int :: * ghci> :k Maybe String Maybe String :: * ghci> :k [Int] [Int] :: * 

什么样的呢?
 ghci> :k Maybe Maybe :: * -> * ghci> :k [] [] :: * -> * 

这是一个很好的例子。

请注意,我们可以使用以下单个高级数据来描述PersonMaybePerson

 data Person' f = Person { pName :: f String , pAge :: f Int } 

在这里,我们将Person'设置f的参数(带有性别*-> * ),这使我们能够执行以下操作以使用原始类型:

 type Person = Person' Identity type MaybePerson = Person' Maybe 

在这里,我们使用一个简单的包装类型的身份

 -- already in Prelude newtype Identity a = Identity { runIdentity :: a } 

尽管这可行,但是对于Person来说有点烦 ,因为现在我们所有的数据都包装在Identity中

 ghci> :t pName @Identity pName :: Person -> Identity String ghci> :t runIdentity. pName runIdentity. pName :: Person -> String 

我们可以简单地消除这种烦恼,然后我们将研究为什么对“ 人”的这种定义真正有用。 为了摆脱标识符,我们可以使用一系列的类型(类型级别的函数)来擦除它们:

 {-# LANGUAGE TypeFamilies #-} -- "Higher-Kinded Data" type family HKD fa where HKD Identity a = a HKD fa = fa data Person' f = Person  { pName :: HKD f String  , pAge :: HKD f Int  } deriving (Generic) 

在本文的第二部分,我们需要通用的结论。

使用HKD类型族意味着GHC在我们的视图中会自动删除所有Identity包装器:

 ghci> :t pName @Identity pName :: Person -> String ghci> :t pName @Maybe pName :: Person -> Maybe String 

正是这种最高版本的Person可以最佳方式替代我们原来的Person

一个显而易见的问题是,我们完成所有这些工作后为自己买了什么。 让我们回到验证的用语来帮助我们回答这个问题。

现在我们可以使用新技术重写它:

 validate :: Person' Maybe -> Maybe Person validate (Person name age) = Person <$> name <*> age 

这不是很有趣的变化吗? 但是有趣的是,几乎没有什么需要改变的。 如您所见,只有我们的类型和模式与我们的初始实现相匹配。 此处的整洁之处在于,我们现在将PersonMaybePerson合并到同一视图中,因此它们不再仅在名义上相连。

泛型和更通用的验证功能


即使代码非常常规,也需要为每种新的数据类型编写当前版本的验证功能。

我们可以编写一个适用于任何更高数据类型的验证版本。

可以使用Template Haskell,但是它会生成代码,并且仅在极端情况下使用。 我们不会。

秘密是联系GHC.Generics 。 如果您不熟悉该库,那么它可以提供从Haskell常规数据类型到可以由智能程序员(即:我们)在结构上控制的一般表示形式的同构方法。 GHC为我们编写了与类型无关的代码。 这是一种非常巧妙的技术,如果您以前从未看过它,它会逗您的脚趾。

最后,我们希望得到如下结果:

 validate :: _ => d Maybe -> Maybe (d Identity) 

泛型的角度来看任何类型通常都可以分为几种设计:

 -- undefined data, lifted version of Empty data V1 p -- Unit: used for constructors without arguments, lifted version of () data U1 p = U1 -- a container for ac, Constants, additional parameters and recursion of kind * newtype K1 icp = K1 { unK1 :: c } -- a wrapper, Meta-information (constructor names, etc.) newtype M1 itfp = M1 { unM1 :: fp } -- Sums: encode choice between constructors, lifted version of Either data (:+:) fgp = L1 (fp) | R1 (gp) -- Products: encode multiple arguments to constructors, lifted version of (,) data (:*:) fgp = (fp) :*: (gp) 

也就是说,可能存在未初始化的结构,无参数的结构,常量结构,元信息(构造函数等)。 以及结构的关联-总计或OR-OR类型和动画类型的关联,它们也是简短形式的关联或记录。

首先,我们需要定义一个将成为我们转型的主力的类。 从经验来看,这始终是最困难的部分-这些广义转换的类型非常抽象,我认为很难推理。 让我们使用:

 {-# LANGUAGE MultiParamTypeClasses #-} class GValidate io where gvalidate :: ip -> Maybe (op) 

您可以使用“软和慢”规则来推理类类型的外观,但是通常您需要输入和输出参数。 它们两者都必须属于*-> *属,然后由于人类未知的黑暗,邪恶的原因而传送存在的p 。 然后,我们使用一张小清单,帮助我们将目光包裹在这可怕的地狱景观上,稍后我们将依次介绍。

无论如何,我们的班级已经掌握了,现在我们只需要为不同类型的GHC.Generic编写类的实例即可 。 我们可以从基本案例开始,我们必须能够验证它, 也许是k

 {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE TypeOperators #-} instance GValidate (K1 a (Maybe k)) (K1 ak) where -- gvalidate :: K1 a (Maybe k) -> Maybe (K1 ak) gvalidate (K1 k) = K1 <$> k {-# INLINE gvalidate #-} 

K1是“常量类型”,这意味着我们的结构递归在此结束。 在我们的Person'的示例中这将是pName :: HKD f String

在大多数情况下,当您拥有基本案例时,其余只是其他类型的机械定义实例。 除非您需要在任何地方访问有关源类型的元数据,否则这些实例几乎总是同质的。

我们可以从乘法结构开始-如果我们有GValidate ioGValidate i'o' ,我们应该能够并行运行它们:

 instance (GValidate io, GValidate i' o') => GValidate (i :*: i') (o :*: o') where gvalidate (l :*: r) = (:*:) <$> gvalidate l <*> gvalidate r {-# INLINE gvalidate #-} 

如果K1直接引用我们的Person'选择器,则(:* :)大致对应于逗号的语法,我们在记录中将字段分开。

我们可以为副产品或摘要结构定义一个类似的GValidate实例(数据定义中的对应值在|中分开):

 instance (GValidate io, GValidate i' o') => GValidate (i :+: i') (o :+: o') where gvalidate (L1 l) = L1 <$> gvalidate l gvalidate (R1 r) = R1 <$> gvalidate r {-# INLINE gvalidate #-} 

另外,由于我们不在乎查找元数据,因此我们只需在元数据构造函数上定义GValidate io

 instance GValidate io => GValidate (M1 _a _b i) (M1 _a' _b' o) where gvalidate (M1 x) = M1 <$> gvalidate x {-# INLINE gvalidate #-} 

现在,我们留下了没有意思的结构以进行完整描述。 我们将为非住宅类型( V1 )和没有任何参数( U1 )的设计师提供以下琐碎的实例:

 instance GValidate V1 V1 where gvalidate = undefined {-# INLINE gvalidate #-} instance GValidate U1 U1 where gvalidate U1 = Just U1 {-# INLINE gvalidate #-} 

在这里使用undefined是安全的,因为只能使用V1值来调用它。 对我们来说幸运的是, V1没有人居住和未初始化,所以这永远不会发生,这意味着在道德上使用undefined是正确的。

事不宜迟,现在我们有了整个机制,我们终于可以编写一个非通用的验证版本:

 {-# LANGUAGE FlexibleContexts #-} validate :: ( Generic (f Maybe) , Generic (f Identity) , GValidate (Rep (f Maybe)) (Rep (f Identity)) ) => f Maybe -> Maybe (f Identity) validate = fmap to . gvalidate . from 

每当函数的签名比实际实现的签名长时,您都会大笑。 这意味着我们雇用了一个编译器来为我们编写代码。 对于验证而言重要的是它没有提到Person' ; 此功能适用于定义为高质量数据的任何类型。 瞧!

总结


今天的家伙就这些了。 我们熟悉了高质量数据的概念,了解了它如何完全等同于以更传统的方式定义的数据类型,并且还瞥见了这种方法有什么可能。

它使您可以做各种令人惊奇的事情,例如:为任意数据类型生成镜头而无需借助Template Haskell; 按数据类型排序 ; 并自动跟踪使用记录字段的依赖性。

高分娩的快乐应用!

原始数据高级数据

Source: https://habr.com/ru/post/zh-CN429104/


All Articles