web-dev-qa-db-ja.com

型安全性に関するHaskellタイプとnewtype

Haskellではnewtypedataと比較されることが多いことは知っていますが、私はこの比較を技術的な問題というよりも設計の観点から提起しています。

必須/オブジェクト指向言語では、アンチパターン " primitive obsession "があります。この場合、プリミティブ型を多用すると、プログラムの型安全性が低下し、同じ型の値の誤った互換性が発生します。さまざまな目的を目的としています。たとえば、多くのものが文字列である可能性がありますが、コンパイラが静的にそれを知ることができれば、それは名前であり、住所の都市であることを意味します。

では、Haskellプログラマーはnewtypeを使用して、それ以外の場合はプリミティブ値に型を区別する頻度はどれくらいですか? typeを使用すると、エイリアスが導入され、プログラムの可読性がより明確になりますが、誤って値が交換されるのを防ぐことはできません。 haskellを学ぶと、型システムがこれまでに出会ったものと同じくらい強力であることに気付きます。したがって、これは自然で一般的な方法だと思いますが、この観点からnewtypeの使用についてはあまり議論されていません。

もちろん、多くのプログラマーは異なることをしますが、これはhaskellではまったく一般的ですか?

63
StevenC

ニュータイプの主な用途は次のとおりです。

  1. タイプの代替インスタンスを定義するため。
  2. ドキュメンテーション。
  3. データ/フォーマットの正確性の保証。

私は現在、ニュータイプを広範囲に使用するアプリケーションに取り組んでいます。 Haskellのnewtypesは、純粋にコンパイル時の概念です。例えば。以下のアンラッパーを使用して、unFilename (Filename "x")を「x」と同じコードにコンパイルします。実行時のヒットはまったくありません。 dataタイプがあります。これは、上記の目標を達成するための非常に優れた方法になります。

-- | A file name (not a file path).
newtype Filename = Filename { unFilename :: String }
    deriving (Show,Eq)

これを誤ってファイルパスとして扱いたくありません。ファイルパスではありません。これは、データベースのどこかにある概念ファイルの名前です。

アルゴリズムが正しいものを参照することは非常に重要です。ニュータイプはこれを助けます。また、セキュリティにとっても非常に重要です。たとえば、Webアプリケーションへのファイルのアップロードを検討してください。私はこれらのタイプを持っています:

-- | A sanitized (safe) filename.
newtype SanitizedFilename = 
  SanitizedFilename { unSafe :: String } deriving Show

-- | Unique, sanitized filename.
newtype UniqueFilename =
  UniqueFilename { unUnique :: SanitizedFilename } deriving Show

-- | An uploaded file.
data File = File {
   file_name     :: String         -- ^ Uploaded file.
  ,file_location :: UniqueFilename -- ^ Saved location.
  ,file_type     :: String         -- ^ File type.
  } deriving (Show)

アップロードされたファイルからファイル名を削除するこの関数があるとします。

-- | Sanitize a filename for saving to upload directory.
sanitizeFilename :: String            -- ^ Arbitrary filename.
                 -> SanitizedFilename -- ^ Sanitized filename.
sanitizeFilename = SanitizedFilename . filter ok where 
  ok c = isDigit c || isLetter c || elem c "-_."

それから、一意のファイル名を生成します。

-- | Generate a unique filename.
uniqueFilename :: SanitizedFilename -- ^ Sanitized filename.
               -> IO UniqueFilename -- ^ Unique filename.

任意のファイル名から一意のファイル名を生成するのは危険です。最初にサニタイズする必要があります。同様に、一意のファイル名は、拡張子によって常に安全です。今すぐファイルをディスクに保存し、必要に応じてそのファイル名をデータベースに入れることができます。

しかし、たくさんラップ/アンラップしなければならないのも面倒です。長期的には、特に値の不一致を回避するために価値があると思います。 ViewPatternsは多少役立ちます:

-- | Get the form fields for a form.
formFields :: ConferenceId -> Controller [Field]
formFields (unConferenceId -> cid) = getFields where
   ... code using cid ..

関数でそれをアンラップするのは問題だと言うかもしれません-cidを関数に間違って渡した場合はどうなりますか?問題ではありません。会議IDを使用するすべての関数はConferenceIdタイプを使用します。出現するのは、コンパイル時に強制される一種の機能間レベルのコントラクトシステムです。かなりいい。そうですね、特に大規模なシステムでは、できるだけ頻繁に使用しています。

58

これは主に状況の問題だと思います。

パス名を検討してください。標準のプレリュードには「typeFilePath = String」があります。これは、便宜上、すべての文字列およびリスト操作にアクセスできるようにするためです。 「newtypeFilePath = FilePath String」がある場合は、filePathLength、filePathMapなどが必要になります。そうでない場合は、変換関数を永久に使用することになります。

一方、SQLクエリを検討してください。 SQLインジェクションは一般的なセキュリティホールであるため、次のようなものを用意するのは理にかなっています。

newtype Query = Query String

次に、引用符をエスケープして文字列をクエリ(またはクエリフラグメント)に変換する関数を追加するか、同じ方法でテンプレートに空白を入力します。そうすれば、引用符のエスケープ機能を実行せずに、誤ってユーザーパラメータをクエリに変換することはできません。

19
Paul Johnson

単純な_X = Y_宣言の場合、typeはドキュメントです。 newtypeは型チェックです。これが、newtypedataと比較される理由です。

私はあなたが説明する目的のためにnewtypeをかなり頻繁に使用します:別のタイプと同じ方法で保存される(そしてしばしば操作される)何かが他のものと混同されないようにします。このようにして、わずかに効率的なdata宣言と同じように機能します。どちらかを選択する特別な理由はありません。 GHCのGeneralizedNewtypeDeriving拡張機能を使用すると、どちらの場合もNumなどのクラスを自動的に導出できるため、Intsの場合と同じように、気温や円を加算および減算できます。またはそれらの下にあるものは何でも。ただし、これには少し注意が必要です。通常、ある温度に別の温度を掛けることはありません。

これらのものがどのくらいの頻度で使用されるかについては、現在取り組んでいるかなり大きなプロジェクトの1つで、dataを約122回、newtypeを39回、96回使用しています。 typeの。

しかし、「単純な」タイプに関する限り、比率はそれが示すよりも少し近いです。なぜなら、typeの96回の使用のうち32回は、実際には次のような関数タイプのエイリアスだからです。

_type PlotDataGen t = PlotSeries t -> [String]
_

ここで、2つの余分な複雑さに注意してください。1つは、単純な_X = Y_エイリアスではなく、実際には関数型であり、もう1つは、パラメータ化されていることです。PlotDataGenは、別の型に適用する型コンストラクタです。 PlotDataGen (Int,Double)などの新しい型を作成します。この種のことを始めたとき、typeはもはや単なるドキュメントではなく、データレベルではなくタイプレベルではありますが、実際には関数です。

newtypeは、再帰型の定義が必要な場合など、typeが使用できない場合に使用されることがありますが、これはかなりまれです。したがって、少なくともこの特定のプロジェクトでは、私の「プリミティブ」型定義の約40%がnewtypesであり、60%がtypesであるように見えます。 newtype定義のいくつかは以前は型でしたが、あなたが述べた正確な理由で間違いなく変換されました。

つまり、そうです、これはよくあるイディオムです。

17
Curt J. Sampson

タイプの区別にnewtypeを使用することは非常に一般的だと思います。多くの場合、これは、異なる型クラスインスタンスを指定したり、実装を非表示にしたりするためですが、偶発的な変換から保護したいだけでも、それを行う明らかな理由です。

10