Why types matter

@volpegabriel87

@gvolpe

Haskell edition

A motivating example

@volpegabriel87

@gvolpe

showName :: String -> String -> String -> String
showName username name email = 
  "Hi " <> name <> "! " 
 <> "Your username is " <> username 
 <> " and your email is " <> email  
main :: IO ()
main = putStrLn $ 
  showName "gvolpe@github.com" "12345" "foo"

Dealing with Strings

A motivating example

@volpegabriel87

@gvolpe

Let's do better!

@volpegabriel87

@gvolpe

newtype Username = Username String
newtype Name = Name String
newtype Email = Email String
main :: IO ()
main = putStrLn $ showName' u n e
 where
  u = Username "gvolpe@github.com"
  n = Name "12345"
  e = Email ""
showName' :: Username -> Name -> Email -> String
showName' (Username u) (Name n) (Email e) =
  "Hi " <> n <> "! " <> "Your username is " 
  <> u <> " and your email is " <> e

Newtypes

Let's do better!

@volpegabriel87

@gvolpe

mkUsername :: String -> Maybe Username
mkUsername [] = Nothing
mkUsername u  = Just (Username u)

mkName :: String -> Maybe Name
mkName [] = Nothing
mkName n  = Just (Name n)

-- Let's pretend we validate it properly
mkEmail :: String -> Maybe Email
mkEmail e = 
  if '@' `elem` e then Just (Email e) else Nothing
main :: IO ()
main = putStrLn $ showName' u n e
 where
  u = fromMaybe (error "Invalid username") (mkUsername "g")
  n = fromMaybe (error "Invalid name") (mkName "G")
  e = fromMaybe (error "Invalid email") (mkEmail "123")

Smart Constructors

Let's do better!

@volpegabriel87

@gvolpe

λ main
types-matter: Invalid email
CallStack (from HasCallStack):
  error, called at app/Main.hs:20:25 in main:Main

Smart Constructors

RUNTIME VALIDATION

Refinement Types

@volpegabriel87

@gvolpe

{-# LANGUAGE DataKinds, FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances #-}

import Refined

data EmailPred

instance Predicate EmailPred String where
  validate p value = unless ('@' `elem` value)
    $ throwRefineOtherException (typeOf p) "Invalid email"

type Username' = Refined NonEmpty String
type Name' = Refined NonEmpty String
type Email' = Refined EmailPred String

Because we can do better

Refinement Types

@volpegabriel87

@gvolpe

{-# LANGUAGE TemplateHaskell #-}

main :: IO ()
main = putStrLn $ showNameRefined u n e
 where 
  u = $$(refineTH "gvolpe") :: Username'
  n = $$(refineTH "Gabriel") :: Name'
  e = $$(refineTH "123#abc") :: Email'

Because we can do better

showNameRefined :: Username' -> Name' -> Email' -> String
showNameRefined username name email =
  "Hi " <> unrefine name <> "! "
    <> "Your username is " <> unrefine username
    <> " and your email is " <> unrefine email

Refinement Types

@volpegabriel87

@gvolpe

COMPILE TIME VALIDATION

λ app/Main.hs:23:18: error:
    * The predicate (EmailPred) does not hold: 
  Invalid email
    * In the Template Haskell splice $$(refineTH "123#abc")
      In the expression: $$(refineTH "123#abc") :: Email'
      In an equation for email':
          email' = $$(refineTH "123#abc") :: Email'
   |
23 |   email'    = $$(refineTH "123#abc") :: Email'
   |                  ^^^^^^^^^^^^^^^^^^

Refinement Types

@volpegabriel87

@gvolpe

Refine functions

Another example

@volpegabriel87

@gvolpe

newtype HttpHost = HttpHost Text 
newtype HttpPort = HttpPort Int 
newtype HttpUri = HttpUri Text 

mkHttpHost :: Text -> Maybe HttpHost
mkHttpHost (null -> True) = Nothing
mkHttpHost h              = Just (HttpHost h)

-- Validation happens at runtime
mkHttpPort :: Int -> Maybe HttpPort
mkHttpPort n = 
  if n >= 1024 && n <= 49151 
  then Just (HttpPort n)
  else Nothing

-- We assume the inputs are validated
mkUri :: HttpHost -> HttpPort -> HttpUri
mkUri (HttpHost h) (HttpPort p) = 
  HttpUri (h <> ":" <> pack (show p))

Validated Http URI

Another example

@volpegabriel87

@gvolpe

main :: IO ()
main = print $ mkUri h p
 where
  h = fromMaybe (error "Invalid host") (mkHttpHost "127.0.0.1")
  p = fromMaybe (error "Invaild port") (mkHttpPort 123)

Validated Http URI

λ main
types-matter: Invaild port
CallStack (from HasCallStack):
  error, called at app/Main.hs:31:22 in main:Main

RUNTIME VALIDATION

Refinement Types

@volpegabriel87

@gvolpe

type HttpHost' = Refined NonEmpty Text
type HttpPort' = Refined (FromTo 1024 49151) Int

mkUri' :: HttpHost' -> HttpPort' -> HttpUri
mkUri' host port =
 HttpUri (unrefine host <> ":" <> pack (show $ unrefine port))

Validated Http URI

{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}

main :: IO ()
main = print $ mkUri h p
 where
  h = $$(refineTH "localhost") :: HttpHost'
  p = $$(refineTH 123) :: HttpPort'

Refinement Types

@volpegabriel87

@gvolpe

Validated Http URI

λ app/Main.hs:33:14: error:
    * The predicate (FromTo 1024 49151) does not hold: 
  Value is out of range (minimum: 1024, maximum: 49151)
    * In the Template Haskell splice $$(refineTH 123)
      In the expression: $$(refineTH 123) :: HttpPort'
      In an equation for port': port' = $$(refineTH 123) :: HttpPort'
   |
33 |   port' = $$(refineTH 123) :: HttpPort'
   |              ^^^^^^^^^^^^

COMPILE TIME VALIDATION

Refinement Types

@volpegabriel87

@gvolpe

  • Logical Predicates
    • Not, And, Or
  • Numeric Predicates
    • LessThan
    • GreaterThan
    • EqualTo
    • From, To, FromTo
    • Positive, Negative
  • Foldable Predicates
    • SizeEqualTo
    • NonEmpty

Refinement Types

@volpegabriel87

@gvolpe

{-# LANGUAGE DataKinds, FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances #-}

import Refined

data LowerCase

instance Predicate LowerCase String where
  validate p value = unless (all isLower value)
    $ throwRefineOtherException (typeOf p) "Not all chars are lowercase"

type LowerCaseString = Refined LowerCase String

Custom Predicates

Performance

@volpegabriel87

@gvolpe

The unrefine function bears zero overhead, since it mearly unwraps newtype. The refineTH function bears zero runtime overhead as well, since it simply packs a value into newtype.

Quoting Nikita Volkov (author):

Dziękuję bardzo!

@volpegabriel87

@gvolpe

Why types matter - FT 2020

By Gabriel Volpe

Why types matter - FT 2020

A type-safe world

  • 1,850