Last active
November 28, 2018 09:01
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# 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