web-dev-qa-db-ja.com

違法な値を表現できないようにする方法は?

関数型プログラミングの設計方法は 違法な状態を表現できないようにする です。私はいつもこれが型の構造で達成されているのを見ますが、型のはどうですか?

Emailという文字列があり、有効な電子メールアドレスのみを保持したい場合はどうなりますか(一部の正規表現に対してチェックされます)? (OOPを使用せずに)機能的な方法でこれを行うにはどうすればよいですか?

22
Kurren

私はあなたがあなたのすべてをするのと同じ方法でランタイムエラー処理を仮定しますか?

「カプセル化用のクラスとプロパティを使用して」実行した場合、呼び出しチェーンの上位にあるコードがhave注意するという例外をスローします(つまり、セッター内)。 。これを魔法のように解決するのはあなたの「クラスとプロパティ」ではなく、例外をスローしてキャッチするのはあなたの規律です。ほとんどのFP言語では、単純なMaybeまたはより詳細なEither(またはこれらが何であれ)から、誤った値/入力を通知するための表現の幅広い武器がありますF#で呼び出されます;)、本格的な例外、stderr-messageによる即時停止の強制。現在のapp/libコンテキストに合わせて。

型の「違法な状態を表現できないようにする」とは、型システム/コンパイラが理解している限り、簡単に作成できる瞬間を先取りするためのものです開発者の間違い- to:not ユーザーエラー

もちろん、これまで以上に多くのクラスのバグの処理を静的(コンパイル)側にシフトする方法についての学術的な調査と研究があります。HaskellにはLiquidHaskellの顕著な例があります。しかし、タイムマシンができるまで、コンパイル後に読み取った入力に誤りがある場合、プログラムのコンパイルをさかのぼって防ぐことはできません:D言い換えると、間違った電子メールアドレスを防ぐ唯一の方法は、おそらく不可能なGUIを課すことです。 1つを通過させます。

20
metaleap

一般的なイディオムは、スマートコンストラクターを使用することです。

module Email (email, fromEmail, Email()) where

-- export the type, but not the constructor
newtype Email = Email String

-- export this
email :: String -> Maybe Email
email s | validEmail s = Just (Email s)
        | otherwise    = Nothing

-- and this
fromEmail :: Email -> String
fromEmail (Email s) = s

これにより、コンパイル時ではなく、実行時に電子メールが検証されます。

コンパイル時の検証では、GADTを多用するStringのバリアントを悪用するか、Template Haskell(メタプログラミング)を使用してチェックを行う必要があります(電子メールの値がリテラルの場合)。

依存型は、値をサポートする言語(Agda、Idris、Coqなど)に対して、値が正しい形式であることを保証することもできます。 F-starは、前提条件/事後条件を検証し、いくつかの高度な静的チェックを実行できるF#のバリアントです。

22
chi

私は通常、@ chiが行った方法で行います。彼が述べたように、テンプレートHaskellを使用して、コンパイル時に提供された電子メールをチェックすることもできます。それを行う例:

#!/usr/bin/env stack
{- stack
     --resolver lts-8.2
     exec ghci
     --package email-validate
     --package bytestring
-}

{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE QuasiQuotes #-}

import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Language.Haskell.TH.Syntax
import Data.ByteString.Char8
import Text.Email.Validate

instance Lift ByteString where
  lift b = [|pack $(lift $ unpack b)|]

instance Lift EmailAddress where
  lift email = lift (toByteString email)

email :: QuasiQuoter
email =
  QuasiQuoter
  { quoteExp =
    \str ->
       let (item :: EmailAddress) =
             case (validate (pack str)) of
               Left msg -> error msg
               Right email -> email
       in [|item|]
  }

さて、これをghciにロードすると:

> :set -XQuasiQuotes
> [email|[email protected]|]
"[email protected]"
> [email|invalidemail|]

<interactive>:6:1: error:
    • Exception when trying to run compile-time code:
        @: not enough input
CallStack (from HasCallStack):
  error, called at EmailV.hs:36:28 in main:EmailV
      Code: quoteExp email "invalidemail"
    • In the quasi-quotation: [email|invalidemail|]

無効な入力でコンパイルエラーが発生する方法を確認できます。

11
Sibi

表示されているように、@ chiと@Sibiの両方の回答が、詳細化タイプに関するものです。つまり、バリデーターでサポートされる値の範囲を制限しながら、他のタイプを囲むタイプ。検証は、ユースケースに応じて、実行時とコンパイル時の両方で実行できます。

たまたま、私が作成した "refined" 、両方の場合の抽象化を提供するライブラリです。詳細な紹介については、リンクをたどってください。

このライブラリをシナリオに適用するには、1つのモジュールで述語を定義します。

import Refined
import Data.ByteString (ByteString)

data IsEmail

instance Predicate IsEmail ByteString where
  validate _ value = 
    if isEmail value
      then Nothing
      else Just "ByteString form an invalid Email"
    where
      isEmail =
        error "TODO: Define me"

-- | An alias for convenince, so that there's less to type.
type EmailBytes =
  Refined IsEmail ByteString

次に、それを他のモジュールで使用します(これはテンプレートHaskellのために必要です)。

コンパイル時と実行時の両方で値を作成できます。

-- * Constructing
-------------------------

{-|
Validates your input at run-time.

Abstracts over the Smart Constructor pattern.
-}
dynamicallyCheckedEmailLiteral :: Either String EmailBytes
dynamicallyCheckedEmailLiteral =
  refine "[email protected]"

{-|
Validates your input at compile-time with zero overhead.

Abstracts over the solution involving Lift and QuasiQuotes.
-}
staticallyCheckedEmailLiteral :: EmailBytes
staticallyCheckedEmailLiteral =
  $$(refineTH "[email protected]")


-- * Using
-------------------------

aFunctionWhichImpliesThatTheInputRepresentsAValidEmail :: EmailBytes -> IO ()
aFunctionWhichImpliesThatTheInputRepresentsAValidEmail emailBytes =
  error "TODO: Define me"
  where
    {-
    Shows how you can extract the "refined" value at zero cost.

    It makes sense to do so in an enclosed setting.
    E.g., here you can see `bytes` defined as a local value,
    and we can be sure that the value is correct.
    -}
    bytes :: ByteString
    bytes =
      unrefine emailBytes

また、これはリファインメントタイプがカバーできる範囲のほんの一部であることに注意してください。実際には、はるかに便利なプロパティがあります。

5
Nikita Volkov

これは最近私のために答えられました。

これが 投稿 です。

あなたの質問の文脈は、有効な電子メールに関するものです。コードの全体的な構造は、アクティブパターンを活用します。

module File1 =

    type EmailAddress = 
        private
        | Valid   of string 
        | Invalid of string

    let createEmailAddress (address:System.String) =
        if address.Length > 0
        then Valid    address 
        else Invalid  address

    // Exposed patterns go here
    let (|Valid|Invalid|) (input : EmailAddress) : Choice<string, string>  = 
        match input with
        | Valid str -> Valid str
        | Invalid str -> Invalid str

module File2 =

    open File1

    let validEmail = Valid "" // Compiler error

    let isValid = createEmailAddress "" // works

    let result = // also works
        match isValid with
        | Valid x -> true
        | _       -> false
3
Scott Nimrod