如何使非法价值无法代表?

时间:2017-02-20 10:03:06

标签: haskell f# functional-programming

功能编程中的设计方法是making illegal states unrepresentable。我总是看到这是通过类型的结构完成的,但类型的呢?

如果我有一个名为Email的字符串怎么办?我只希望它保留一个有效的电子邮件地址(针对某些正则表达式进行检查)?我怎样才能以功能的方式做到这一点(不使用OOP)?

5 个答案:

答案 0 :(得分:22)

常见的习惯用法是使用智能构造函数

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

这将在运行时验证电子邮件,而不是编译时间。

对于编译时验证,需要利用String的GADT重型变体,或使用模板Haskell(元编程)进行检查(假设电子邮件值是文字)。

对于那些支持它们的语言(例如Agda,Idris,Coq),依赖类型也可以确保值的形式正确。 F-star是F#的一种变体,它可以验证前置条件/​​后置条件,并实现一些高级静态检查。

答案 1 :(得分:19)

我定位,就像你执行所有运行时错误处理一样吗?

如果你使用类和属性进行封装",你就会抛出一个异常(即在setter中),某些代码,在调用链中更高的位置,会 要小心。这不是你的"类和属性"神奇地解决这个问题,抛出和捕捉异常是你的纪律。在大多数任何FP语言中,您都有大量的表示形式,用于表示错误的值/输入,从简单的Maybe或更详细的Either(或在F#中调用的任何内容)到完整的异常,强制立即停止与stderr消息。适合当前的app / lib上下文。

"使非法国家无法代表"类型系统/编译器理解方法:不是,因为它可以预先排除很多易于制造的开发人员错误。用户错误

当然,我们如何将处理越来越多的错误类型转移到静态(编译)方面进行学术探索和研究,在Haskell中,LiquidHaskell就是一个突出的例子。但是,如果你有一个时间机器,如果编译后读取的输入是错误的,你就不能追溯性地编译你的程序:D换句话说,防止错误的电子邮件地址的唯一方法是强加一个GUI不可能让一个人通过。

答案 2 :(得分:11)

我通常按照@chi的方式做。正如他所说,你也可以使用Template 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|sibi@mydomain.in|]
"sibi@mydomain.in"
> [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|]

您可以看到如何在无效输入上获得编译错误。

答案 3 :(得分:5)

看来,@ chi和@Sibi的答案都是精炼类型的答案。即,包含其他类型的类型,同时使用验证器限制支持的值的范围。验证可以在运行时和编译时完成,具体取决于用例。

恰好我已经创作了"refined"这个图书馆,它为这两个案例提供了抽象。请点击链接进行广泛的介绍。

要在场景中应用此库,请在一个模块中定义谓词:

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@example.info"

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

Abstracts over the solution involving Lift and QuasiQuotes.
-}
staticallyCheckedEmailLiteral :: EmailBytes
staticallyCheckedEmailLiteral =
  $$(refineTH "email@example.info")


-- * 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

另外请注意,这只是Refinement Types可以涵盖的表面。对他们来说实际上有更多有用的属性。

答案 4 :(得分:3)

最近我回答了这个问题。

这是post

您的问题的背景是关于有效的电子邮件。 代码的整体结构将利用活动模式:

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