Skip to content

Instantly share code, notes, and snippets.

@Elvecent
Last active November 28, 2018 09:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Elvecent/0bf2bca881a12331b8731fb871839b9c to your computer and use it in GitHub Desktop.
Save Elvecent/0bf2bca881a12331b8731fb871839b9c to your computer and use it in GitHub Desktop.
An example on typesafe field validation in Haskell with basic type-level hackery.
{-# Language TypeSynonymInstances
, FlexibleInstances
, MultiParamTypeClasses
, KindSignatures
, GADTs
, DataKinds
, TypeFamilies
, AllowAmbiguousTypes
, TypeApplications
#-}
module Main where
import Data.Functor.Identity
data Valid a = Valid a -- constructor should be hidden
newtype TaskName = TaskName String
deriving Show
newtype TaskDesc = TaskDesc String
deriving Show
data TaskState = NotStarted | InProgress | Finished
deriving Show
data TaskV
(name :: Bool)
(desc :: Bool)
(state :: Bool) where
NewTask :: TaskV False False False
NameTask :: Valid TaskName -> TaskV a b c -> TaskV True b c
DescTask :: Valid TaskDesc -> TaskV a b c -> TaskV a True c
StateTask :: Valid TaskState -> TaskV a b c -> TaskV a b True
FullTask :: Valid TaskName ->
Valid TaskDesc ->
Valid TaskState ->
TaskV True True True
data Task = Task TaskName TaskDesc TaskState
deriving Show
-- this whole function never fails thanks to typesafety
mkTask :: TaskV True True True -> Task
mkTask (FullTask
(Valid name)
(Valid desc)
(Valid state)) = Task name desc state
mkTask t = mkTask $ conflateTask t
where
conflateTask :: TaskV a b c -> TaskV True True True
conflateTask t = case t of
-- just so it typechecks
NewTask -> FullTask undefined undefined undefined
NameTask name t' -> case (conflateTask t') of
(FullTask a b c) -> FullTask name b c
DescTask desc t' -> case (conflateTask t') of
(FullTask a b c) -> FullTask a desc c
StateTask state t' -> case (conflateTask t') of
(FullTask a b c) -> FullTask a b state
type family HKD (f :: * -> *) (a :: *) where
HKD Identity a = a
HKD f a = f a
class Validatable f a err where
validate :: HKD f a -> Either err (Valid a)
instance Validatable Identity TaskName String where
validate tn@(TaskName n) = if (length n > 15)
then Left "Too long for a task name"
else Right $ Valid tn
instance Validatable Identity TaskDesc String where
validate (TaskDesc "") = Left "Task description can't be empty"
validate x = Right $ Valid x
instance Validatable Maybe TaskState String where
validate Nothing = Left "Invalid task state"
validate (Just x) = Right $ Valid x
parseState :: String -> Maybe TaskState
parseState "not started" = Just NotStarted
parseState "in progress" = Just InProgress
parseState "finished" = Just Finished
parseState _ = Nothing
triMap :: (a -> a', b -> b', c -> c') ->
(a, b, c) -> (a', b', c')
triMap (f, g, h) (x, y, z) = (f x, g y, h z)
triSequence :: Applicative m => (m a, m b, m c) -> m (a, b, c)
triSequence (ma, mb, mc) = (,,) <$> ma <*> mb <*> mc
task :: Either String Task
task = do
(n, d, s) <- triSequence $ triMap
( validate @Identity
, validate @Identity
, validate @Maybe)
( TaskName "name" -- try "very long task name"
, TaskDesc "description" -- try ""
, parseState "not started") -- try "wtf task state"
return $
mkTask $
NameTask n $
DescTask d $ -- try removing either of those to get a type error!
StateTask s $
NewTask
main :: IO ()
main = print task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment