しかし、最初に、いくつかの歌詞。
Habréでいくつかの記事が公開され、関数型言語でのデータ検証の方法が詳細に説明されました。
この記事は、この誇大広告の私の5つのコペックです。 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ブラシを取得します。
モデル化する1つの方法は、2番目のデータ型を使用することです。
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の単なる中置同義語です
AND (<*>)はApplicative Functorを適用する関数です
オプションのタイプの場合、これらの関数には次の定義があります
-- already in Prelude fmap :: Functor f => (a -> b) -> fa -> fb (<$>) :: Functor f => (a -> b) -> fa -> fb (<$>) = fmap
AND (<*>)はApplicative Functorを適用する関数です
-- 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
検証は機能しますが、追加のルーチンコードを手動で記述するのは面倒です。これは完全に機械的に行われるためです。 さらに、これらの努力の重複は、3つの定義すべてが同期したままであることを確認するために、将来的に脳を使用する必要があることを意味します。 コンパイラがこれを処理できたら素晴らしいでしょうか?
サプライズ! HE CAN! 背の高い家族が私たちを助けてくれます!
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 [] [] :: * -> *
これは優良企業の例です。
PersonとMaybePersonの両方を、次の単一の高品位データで説明できることに注意してください。
data Person' f = Person { pName :: f String , pAge :: f Int }
ここで、何かf (性別*-> * )でPerson 'をパラメーター化します。これにより、元の型を使用するために次のことができます。
type Person = Person' Identity type MaybePerson = Person' Maybe
ここでは、単純なラッパータイプのIdentityを使用します
-- 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
この煩わしさをさりげなく除去することができます。その後、 Person 'のそのような定義が本当に有用である理由を調べます。 識別子を取り除くために、それらを消去するタイプのファミリ(タイプレベルの関数)を使用できます。
{-# 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)
記事の第2部については、 Genericの結論が必要です。
HKDタイプファミリを使用するということは、GHCがビューのIDラッパーを自動的に消去することを意味します。
ghci> :t pName @Identity pName :: Person -> String ghci> :t pName @Maybe pName :: Person -> Maybe String
そして、まさにこのバージョンの最高のPersonが、元のバージョンの代替として最適な方法で使用できます。
明らかな問題は、このすべての作業を完了して自分用に購入したものです。 この質問に答えるのに役立つ検証の文言に戻りましょう。
これで、新しい手法を使用して書き換えることができます。
validate :: Person' Maybe -> Maybe Person validate (Person name age) = Person <$> name <*> age
それほど興味深い変更ではないでしょうか? しかし、陰謀は、変更する必要がほとんどないことです。 ご覧のとおり、最初の実装に一致するのは型とパターンのみです。 ここで適切なのは、 PersonとMaybePersonを同じビューに統合したため、名目上の意味でのみ接続されなくなったことです。
ジェネリックとより一般的な検証関数
検証関数の現在のバージョンは、コードが非常に日常的であっても、新しいデータ型ごとに記述する必要があります。
上位のデータ型で機能する検証バージョンを作成できます。
TemplateHaskellを使用できますが、コードを生成し、極端な場合にのみ使用されます。 しません。
秘密はGHC.Genericsに連絡することです 。 ライブラリに慣れていない場合、Haskellの通常のデータ型から、スマートプログラマー(つまり:us。)によって構造的に制御できる一般的な表現への同型を提供します。 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 ioとGValidate 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 'に言及していないことです。 この関数は、高品質データとして定義されたすべてのタイプで機能します。 出来上がり!
まとめ
今日はこれで終わりです。 高品質のデータの概念に精通し、より伝統的な方法で定義されたデータ型と完全に同等である方法を理解し、このアプローチで可能なことを垣間見ました。
次のような、あらゆる種類の驚くべきことを実行できます。TemplateHaskellに頼らずに、任意のデータ型のレンズを生成します。 データ型ごとのシーケンス 。 レコードフィールドを使用するための依存関係を自動的に追跡します。
高出産の幸せなアプリケーション!
オリジナル: 高次データ