Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type-safe named fields in haskell ADTs

Tags:

haskell

There's a very common problem that is very easy to get into with haskell. Code-snippet that describes it is this:

data JobDescription = JobOne { n :: Int }
                    | JobTwo
                    | JobThree { n :: Int }
  deriving (Show, Eq)

taskOneWorker :: JobDescription -> IO ()
taskOneWorker t = do
    putStrLn $ "n: " ++ (show $ n t)

main :: IO ()
main = do
  -- this runs ok:
  taskOneWorker (JobOne 10)

  -- this fails at runtime:
  -- taskOneWorker JobTwo

  -- this works, but we didn't want it to:
  -- taskOneWorker (JobThree 10)

I described this problem and it's possible solutions in this article: https://www.fpcomplete.com/user/k_bx/playing-with-datakinds

Here, on StackOverflow, I'd like to ask you -- what are the best solutions to this problem, using which tools and what documentation?

like image 798
Konstantine Rybnikov Avatar asked Oct 18 '14 12:10

Konstantine Rybnikov


1 Answers

I suggest using a Prism. The Prism _JobOne represents the Int value that lives inside the JobOne constructor. In taskOneWorker we look it up using ^?. Either the value t is indeed a JobOne constructor and its argument is return in the Just, or it is not a JobOne constructor and we get back a Nothing.

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data JobDescription = JobOne { n :: Int }
                    | JobTwo
                    | JobThree { n :: Int }
  deriving (Show, Eq)
$(makePrisms ''JobDescription)

taskOneWorker :: JobDescription -> IO ()
taskOneWorker t = do
  case t ^? _JobOne of
    Just n  -> putStrLn $ "n: " ++ show n
    Nothing -> putStrLn "Not a JobOne"
               -- Or 'return ()' if you want to ignore these cases

main :: IO ()
main = do
  taskOneWorker (JobOne 10)
  taskOneWorker JobTwo
  taskOneWorker (JobThree 10)

-- *Main> main
-- n: 10
-- Not a JobOne
-- Not a JobOne
like image 62
Tom Ellis Avatar answered Jan 01 '23 21:01

Tom Ellis