KEMBAR78
LC2018 - Input Validation in PureScript | PDF
Input Validation in PureScript
Simpler, Safer, Stronger
Joe Kachmar
LambdaConf 2018
4 Jun 2018
Introduction
● Software Engineer at WeWork
○ Mostly writing Scala
○ Snuck a little PureScript into my last project
● Learned PureScript after Haskell
○ Finished Haskell Programming from First Principles
○ Mostly using JavaScript at work at the time
○ Wanted Haskell-style FP with JavaScript support
(actual me)
(GitHub me)
Overview
● Show what we’re trying to avoid (a.k.a. Boolean Validation)
● Define errors that can be encountered while validating any field
● Write functions that perform these primitive validations
● Show how primitive validation functions can be combined
● Define types that represent valid fields
● Define field-specific errors in terms of primitive errors
● Write functions to perform field-specific validations
● Write a function that combines field-specific validations to validate a form
Boolean Validation
function validateForm({ emailAddress, password }) {
const isEmailValid = validateEmail(emailAddress);
const isPasswordValid = validatePassword (password);
return isEmailValid && isPasswordValid
}
● validateEmailand validatePasswordreturn booleans
● Any validation failures will cause the whole function to return false
○ Doesn’t communicate which elements failed validation
○ Doesn’t communicate why failing elements were invalid
Validations on Primitive Types
The Type of Validation
data V err result = Invalid err | Valid result
● V - the validation type, parameterized by types describing errors and results
● Invalid- construct a validation error
● Valid- construct a successful validation result
Validation Helper Functions
● purescript-validation doesn’t expose Validor Invaliddirectly
● invalidfunction used to signal validation errors
● validfunction used to signal validation success
● unV function used to unwrap the validation and handle errors or successes
Representing Simple Form Input
type UnvalidatedForm =
{ emailAddress :: String
, password :: String
}
● PureScript record representation of a simple form with two input fields
● Fields have very simple, “primitive” types
Representing Primitive Errors
● Empty fields
● Fields with invalid email addresses
● Fields that are too short
● Fields that are too long
● Fields that only have uppercase characters
● Fields that only have lowercase characters
data InvalidPrimitive
= EmptyField
| InvalidEmail String
| TooShort Int Int
| TooLong Int Int
| NoLowercase String
| NoUppercase String
Primitive Validation Type Signature
validateMinimumLength
:: String
-> Int
-> V (NonEmptyList InvalidPrimitive ) String
● Validates that the input string is greater than some given length
● Takes a input and minimum length arguments
● Returns either a list of primitive errors or a valid result
Primitive Validation Function Implementation
validateMinimumLength input min
| (length input) < min =
invalid (singleton ( TooShort (length input) min))
| otherwise = pure input
● Return an error if the input is too short
● Otherwise return the valid input
Full Primitive Validation Function
validateMinimumLength
:: String
-> Int
-> V (NonEmptyList InvalidPrimitive ) String
validateMinimumLength input min
| (length input) < min =
invalid (singleton ( TooShort (length input) min))
| otherwise = pure input
● Validates that the input string is greater than some given length
Primitive Validation Function Type Signature
(Round 2)
validateContainsLowercase
:: String
-> V (NonEmptyList InvalidPrimitive ) String
● Validates that the input string is greater than some given length
● Takes a String (the input to be validated)
● Returns either a non-empty list of invalid primitive errors, or the input string
Primitive Validation Function Implementation
(Round 2)
validateContainsLowercase input
| (toUpper input) == input =
invalid (singleton ( NoLowercase input))
| otherwise = pure input
● Return an error if the input is equal to an uppercase version of itself
● Otherwise return the valid input
validateContainsLowercase
:: String
-> V (NonEmptyList InvalidPrimitive ) String
validateContainsLowercase input
| (toUpper input) == input =
invalid (singleton ( NoLowercase input))
| otherwise = pure input
● Validates that the input string contains at least one lowercase character
Full Primitive Validation Function
(Round 2)
Exercise 1.1
Write a Function to Validate that Input is Non-Empty
● Exercise URL: http://bit.ly/2sckTgl
● Hint:
○ Check out Data.String#null for testing string emptiness
● When you’re done, you should see the following output on the right hand side
Solution 1.1
Write a Function to Validate that Input is Non-Empty
validateNonEmpty
:: String
-> V (NonEmptyList InvalidPrimitive ) String
validateNonEmpty input
| null input = invalid (singleton EmptyField)
| otherwise = pure input
Exercise 1.2
Write a Function to Validate that Input Contains Uppercase Characters
● Exercise URL: http://bit.ly/2LyG6t9
● Hints:
○ Think back to the validateContainsLowercase function from before
○ Check out Data.String#toLower for making lowercase strings
● When you’re done, you should see the following output on the right hand side
Solution 1.2
Write a Function to Validate that Input Contains Uppercase Characters
validateContainsUppercase
:: String
-> V (NonEmptyList InvalidPrimitive ) String
validateContainsUppercase input
| (toLower input) == input =
invalid (singleton ( NoUppercase input))
| otherwise = pure input
Combining Primitive Validations
(with Semigroup and Apply)
Combining Validations with Apply
instance applyV :: Semigroup err => Apply (V err) where
apply (Invalid err1) (Invalid err2) = Invalid (err1 <> err2)
apply (Invalid err) _ = Invalid err
apply _ ( Invalid err) = Invalid err
apply (Valid f) (Valid x) = Valid (f x)
● Applylet’s us sequence validations together in a way that accumulates errors
● Note the Semigroupconstraint on err
● Applyalso defines an infix operator for the applyfunction: (<*>)
Embedding Valid Results with Applicative
instance applicativeV :: Semigroup err => Applicative (V err) where
pure = Valid
● Applicativelet’s us wrap a valid result in V using pure
● Note the Semigroupconstraint on err
Combining Primitive Validations
validateLength
:: String
-> Int
-> Int
-> V (NonEmptyList InvalidPrimitive ) String
validateLength input minLength maxLength =
validateMinimumLength input minLength
*> validateMaximumLength input maxLength
● *> is an infix operator we can use to sequence validations
● Applyguarantees that all errors will be collected and returned
Exercise 2
Write a Function to Validate that Input Contains Upper- and Lowercase Characters
● Exercise URL: http://bit.ly/2xeF6XT
● Hints:
○ Use primitive functions defined earlier
○ All primitive validation functions from earlier have been defined
● When you’re done, you should see the following output on the right hand side
Solution 2
Write a Function Validating that a Field Contains Uppercase Characters
validateContainsMixedCase
:: String
-> V (NonEmptyList InvalidPrimitive ) String
validateContainsMixedCase input =
validateContainsLowercase input
*> validateContainsUppercase input
Break
Validations on Specialized Fields
Representing Validated Fields
newtype EmailAddress = EmailAddress String
newtype Password = Password String
● Validated fields are opaque wrappers around String
● One unique wrapper per unique field
● Creates a boundary between unvalidated input and validated output
Representing Field Errors
data InvalidField
= InvalidEmailAddress (NonEmptyList InvalidPrimitive )
| InvalidPassword (NonEmptyList InvalidPrimitive )
● Each field must have a corresponding InvalidFieldconstructor
● Each constructor wraps a list of errors from that field’s validation
Applying a Single Primitive Validation to a Field
validateEmailAddress
:: String
-> V (NonEmptyList InvalidField) EmailAddress
validateEmailAddress input =
let result = validateNonEmpty input
in bimap (singleton <<< InvalidEmailAddress ) EmailAddress result
● Apply a single validation to the input
● Transform the primitive validation into a field validation
Applying Multiple Primitive Validations to a Field
validateEmailAddress
:: String
-> V (NonEmptyList InvalidField) EmailAddress
validateEmailAddress input =
let result = validateNonEmpty input *> validateEmailRegex input
in bimap (singleton <<< InvalidEmailAddress ) EmailAddress result
● Nearly identical to the previous function
● Performs two validations on the input
● Any and all errors from either validation will be collected
Exercise 3
Write a Function to Validate a Password
● Exercise URL: http://bit.ly/2GWa0Uh
● Solution must check for...
○ Non-empty input
○ Mixed case input
○ Valid length
● When you’re done, you should see the following output on the right hand side
Solution 3
Write a Function to Validate a Password
validatePassword
:: String
-> Int
-> Int
-> V (NonEmptyList InvalidField) Password
validatePassword input minLength maxLength =
let result =
validateNonEmpty input
*> validateContainsMixedCase input
*> (validateLength input minLength maxLength)
in bimap (singleton <<< InvalidPassword ) Password result
Validations on Forms
Representing a Form
● An unvalidated form is a record where all fields are primitive types
type UnvalidatedForm =
{ emailAddress :: String
, password :: String
}
● A validated form is a record where all fields are unique, opaque types
type ValidatedForm =
{ emailAddress :: EmailAddress
, password :: Password
}
Form Validation Type Signature
validateForm
:: UnvalidatedForm
-> V (NonEmptyList InvalidField) ValidatedForm
● Validates that a form contains only valid fields
● Returns either a validated form or a list of invalid field errors
Form Validation Function Implementation
validateForm { emailAddress, password } =
{ emailAddress: _, password: _ }
<$> (validateEmailAddress emailAddress)
<*> (validatePassword password 0 60)
● Destructure the unvalidated form input to get its fields
● Create an anonymous function to produce the validated record
● Partially apply the function to the email validation result with (<$>)
● Fully apply the function to the password validation result with (<*>)
Full Form Validation Function
validateForm
:: UnvalidatedForm
-> V (NonEmptyList InvalidField) ValidatedForm
validateForm { emailAddress, password } =
{ emailAddress: _, password: _ }
<$> (validateEmailAddress emailAddress)
<*> (validatePassword password 0 60)
● Validates a simple input form
Extending the Form Validation Function
validateForm { emailAddress, password, thirdField } =
{ emailAddress: _, password: _, thirdField: _ }
<$> (validateEmailAddress emailAddress)
<*> (validatePassword password 0 60)
<*> (validateThirdInput thirdField)
● Extending the validation is relatively straightforward
Exercise 4
Extend the Form Validator to Support an Additional Field
● Exercise URL: http://bit.ly/2Jcu2PK
● More free-form than previous exercises
● Details provided in block comments in the exercise
● Hints:
○ The names of the destructured record must match the actual record field names
○ Your newtype will need some instances, check the bottom of the module and copy
what was done for EmailAddress and Password
○ InvalidField’s Show instance will need to be updated for your new field error,
check the bottom of the module and copy what was done for
InvalidEmailAddress and InvalidPassword
Solution 4
Extend the Form Validator to Support an Additional Field
● Solution URL: http://bit.ly/2JcXogJ
Break
Validations on Branching Fields
(with Semiring and Alt)
Representing Branching Fields
● What if one of the fields could have multiple valid representations?
data UserId
= EmailAddress String
| Username String
● Represent the field with a sum type!
● UserIdis the new field type
● Can be constructed as either an email address or username
Representing Branching Errors
● Semigroupcan’t accumulate branching errors
● Semiringcan!
○ Uses (+) to represent associative operations (like (<>))
○ Uses (*) to represent distributive operations
● Let’s us model branching validations while still collecting all errors
Combining Validations with Apply
(and Semiring)
instance applyV :: Semiring err => Apply (V err) where
apply (Invalid err1) (Invalid err2) = Invalid (err1 * err2)
apply (Invalid err) _ = Invalid err
apply _ ( Invalid err) = Invalid err
apply (Valid f) (Valid x) = Valid (f x)
● Nearly identical to the Semigroupversion
○ err has a Semiringconstraint instead of a Semigroupconstraint
○ Uses (*) instead of (<>) for combining errors
Embedding Valid Results with Applicative
(and Semiring)
instance applicativeV :: Semiring err => Applicative (V err) where
pure = Valid
● Again, nearly identical to the Semigroupversion
Selecting Alternative Validations with Alt
instance altV :: Semiring err => Alt (V err) where
alt (Invalid err1) (Invalid err2) = Invalid (err1 + err2)
alt (Invalid _) a = a
alt (Valid a) _ = Valid a
● Alt let’s us provide alternative validations in a way that accumulates errors
● Note the Semiringconstraint on err
○ Uses (+) to collect errors, rather than (*) or (<>)
● Alt also defines an infix operator: (<|>)
Branching Email Address Validation Function
validateEmailAddress
:: String
-> V (Free InvalidField) UserId
validateEmailAddress input =
let result = validateNonEmpty input *> validateEmailRegex
input
in bimap (free <<< InvalidEmailAddress ) EmailAddress result
● Email validation that supports Alt and Semiring
Branching Username Validation Function
validateUsername
:: String -> Int -> Int
-> V (Free InvalidField) UserId
validateUsername input min max =
let result =
validateNonEmpty input
*> (validateLength input minLength maxLength)
in bimap (free <<< InvalidUsername ) UserName result
● Username validation that supports Alt and Semiring
Exercise 5
Extend the Form Validator to Support Branching Validation with Semiring and Alt
● Exercise URL: http://bit.ly/2kLTaiw
● Guided exercise (there’s a lot going on here, so we’ll go through it together)
○ Switch from Data.Validation.Semigroup to Data.Validation.Semiring
○ Update all validation functions to use Free and free
○ Create a UserId sum type with EmailAddress and Username constructors
○ Add an InvalidUsername constructor to the InvalidField type
○ Update validateEmailAddress to work with UserId
○ Write validateUsername function to validate a username
○ Use Control.Alt#(<|>) select between alternative validations
○ Update UnvalidatedForm and ValidatedForm with new input and output fields
Solution 5
Extend the Form Validator to Support Branching Validation with Semiring and Alt
● Solution URL: http://bit.ly/2xNcBRx
Summary
Further Reading
● My own “validation experiment”
○ https://github.com/jkachmar/purescript-validation-experiment
● purescript-polyform - validation toolkit using some of these concepts
○ https://github.com/paluh/purescript-polyform
● purescript-ocelot - component library using polyform
○ https://github.com/citizennet/purescript-ocelot
These Ideas Aren’t Specific to Validation!
Tuple <$> Just 1 <*> Just 2 == Just (Tuple 1 2)
Tuple <$> Just 1 <*> Nothing == Nothing
● Build an optional Tuplefrom optional components
Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com"
● Select the first successful response from concurrent computations
We’re Hiring!
Questions?

LC2018 - Input Validation in PureScript

  • 1.
    Input Validation inPureScript Simpler, Safer, Stronger Joe Kachmar LambdaConf 2018 4 Jun 2018
  • 2.
    Introduction ● Software Engineerat WeWork ○ Mostly writing Scala ○ Snuck a little PureScript into my last project ● Learned PureScript after Haskell ○ Finished Haskell Programming from First Principles ○ Mostly using JavaScript at work at the time ○ Wanted Haskell-style FP with JavaScript support (actual me) (GitHub me)
  • 3.
    Overview ● Show whatwe’re trying to avoid (a.k.a. Boolean Validation) ● Define errors that can be encountered while validating any field ● Write functions that perform these primitive validations ● Show how primitive validation functions can be combined ● Define types that represent valid fields ● Define field-specific errors in terms of primitive errors ● Write functions to perform field-specific validations ● Write a function that combines field-specific validations to validate a form
  • 4.
    Boolean Validation function validateForm({emailAddress, password }) { const isEmailValid = validateEmail(emailAddress); const isPasswordValid = validatePassword (password); return isEmailValid && isPasswordValid } ● validateEmailand validatePasswordreturn booleans ● Any validation failures will cause the whole function to return false ○ Doesn’t communicate which elements failed validation ○ Doesn’t communicate why failing elements were invalid
  • 5.
  • 6.
    The Type ofValidation data V err result = Invalid err | Valid result ● V - the validation type, parameterized by types describing errors and results ● Invalid- construct a validation error ● Valid- construct a successful validation result
  • 7.
    Validation Helper Functions ●purescript-validation doesn’t expose Validor Invaliddirectly ● invalidfunction used to signal validation errors ● validfunction used to signal validation success ● unV function used to unwrap the validation and handle errors or successes
  • 8.
    Representing Simple FormInput type UnvalidatedForm = { emailAddress :: String , password :: String } ● PureScript record representation of a simple form with two input fields ● Fields have very simple, “primitive” types
  • 9.
    Representing Primitive Errors ●Empty fields ● Fields with invalid email addresses ● Fields that are too short ● Fields that are too long ● Fields that only have uppercase characters ● Fields that only have lowercase characters data InvalidPrimitive = EmptyField | InvalidEmail String | TooShort Int Int | TooLong Int Int | NoLowercase String | NoUppercase String
  • 10.
    Primitive Validation TypeSignature validateMinimumLength :: String -> Int -> V (NonEmptyList InvalidPrimitive ) String ● Validates that the input string is greater than some given length ● Takes a input and minimum length arguments ● Returns either a list of primitive errors or a valid result
  • 11.
    Primitive Validation FunctionImplementation validateMinimumLength input min | (length input) < min = invalid (singleton ( TooShort (length input) min)) | otherwise = pure input ● Return an error if the input is too short ● Otherwise return the valid input
  • 12.
    Full Primitive ValidationFunction validateMinimumLength :: String -> Int -> V (NonEmptyList InvalidPrimitive ) String validateMinimumLength input min | (length input) < min = invalid (singleton ( TooShort (length input) min)) | otherwise = pure input ● Validates that the input string is greater than some given length
  • 13.
    Primitive Validation FunctionType Signature (Round 2) validateContainsLowercase :: String -> V (NonEmptyList InvalidPrimitive ) String ● Validates that the input string is greater than some given length ● Takes a String (the input to be validated) ● Returns either a non-empty list of invalid primitive errors, or the input string
  • 14.
    Primitive Validation FunctionImplementation (Round 2) validateContainsLowercase input | (toUpper input) == input = invalid (singleton ( NoLowercase input)) | otherwise = pure input ● Return an error if the input is equal to an uppercase version of itself ● Otherwise return the valid input
  • 15.
    validateContainsLowercase :: String -> V(NonEmptyList InvalidPrimitive ) String validateContainsLowercase input | (toUpper input) == input = invalid (singleton ( NoLowercase input)) | otherwise = pure input ● Validates that the input string contains at least one lowercase character Full Primitive Validation Function (Round 2)
  • 16.
    Exercise 1.1 Write aFunction to Validate that Input is Non-Empty ● Exercise URL: http://bit.ly/2sckTgl ● Hint: ○ Check out Data.String#null for testing string emptiness ● When you’re done, you should see the following output on the right hand side
  • 17.
    Solution 1.1 Write aFunction to Validate that Input is Non-Empty validateNonEmpty :: String -> V (NonEmptyList InvalidPrimitive ) String validateNonEmpty input | null input = invalid (singleton EmptyField) | otherwise = pure input
  • 18.
    Exercise 1.2 Write aFunction to Validate that Input Contains Uppercase Characters ● Exercise URL: http://bit.ly/2LyG6t9 ● Hints: ○ Think back to the validateContainsLowercase function from before ○ Check out Data.String#toLower for making lowercase strings ● When you’re done, you should see the following output on the right hand side
  • 19.
    Solution 1.2 Write aFunction to Validate that Input Contains Uppercase Characters validateContainsUppercase :: String -> V (NonEmptyList InvalidPrimitive ) String validateContainsUppercase input | (toLower input) == input = invalid (singleton ( NoUppercase input)) | otherwise = pure input
  • 20.
  • 21.
    Combining Validations withApply instance applyV :: Semigroup err => Apply (V err) where apply (Invalid err1) (Invalid err2) = Invalid (err1 <> err2) apply (Invalid err) _ = Invalid err apply _ ( Invalid err) = Invalid err apply (Valid f) (Valid x) = Valid (f x) ● Applylet’s us sequence validations together in a way that accumulates errors ● Note the Semigroupconstraint on err ● Applyalso defines an infix operator for the applyfunction: (<*>)
  • 22.
    Embedding Valid Resultswith Applicative instance applicativeV :: Semigroup err => Applicative (V err) where pure = Valid ● Applicativelet’s us wrap a valid result in V using pure ● Note the Semigroupconstraint on err
  • 23.
    Combining Primitive Validations validateLength ::String -> Int -> Int -> V (NonEmptyList InvalidPrimitive ) String validateLength input minLength maxLength = validateMinimumLength input minLength *> validateMaximumLength input maxLength ● *> is an infix operator we can use to sequence validations ● Applyguarantees that all errors will be collected and returned
  • 24.
    Exercise 2 Write aFunction to Validate that Input Contains Upper- and Lowercase Characters ● Exercise URL: http://bit.ly/2xeF6XT ● Hints: ○ Use primitive functions defined earlier ○ All primitive validation functions from earlier have been defined ● When you’re done, you should see the following output on the right hand side
  • 25.
    Solution 2 Write aFunction Validating that a Field Contains Uppercase Characters validateContainsMixedCase :: String -> V (NonEmptyList InvalidPrimitive ) String validateContainsMixedCase input = validateContainsLowercase input *> validateContainsUppercase input
  • 26.
  • 27.
  • 28.
    Representing Validated Fields newtypeEmailAddress = EmailAddress String newtype Password = Password String ● Validated fields are opaque wrappers around String ● One unique wrapper per unique field ● Creates a boundary between unvalidated input and validated output
  • 29.
    Representing Field Errors dataInvalidField = InvalidEmailAddress (NonEmptyList InvalidPrimitive ) | InvalidPassword (NonEmptyList InvalidPrimitive ) ● Each field must have a corresponding InvalidFieldconstructor ● Each constructor wraps a list of errors from that field’s validation
  • 30.
    Applying a SinglePrimitive Validation to a Field validateEmailAddress :: String -> V (NonEmptyList InvalidField) EmailAddress validateEmailAddress input = let result = validateNonEmpty input in bimap (singleton <<< InvalidEmailAddress ) EmailAddress result ● Apply a single validation to the input ● Transform the primitive validation into a field validation
  • 31.
    Applying Multiple PrimitiveValidations to a Field validateEmailAddress :: String -> V (NonEmptyList InvalidField) EmailAddress validateEmailAddress input = let result = validateNonEmpty input *> validateEmailRegex input in bimap (singleton <<< InvalidEmailAddress ) EmailAddress result ● Nearly identical to the previous function ● Performs two validations on the input ● Any and all errors from either validation will be collected
  • 32.
    Exercise 3 Write aFunction to Validate a Password ● Exercise URL: http://bit.ly/2GWa0Uh ● Solution must check for... ○ Non-empty input ○ Mixed case input ○ Valid length ● When you’re done, you should see the following output on the right hand side
  • 33.
    Solution 3 Write aFunction to Validate a Password validatePassword :: String -> Int -> Int -> V (NonEmptyList InvalidField) Password validatePassword input minLength maxLength = let result = validateNonEmpty input *> validateContainsMixedCase input *> (validateLength input minLength maxLength) in bimap (singleton <<< InvalidPassword ) Password result
  • 34.
  • 35.
    Representing a Form ●An unvalidated form is a record where all fields are primitive types type UnvalidatedForm = { emailAddress :: String , password :: String } ● A validated form is a record where all fields are unique, opaque types type ValidatedForm = { emailAddress :: EmailAddress , password :: Password }
  • 36.
    Form Validation TypeSignature validateForm :: UnvalidatedForm -> V (NonEmptyList InvalidField) ValidatedForm ● Validates that a form contains only valid fields ● Returns either a validated form or a list of invalid field errors
  • 37.
    Form Validation FunctionImplementation validateForm { emailAddress, password } = { emailAddress: _, password: _ } <$> (validateEmailAddress emailAddress) <*> (validatePassword password 0 60) ● Destructure the unvalidated form input to get its fields ● Create an anonymous function to produce the validated record ● Partially apply the function to the email validation result with (<$>) ● Fully apply the function to the password validation result with (<*>)
  • 38.
    Full Form ValidationFunction validateForm :: UnvalidatedForm -> V (NonEmptyList InvalidField) ValidatedForm validateForm { emailAddress, password } = { emailAddress: _, password: _ } <$> (validateEmailAddress emailAddress) <*> (validatePassword password 0 60) ● Validates a simple input form
  • 39.
    Extending the FormValidation Function validateForm { emailAddress, password, thirdField } = { emailAddress: _, password: _, thirdField: _ } <$> (validateEmailAddress emailAddress) <*> (validatePassword password 0 60) <*> (validateThirdInput thirdField) ● Extending the validation is relatively straightforward
  • 40.
    Exercise 4 Extend theForm Validator to Support an Additional Field ● Exercise URL: http://bit.ly/2Jcu2PK ● More free-form than previous exercises ● Details provided in block comments in the exercise ● Hints: ○ The names of the destructured record must match the actual record field names ○ Your newtype will need some instances, check the bottom of the module and copy what was done for EmailAddress and Password ○ InvalidField’s Show instance will need to be updated for your new field error, check the bottom of the module and copy what was done for InvalidEmailAddress and InvalidPassword
  • 41.
    Solution 4 Extend theForm Validator to Support an Additional Field ● Solution URL: http://bit.ly/2JcXogJ
  • 42.
  • 43.
    Validations on BranchingFields (with Semiring and Alt)
  • 44.
    Representing Branching Fields ●What if one of the fields could have multiple valid representations? data UserId = EmailAddress String | Username String ● Represent the field with a sum type! ● UserIdis the new field type ● Can be constructed as either an email address or username
  • 45.
    Representing Branching Errors ●Semigroupcan’t accumulate branching errors ● Semiringcan! ○ Uses (+) to represent associative operations (like (<>)) ○ Uses (*) to represent distributive operations ● Let’s us model branching validations while still collecting all errors
  • 46.
    Combining Validations withApply (and Semiring) instance applyV :: Semiring err => Apply (V err) where apply (Invalid err1) (Invalid err2) = Invalid (err1 * err2) apply (Invalid err) _ = Invalid err apply _ ( Invalid err) = Invalid err apply (Valid f) (Valid x) = Valid (f x) ● Nearly identical to the Semigroupversion ○ err has a Semiringconstraint instead of a Semigroupconstraint ○ Uses (*) instead of (<>) for combining errors
  • 47.
    Embedding Valid Resultswith Applicative (and Semiring) instance applicativeV :: Semiring err => Applicative (V err) where pure = Valid ● Again, nearly identical to the Semigroupversion
  • 48.
    Selecting Alternative Validationswith Alt instance altV :: Semiring err => Alt (V err) where alt (Invalid err1) (Invalid err2) = Invalid (err1 + err2) alt (Invalid _) a = a alt (Valid a) _ = Valid a ● Alt let’s us provide alternative validations in a way that accumulates errors ● Note the Semiringconstraint on err ○ Uses (+) to collect errors, rather than (*) or (<>) ● Alt also defines an infix operator: (<|>)
  • 49.
    Branching Email AddressValidation Function validateEmailAddress :: String -> V (Free InvalidField) UserId validateEmailAddress input = let result = validateNonEmpty input *> validateEmailRegex input in bimap (free <<< InvalidEmailAddress ) EmailAddress result ● Email validation that supports Alt and Semiring
  • 50.
    Branching Username ValidationFunction validateUsername :: String -> Int -> Int -> V (Free InvalidField) UserId validateUsername input min max = let result = validateNonEmpty input *> (validateLength input minLength maxLength) in bimap (free <<< InvalidUsername ) UserName result ● Username validation that supports Alt and Semiring
  • 51.
    Exercise 5 Extend theForm Validator to Support Branching Validation with Semiring and Alt ● Exercise URL: http://bit.ly/2kLTaiw ● Guided exercise (there’s a lot going on here, so we’ll go through it together) ○ Switch from Data.Validation.Semigroup to Data.Validation.Semiring ○ Update all validation functions to use Free and free ○ Create a UserId sum type with EmailAddress and Username constructors ○ Add an InvalidUsername constructor to the InvalidField type ○ Update validateEmailAddress to work with UserId ○ Write validateUsername function to validate a username ○ Use Control.Alt#(<|>) select between alternative validations ○ Update UnvalidatedForm and ValidatedForm with new input and output fields
  • 52.
    Solution 5 Extend theForm Validator to Support Branching Validation with Semiring and Alt ● Solution URL: http://bit.ly/2xNcBRx
  • 53.
  • 54.
    Further Reading ● Myown “validation experiment” ○ https://github.com/jkachmar/purescript-validation-experiment ● purescript-polyform - validation toolkit using some of these concepts ○ https://github.com/paluh/purescript-polyform ● purescript-ocelot - component library using polyform ○ https://github.com/citizennet/purescript-ocelot
  • 55.
    These Ideas Aren’tSpecific to Validation! Tuple <$> Just 1 <*> Just 2 == Just (Tuple 1 2) Tuple <$> Just 1 <*> Nothing == Nothing ● Build an optional Tuplefrom optional components Ajax.get "http://foo.com" <|> Ajax.get "http://bar.com" ● Select the first successful response from concurrent computations
  • 56.
  • 57.