An exploration of how the Applicative and Alternative typeclasses in PureScript can help define well-typed validation functions over records of form input.
1. Input Validation in PureScript
Simpler, Safer, Stronger
Joe Kachmar
LambdaConf 2018
4 Jun 2018
2. 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)
3. 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
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
6. 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
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 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
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 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
11. 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
12. 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
13. 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
14. 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
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 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
17. 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
18. 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
19. 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
21. 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: (<*>)
22. 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
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 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
25. Solution 2
Write a Function Validating that a Field Contains Uppercase Characters
validateContainsMixedCase
:: String
-> V (NonEmptyList InvalidPrimitive ) String
validateContainsMixedCase input =
validateContainsLowercase input
*> validateContainsUppercase input
28. 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
29. 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
30. 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
31. 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
32. 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
33. 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
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 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
37. 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 (<*>)
38. 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
39. 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
40. 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
41. Solution 4
Extend the Form Validator to Support an Additional Field
● Solution URL: http://bit.ly/2JcXogJ
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 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
47. Embedding Valid Results with Applicative
(and Semiring)
instance applicativeV :: Semiring err => Applicative (V err) where
pure = Valid
● Again, nearly identical to the Semigroupversion
48. 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: (<|>)
49. 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
50. 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
51. 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
52. Solution 5
Extend the Form Validator to Support Branching Validation with Semiring and Alt
● Solution URL: http://bit.ly/2xNcBRx
54. 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
55. 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