Functional Data Validation

ABOUT ME

Functional Data Validation

  1. Simple validation: Maybe Monad
  2. A bit more descriptive: Either Monad
  3. Validation with full report of errors

Everybody needs to validate something. From the content of a file to a web form.

SIMPLE VALIDATION

MAYBE MONAD

data Address =
  Address {
    getHouseNumber :: Int,
    getStreetName :: String
  } deriving (Eq, Show)
  • House Number > 0
  • Street Name not empty
makeAddress :: Int -> String -> Maybe Address
data Maybe a = Nothing | Just a
makeAddress x y
  | x <= 0 || y == "" = Nothing
  | otherwise         = Just $ Address x y

SIMPLE VALIDATION

MAYBE MONAD

data Person =
  Person {
    getName :: String,
    getAddress :: Address
  } deriving (Eq, Show)
  • Name not empty
makePerson :: String -> Maybe Address -> Maybe Person
makePerson n address
  | n == ""   = Nothing
  | otherwise = address >>= \a -> Just $ Person n a

A BIT MORE DESCRIPTIVE

Either MONAD

data Address =
  Address {
    getHouseNumber :: Int,
    getStreetName :: String
  } deriving (Eq, Show)
  • House Number > 0
  • Street Name not empty
makeAddress :: Int -> String -> Either Error Address
data Either a b = Left a | Right b

A BIT MORE DESCRIPTIVE

Either MONAD

makeAddress :: Int -> String -> Either Error Address
streetNumberGreaterThanZero :: Int -> Either Error Int
streetNumberGreaterThanZero x
  | x <= 0    = Left "Number must be greater than zero!"
  | otherwise = Right x
notEmpty :: String -> Error -> Either Error String
notEmpty x err
  | x == ""   = Left err
  | otherwise = Right x

streetNameNotEmpty :: String -> Either Error String
streetNameNotEmpty x = notEmpty x "Street name must not be empty!"
makeAddress x y = do
  number <- streetNumberGreaterThanZero x
  name   <- streetNameNotEmpty y
  Right $ Address number name

A BIT MORE DESCRIPTIVE

Either MONAD

data Person =
  Person {
    getName :: String,
    getAddress :: Address
  } deriving (Eq, Show)
  • Name not empty
makePerson :: String -> Either Error Address -> Either Error Person
nameNotEmpty :: String -> Either Error String
nameNotEmpty x = notEmpty x "Name must not be empty!"
makePerson n a = do
  name    <- nameNotEmpty n
  address <- a
  Right $ Person name address
makePerson' :: String -> Either Error Address -> Either Error Person
makePerson' n a =
  nameNotEmpty n >>= \name ->
  (Person name)  <$> a

FULL REPORT OF ERRORS

THE IMPERATIVE WAY

def makeAddress(number: Int, name: String): Address = {
  if (number <= 0) {
    throw new Exception("Street number must be greater than zero!")
  } else if (name.isEmpty) { 
    throw new Exception("Street name must not be empty!")
  } else if (name.length > 10) {
    throw new Exception("Street name must not have more than 10 characters!")
  } else {
    Address(number, name) 
  }
}
def makePerson(name: String, address: Address): Person = {
  if (name.isEmpty) {
    throw new Exception("Name must not be empty!")
  } else if (address == null) {
    throw new IllegalArgumentException()
  } else {
    Person(name, address)
  }
}
try {
  val address = makeAddress(0, "")
  val person  = makePerson("Gabi", address)
  println(person)
} catch {
  case e: Exception => println(e.getMessage)
}

FULL REPORT OF ERRORS

THE OOP WAY: NOtification Pattern

private def validateAddress(houseNumber: Int, streetName: String, notification: Notification) = {
  if (houseNumber <= 0)
    notification.addError("Street number must be greater than zero!")
  if (streetName.isEmpty)
    notification.addError("Street name must not be empty!")
  if (streetName.length > 10)
    notification.addError("Street name must not have more than 10 characters!")
}
private def validatePerson(personName: String, notification: Notification) = {
  if (personName.isEmpty)
    notification.addError("Name must not be empty!")
}
val notification = new Notification()

validateAddress(houseNumber, streetName, notification)
validatePerson(personName, notification)

val address = Address(houseNumber, streetName)
val person  = Person(personName, address)

if (notification.hasErrors) println(notification.reportErrors)
else println(person)

FULL REPORT OF ERRORS

Validation Applicative

data Address =
  Address {
    getHouseNumber :: Int,
    getStreetName :: String
  } deriving (Eq, Show)
  • House Number > 0
  • Street Name not empty
type Result a = Validation [Error] a

makeAddress :: Int -> String -> Result Address
data Validation err a = Failure err | Success a

FULL REPORT OF ERRORS

Validation Applicative

numberGreaterThanZero :: Int -> Result Int
numberGreaterThanZero x
  | x > 0     = Success x
  | otherwise = Failure ["Number must be greater than zero!"]
notEmpty :: String -> Error -> Result String
notEmpty x m
  | x == ""   = Failure [m]
  | otherwise = Success x

streetNameNotEmpty :: String -> Result String
streetNameNotEmpty x = notEmpty x "Street name must not be empty!"
data Validation err a = Failure err | Success a

type Result a = Validation [Error] a

FULL REPORT OF ERRORS

Validation Applicative

instance Functor (Validation a) where
  fmap _ (Failure e) = Failure e
  fmap f (Success x) = Success $ f x
makeAddress :: Int -> String -> Result Address
makeAddress x y = Address <$> numberGreaterThanZero x <*> streetNameNotEmpty y
import Data.Semigroup

instance Semigroup String where
  x <> y = x ++ y
instance Semigroup err => Applicative (Validation err) where
  pure = Success
  (Success f) <*> (Failure e) = Failure e
  (Failure e) <*> (Success x) = Failure e
  (Failure e) <*> (Failure t) = Failure $ e <> t
  (Success f) <*> (Success x) = Success $ f x

FULL REPORT OF ERRORS

Validation Applicative

data Address =
  Address {
    getHouseNumber :: Int,
    getStreetName :: String
  } deriving (Eq, Show)
  • Street Name length <= 10
streetNameLength :: String -> Result String
streetNameLength x
  | length x > 10 = Failure ["Name must not exceed 10 characters!"]
  | otherwise     = Success x

FULL REPORT OF ERRORS

Validation Applicative

instance Semigroup err => Semigroup (Validation err a) where
  (Success x) <> (Success y) = Success y
  (Failure x) <> (Failure y) = Failure (x <> y)
  _           <> (Failure y) = Failure y
  (Failure x) <> _           = Failure x
streetNameValidation :: String -> Result String
streetNameValidation x = streetNameNotEmpty x <> streetNameLength x
makeAddress' :: Int -> String -> Result Address
makeAddress' x y = Address <$> numberGreaterThanZero x <*> streetNameValidation y

FULL REPORT OF ERRORS

Validation Applicative

data Person =
  Person {
    getName :: String,
    getAddress :: Address
  } deriving (Eq, Show)
  • Name not empty
makePerson :: String -> Result Address -> Result Person
nameNotEmpty :: String -> Result String
nameNotEmpty x = notEmpty x "Name must not be empty!"
makePerson n a = Person <$> (nameNotEmpty n) <*> a

FULL REPORT OF ERRORS

Validation Applicative

Summarising

  • Validation type
  • Semigroup instance
  • Functor instance
  • Applicative instance
  • Some rules

Summarising

  • Validation type
  • Semigroup instance
  • Functor instance
  • Applicative instance
  • Some rules

QUESTIONS?

Functional Data Validation

DANKJEWEL!!!

Functional Data Validation

By Gabriel Volpe

Functional Data Validation

Functional Data Validation in Haskell

  • 2,884