型レベルリテラルを使用したHaskell範囲制御演算

関数型プログラミング(FP)は、ご存じのとおり、信頼性のある(エラーのない)コードを作成するのに役立ちます。



明らかに、これは格言です。 エラーのないプログラムはありません。 ただし、FPは厳密な静的型付けと型システムの開発を組み合わせることにより、コンパイル段階でプログラマの避けられないエラーを特定することができます。 おそらくOCamlにも当てはまりますが、私はHaskellについて話しています。



しかし、信頼できるコードを書くという目標を設定した場合、Haskellの可能性はここで無限ではないことがすぐにわかります。 他の言語でこの目的(安全なコードの構築)のために存在するすべてがHaskellで簡単に実装されるわけではありません。 彼らがここで私を修正してくれたらいいのですが、悲しいかな。



もちろん、まず第一に、信頼性の高いコード記述(それほどではないが、関連するPascal)のために特別に開発されたAda言語に注意を払う価値があります。 私の意見では、Adaのイデオロギーは古くから古くなっていますが、構文は80年代の化石を引き出しており、コードのセキュリティを高めると思われるいくつかのアイデアは今やにやにや笑いを引き起こします。 そのすべてのために、Adaは特定の条件( データ検証制約検証 )に対して静的および動的なデータ検証のシステムを開発しました。 簡単に言えば、変数が更新されるたびに、コンパイラーは指定したテストの実行を出力コードに追加できます。 最も単純で最も一般的なケースは範囲の検証です。値は指定された制限を超えます。 このような制御もパスカルです。 Adaを置き換えることを主張することなく(軍隊や航空電子工学などの標準です)、Adaのセキュリティ標準にアプローチして、Haskellで範囲検証を始めようとします。 明らかに、 Numクラスの算術関数(少なくとも)をオーバーロードし、それらに範囲制御を設定し、それを超えると例外をスローする必要があります。

すぐに問題に遭遇します-Numクラスの算術関数で、

(+) :: a -> a -> a
      
      





チェックされた範囲の境界を設定する場所はありません。 何ができますか?



オプション1 範囲の境界と処理される値の3つの数値のレコードを作成し、そのようなNumレコードを定義(インスタンス化)します。 欠点は明らかです。 範囲の境界を、値ごと(100,000の場合もあります)ではなく、タイプごとに1つのインスタンスに格納すれば十分です。



オプション2 テンプレートHaskellを使用して生成されたクラスでハードバインドチェックを定義できます。 このオプションは非常に可能です。 THを使用すると、すべてを行うことができます。 しかし、コンパイル時に他の方法で範囲制限を設定する方法を見つけてみましょう。



オプション3 比較的最近、GHC 7.8以降、混乱しない限り、 Type-Level Literalsと呼ばれる機会が現れました 。 型の説明に定数を設定し、さらに関数で使用できます。



このメカニズムを使用して範囲検証を実装してみましょう。



制御された数の場合、経済的に完全なデータ型を開始するのではなく、newtypeの実行中に必要なオーバーヘッドが少なくなるように記述します。



 newtype RgVld (lo :: Nat) (hi :: Nat) a = RgVld { unRgVld :: a } deriving (Eq, Ord)
      
      





RgVldは範囲検証の略です。 そして、 Nat型( GHC.TypeLitsで定義されている )のlohiは、型定義の同じ定数-範囲境界です。 ここでは、それらは整数です(それらは整数に変換されますが、負の値にはできません)。 文字列もあります-しかし、文字列で制限を記述し、実行時にそれらを文字列に変換するために-いいえ、スクリプトを書いていません。



実際、このタイプは範囲検証の実装の本質です。 これで作成できます:

 instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Num (RgVld lo hi a) where (RgVld l) + (RgVld r) = chkVld "(+)" $ RgVld $ l+r (RgVld l) - (RgVld r) = chkVld "(-)" $ RgVld $ lr (RgVld l) * (RgVld r) = chkVld "(*)" $ RgVld $ l*r fromInteger n = chkVld "fromInteger" $ RgVld $ fromInteger n abs = id signum (RgVld v) = RgVld $ signum v
      
      





KnownNatクラスはGHC.TypeLitsでも定義されています 。 なぜなら 結果の値のチェックは同じです。それらについては、ヘルパークラスを作成できます。

 class CheckValidation a where chkVld:: String -> a -> a
      
      



(これは、他のタイプのチェックに適している場合があります)唯一の関数chkVldを使用します 。この関数は、範囲に入る値を渡し、範囲外の値に対して例外をスローします。 その最初の引数は、例外を引き起こした関数を示す例外メッセージのサブストリングです。



 instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => CheckValidation (RgVld lo hi a) where chkVld whr r@(RgVld v) = let lo' = natVal (Proxy :: Proxy lo) hi' = natVal (Proxy :: Proxy hi) in if v < fromInteger lo' || v > fromInteger hi' then throw $ OutOfRangeException $ "out of range [" ++ show lo' ++ " .. " ++ show hi' ++ "], value " ++ show v ++ " in " ++ whr else r
      
      





当然、例外クラス自体を作成することを忘れないでください:



 data OutOfRangeException = OutOfRangeException String deriving Typeable instance Show OutOfRangeException where show (OutOfRangeException s) = s instance Exception OutOfRangeException
      
      





RgVld型の場合、この場合はShowRead 、および非常にシンプルですが明らかに有用なBoundedクラスも実装します。



 instance (KnownNat lo, KnownNat hi, Show a) => Show (RgVld lo hi a) where show (RgVld v) = show v instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a, Read a) => Read (RgVld lo hi a) where readsPrec w = \s -> case readsPrec ws of [] -> [] [(v,s')] -> [(chkVld "readsPrec" $ RgVld v, s')] instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Bounded (RgVld lo hi a) where minBound = fromInteger $ natVal (Proxy :: Proxy lo) maxBound = fromInteger $ natVal (Proxy :: Proxy hi)
      
      





なぜなら それは「軍事」に強く関連するAda言語に関するものでした。私たちのプログラムは複数の弾頭を持つICBMを制御すると仮定します。 それらに1から番号が付けられており、合計で20あり、もちろんそれぞれが原子爆弾-A-bomb、 "H-bomb"を持っているとします。 abに減らします。 そして、ここにH爆弾を作成するための補助関数があります:



 ab:: Int -> RgVld 1 20 Int ab = RgVld
      
      





変数は、ICBMの爆弾番号で、範囲は1〜20です。ミサイルをアップグレードする場合、この補助機能でのみ番号20を変更する必要があります。 ご覧ください。



 *RangeValidation> ab 2 + ab 3 5 *RangeValidation> ab 12 + ab 13 *** Exception: out of range [1 .. 20], value 25 in (+) *RangeValidation>
      
      





-これがHaskellの範囲制御です。



注意深い読者は、「通常、範囲内に2つの数値を追加するのではなく、範囲タイプにオフセットを追加する」ことに反対する場合があります。 実際、これはこの実装にとって重要ではありません。チェックされるのは操作の入力値ではなく、出力のみであるため、割り込みは発生しません。



 *RangeValidation> ab 20 + ab 0 20 *RangeValidation>
      
      





しかし、どうやらそれは見た目も美しくもありません。 追加のクラスを紹介します

 class Num' ab where (+.) :: a -> b -> a (-.) :: a -> b -> a (*.) :: a -> b -> a
      
      



異なるタイプのオペランドを使用して算術演算を実装し、定義することによりRgVldをそのインスタンスにします

 instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Num' (RgVld lo hi a) a where (RgVld l) +. r = chkVld "(+.)" $ RgVld $ l+r (RgVld l) -. r = chkVld "(-.)" $ RgVld $ lr (RgVld l) *. r = chkVld "(*.)" $ RgVld $ l*r
      
      





関数(+。)、(-。)、(*。)は通常のものと似ていますが、範囲タイプと通常の番号でアクションを実行します。 例:

 *RangeValidation> ab 5 -. (3 :: Int) 2 *RangeValidation>
      
      



-はい、定数の場合、数値のタイプを明示的に指定する必要があります。



当然、範囲タイプは整数である必要はありません。 燃料レベルを決定するための補助関数を作成します。



 fuel:: Double -> RgVld 0 15 Double fuel = RgVld
      
      





そして、給油するときに範囲タイプの動作を確認します。



 *RangeValidation> fuel 4.6 + fuel 4.5 9.1 *RangeValidation> fuel 9.1 + fuel 6 *** Exception: out of range [0 .. 15], value 15.1 in (+) *RangeValidation>
      
      



-ああ、イェイ、イェイ。 注いだ!



残念ながら、適用された「技術」の制限のために「 型レベルリテラル」のため、範囲はまだ整数で指定されています。 GHCの作者はそれを改善するかもしれません(一般的には、別の人のために多少考えられていました)。 それまでの間、私たちは出会ったことに満足しています。



完全なサンプルコード:



 {-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables, MultiParamTypeClasses, FlexibleInstances #-} {-# LANGUAGE DeriveDataTypeable #-} module RangeValidation where import Data.Proxy import GHC.TypeLits import Data.Typeable import Control.Exception data OutOfRangeException = OutOfRangeException String deriving Typeable instance Show OutOfRangeException where show (OutOfRangeException s) = s instance Exception OutOfRangeException class CheckValidation a where chkVld:: String -> a -> a instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => CheckValidation (RgVld lo hi a) where chkVld whr r@(RgVld v) = let lo' = natVal (Proxy :: Proxy lo) hi' = natVal (Proxy :: Proxy hi) in if v < fromInteger lo' || v > fromInteger hi' then throw $ OutOfRangeException $ "out of range [" ++ show lo' ++ " .. " ++ show hi' ++ "], value " ++ show v ++ " in " ++ whr else r newtype RgVld (lo :: Nat) (hi :: Nat) a = RgVld { unRgVld :: a } deriving (Eq, Ord) instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Num (RgVld lo hi a) where (RgVld l) + (RgVld r) = chkVld "(+)" $ RgVld $ l+r (RgVld l) - (RgVld r) = chkVld "(-)" $ RgVld $ lr (RgVld l) * (RgVld r) = chkVld "(*)" $ RgVld $ l*r fromInteger n = chkVld "fromInteger" $ RgVld $ fromInteger n abs = id signum (RgVld v) = RgVld $ signum v infixl 6 +.,-. infixl 7 *. class Num' ab where (+.) :: a -> b -> a (-.) :: a -> b -> a (*.) :: a -> b -> a -- (/.) :: a -> b -> a instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Num' (RgVld lo hi a) a where (RgVld l) +. r = chkVld "(+.)" $ RgVld $ l+r (RgVld l) -. r = chkVld "(-.)" $ RgVld $ lr (RgVld l) *. r = chkVld "(*.)" $ RgVld $ l*r instance (KnownNat lo, KnownNat hi, Show a) => Show (RgVld lo hi a) where show (RgVld v) = show v instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a, Read a) => Read (RgVld lo hi a) where readsPrec w = \s -> case readsPrec ws of [] -> [] [(v,s')] -> [(chkVld "readsPrec" $ RgVld v, s')] instance (KnownNat lo, KnownNat hi, Num a, Ord a, Show a) => Bounded (RgVld lo hi a) where minBound = fromInteger $ natVal (Proxy :: Proxy lo) maxBound = fromInteger $ natVal (Proxy :: Proxy hi) -- examples ab:: Int -> RgVld 1 20 Int ab = RgVld fuel:: Double -> RgVld 0 15 Double fuel = RgVld
      
      






All Articles