2017-02-20 26 views
21

Метод проектирования в функциональном программировании making illegal states unrepresentable. Я всегда вижу, что это выполняется с помощью структуры типов, но как насчет значения типов?Как сделать незаконные ценности непредсказуемыми?

Что делать, если у меня есть строка с именем «Электронная почта», и я хочу, чтобы она сохраняла действительный адрес электронной почты (проверено на некоторое регулярное выражение)? Как я могу это сделать функциональным способом (без использования ООП)?

+5

Сделать неизменный тип, конструктор бросает, если данные некорректны. – ildjarn

+3

В Haskell вы можете записать тип регулярных выражений RegExp: включить «DataKinds», и теперь вы можете записать тип данных, действительный для данного regexp 'RegData :: RegExp -> *', и * singleton * типы для регулярных выражений 'REGEXP :: RegExp -> *'. Вероятно, вам также понадобится проверительный синтаксический анализатор 'regParse :: REGEXP r -> String -> Maybe (RegData r)', поэтому вы можете перейти от строк к проверенным данным, учитывая значение singleton для регулярного выражения, которое вы хотите. Если вы можете написать распознаватель регулярных выражений, то 'regParse' является доказательством, уточняющим ту же идею. – pigworker

+6

"действительный адрес электронной почты (отмеченный против некоторого Regex)" Не очень хороший пример, потому что очень сложно создать регулярное выражение, которое может правильно соответствовать полному набору действительных адресов электронной почты. – JAB

ответ

19

Я бы положил, так же, как вы делаете все свои runtime обработка ошибок?

Если вы сделали это с помощью классов и свойств для инкапсуляции, вы бы выбрали исключение (то есть в установщике), что какой-то код, где-то выше в цепочке вызовов, будет иметь, чтобы не упустить. Это не ваши «классы и свойства», которые волшебным образом решают это, ваша дисциплина бросает и ломает исключения. На большинстве любых языков FP у вас есть широкий арсенал представлений для сигнализации ошибочных значений/входов, от простых Maybe или более подробно Either (или что бы они ни назывались в F #;), до полномасштабных исключений, для принудительного немедленного прекращения -stderr-сообщение. Как подходит к текущему контексту приложения/lib.

«делает незаконные состояния непредставимо» типов для упреждения как многие простой в изготовлении, в-тепло-оф-момент ошибки разработчиков как система типа/компилятор понимает, как к : не пользовательская ошибка.

Есть, конечно, научные исследования и исследования того, как мы можем перенести обработку все большего количества ошибок на статическую (компиляционную) сторону, в Haskell есть яркий пример LiquidHaskell. Но пока у вас нет машины времени, вы не можете ретроактивно предотвратить компиляцию своей программы, если ввод, который он читает после компиляции, является ошибочным: D, другими словами, единственный способ предотвратить неправильные адреса электронной почты - наложить графический интерфейс, который не может пусть один через.

+7

В F #, 'Maybe' и' Either' являются 'Option' и' Choice' (или 'Result' в F # 4.1). – ildjarn

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 

Это проверит электронные письма во время выполнения, а не время компиляции.

Для проверки времени компиляции необходимо использовать GADT-тяжелый вариант String или использовать шаблон Haskell (метапрограммирование) для выполнения проверок (при условии, что значение электронной почты является литералом).

Зависимые типы также могут гарантировать, что значения имеют правильную форму для тех языков, которые их поддерживают (например, Agda, Idris, Coq). F-star - это вариант F #, который может проверять предварительные условия/постусловия и достигать некоторых расширенных статических проверок.

+1

Если возможно, мне бы хотелось увидеть способ GADT. :) – Sibi

+0

@ Сиби Я на самом деле этого не делаю! : -P Это, вероятно, будет включать нечто вроде singleton 'data C (c :: Char), где A :: C 'a'; ... ', а затем' data S (s :: String), где Nil :: S ""; Минусы :: C c -> S s -> S (c: s) '. Затем у вас есть «данные электронной почты, где E-mail: S s -> Proof s Start -> Email», где «Proof s Start» мучительно кодирует DFA на уровне типа, проверяя, что строка действительно является электронной почтой. Для конечных состояний у вас есть «Pi :: Proof» «End» и для переходов «q1-c-> q2» middle 'Pj :: C c -> Доказательство s q2 -> Доказательство (c: s) q1' и т. Д. Болезненный и совсем не удобно, ИМО :-P – chi

+0

А, ладно!Я посмотрю, смогу ли я придумать эту реализацию сегодня вечером, просто для удовольствия. :-) – Sibi

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|[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|] 

Вы можете увидеть, как вы получите компилировать ошибку недопустимого ввода.

+0

И все это можно сделать в одном файле для экспериментов? Очень круто. – Dogweather

+0

Да. Я использую опцию 'exec ghci' специально из стека в скрипте: https://docs.haskellstack.org/en/stable/GUIDE/#loading-scripts-in-ghci – Sibi

5

Как представляется, оба ответа от @chi и @Sibi - это то, что касается типов уточнения. I.e., типы, которые включают другие типы, ограничивая диапазон поддерживаемых значений валидатором. Проверка может выполняться как во время выполнения, так и во время компиляции в зависимости от варианта использования.

Так получилось, что я создал "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 

затем использовать его в любом другом модуле (это требуется в связи с Template 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

Это скорее реклама, чем ответ; вы могли бы хотя бы привести пример того, как работает ваша библиотека здесь? – chepner

+0

Ответ обновлен. –

+0

Спасибо! Трудно было найти практические примеры уточненных или зависимых типов; используется для решения общих и обычных задач разработки приложений. Ваша библиотека - это, по сути, слой абстракции и упрощения LH? – Dogweather

3

Это ответили для меня недавно.

Вот post.

Контекст вашего вопроса касательно действительного письма. Общая структура кода будет использовать Активные Patterns:

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