Подтвердить что ты не робот

Как сделать незаконные ценности непредсказуемыми?

Метод проектирования в функциональном программировании делает незаконные состояния непредсказуемыми. Я всегда вижу, что это выполняется со структурой типов, но как насчет значения типов?

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

4b9b3361

Ответ 1

Я бы положил, так же, как и вся обработка ошибок во время выполнения?

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

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

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

Ответ 2

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

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 #, который может проверять предварительные условия/постусловия и выполнять некоторые дополнительные статические проверки.

Ответ 3

Я обычно делаю так, как сделал @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|]

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

Ответ 4

Как оказалось, оба ответа @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 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

В последнее время мне ответили.

Здесь сообщение.

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

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