Context: I\'m approaching Haskell from a standpoint of converting runtime errors to compile-time errors. My hypothesis is that this is possible if one can c
Phantom types to the rescue! is Bryan O'Sullivan example of implementing read-only vs read/write access at the type level using phantom types.
Similarly, for your use-case:
data Unknown -- unknown users
data Authenticated -- verified users
newtype User a i = Id i deriving Show
It is important that the data constructor Id
is not exposed to user, but the module provides functions to initialize and authenticate users:
-- initializes an un-authenticated user
newUser :: i -> User Unknown i
newUser = Id
-- authenticates a user
authUser :: (User a i) -> User Authenticated i
authUser (Id i) = Id i -- dummy implementation
then, you may control access at the type level without code duplication, without run-time checks and without run-time cost:
-- open to all users
getId :: User a i -> i
getId (Id i) = i
-- only authenticated users can pass through
getId' :: User Authenticated i -> i
getId' (Id i) = i
For example, if
\> let jim = newUser "jim"
\> let joe = authUser $ newUser "joe"
joe
is an authenticated user and can be passed to either function:
\> getId joe
"joe"
\> getId' joe
"joe"
whereas, you will get compile-time error if you call getId'
with jim
:
\> getId jim
"jim"
\> getId' jim -- compile-time error! not run-time error!
<interactive>:28:8:
Couldn't match type ‘Unknown’ with ‘Authenticated’
Expected type: User Authenticated [Char]
Actual type: User Unknown [Char]
In the first argument of ‘getId'’, namely ‘jim’
In the expression: getId' jim