功能编程中的设计方法是making illegal states unrepresentable。我总是看到这是通过类型的结构完成的,但类型的值呢?
如果我有一个名为Email的字符串怎么办?我只希望它保留一个有效的电子邮件地址(针对某些正则表达式进行检查)?我怎样才能以功能的方式做到这一点(不使用OOP)?
答案 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