Designing a library is a conversation between the library author and the users where perhaps the hardest and most important question is "is this the right thing to offer?".
It's my belief that types are a powerful language to carry out this conversation and thus the nature of types illustrate a path to having this conversation go smoothly.
This talk investigates this idea via a simple real world example: getting configuration data from the environment. It also tells a parallel story of how the structure of types illustrates tradeoffs in this design space. This power arises from the historical roots of type systems in logic by stealing a powerful concept from Wittgenstein, Gentzen, and Dummett: Logical Harmony.
3. $ cd company-app
$ ./app
*** Exception: CLIENT_ID: getEnv: does not exist (no
environment variable)
$ export CLIENT_ID=b114b7e51f0e6810efeacf80132670
$ ./app
*** Exception: CLIENT_SECRET: getEnv: does not exist
(no environment variable)
$ export CLIENT_SECRET=0400eb02db68230048010d39f63fef08
$ ./app
[log] Starting server
.
.
.
*** Exception: NETWORK_ID: getEnv: does not exist (no
environment variable)
$ ahoiyinyasetinoasyet
bash: ahoiyinyasetinoasyet: command not found
$ rm -rf /
9. A type E a
A “computation” which reads from the
environment to produce a value of type a.
An API get :: String -> E String
Get a single key from the environment.
run :: E a -> IO a
Run the E a “computation” in IO to receive the
promised a value.
10. Instances instance Functor E
With this we can interpret Strings from the
environment as more sophisticated types
instance Monad E
With this we can compose calls to get together to
build larger types like Config
12. Instances instance Functor E
With this we can interpret Strings from the
environment as more sophisticated types
instance Monad E
With this we can compose calls to get together to
build larger types like Config
instance Applicative E
… well, Monad implies this so we might as well
throw it in there, too
13. data Config =
Config
{ clientId :: String
, clientSecret :: String
, networkId :: String
} deriving ( Eq, Show )
getConfig :: E Config
getConfig = do
id <- get “CLIENT_ID”
secret <- get “CLIENT_SECRET”
netid <- get “NETWORK_ID”
return (Config id secret netid)
14. main :: IO ()
main = do
config <- run getConfig
server <- Server.start config
{- ... -}
Server.bind config
20. -- the data type
data Nat = Zero | Wind Nat
-- the introductory side of the API
zero :: Nat
wind :: Nat -> Nat
-- the elimination side of the API
unwind :: (r -> r, r) -> (Nat -> r)
unwind (wind, zero) n = {- ... -}
21. Things fit together
—- if we let (r ——> Nat)
unwind (wind, zero) :: Nat -> Nat
unwind (wind, zero) == id
31. A type E a
A “computation” which reads from the
environment to produce a value of type a.
An API get :: String -> E String
Get a single key from the environment.
run :: E a -> IO a
Run the E a “computation” in IO to receive the
promised a value.
34. My theory
As we discover the “right” API for a library we move
from offering unknown types to concrete ones and must
gradually move to respect logical harmony
35. A corollary
If we offer too much, too quickly on the side of elims or
intros then this lack of balance will eventually be felt
37. $ cd company-app
$ ./app
*** Exception: Environment requires following vars:
CLIENT_ID, CLIENT_SECRET, NETWORK_ID, TIMEOUT.
$ ./app --help
Some Company, Inc THE APP
Usage: app
Environment:
CLIENT_ID: id for authentication handshake
CLIENT_SECRET: secret for authentication handshake
NETWORK_ID: name for server to listen on
TIMEOUT: milliseconds to wait
$ :)
bash: syntax error near unexpected token `)'
38. A type E a
A “computation” which reads from the
environment to produce a value of type a.
An API get :: String -> E String
Get a single key from the environment.
run :: E a -> IO a
Run the E a “computation” in IO to receive the
promised a value.
examine :: E a -> [String]
Extract from the computation all variables that
will be accessed when run.
39. Instances instance Functor E
With this we can interpret Strings from the
environment as more sophisticated types
instance Monad E
With this we can compose calls to get together to
build larger types like Config
instance Applicative E
… well, Monad implies this so we might as well
throw it in there, too
40.
41. data E a =
E
{ examine :: [String]
, run :: IO a
}
42. data E a =
E
{ examine :: [String]
, run :: IO a
}
deriving instance Functor E
get :: String -> E String
get name = E { examine = [name], run = getEnv name }
43. data E a =
E
{ examine :: [String]
, run :: IO a
}
instance Applicative E where
pure a = E { examine = [], run = return a }
E ns1 io1 <*> ns2 io2 =
E (ns1 ++ ns2) (io1 <*> io2)
54. data E a =
E
{ examine :: [String]
, run :: IO a
} derving Functor
instance Applicative (E a) where
pure a = E [] (pure a)
E ns1 io1 <*> E ns2 io2 =
E (ns1 <> ns2) (io1 <*> io2)
get name = E [name] (getEnv name)
55. data Config =
Config
{ clientId :: String
, clientSecret :: String
, networkId :: String
, timeout :: Int
} deriving ( Eq, Show )
getConfig :: E Config
getConfig =
Config
<$> get “CLIENT_ID”
<*> get “CLIENT_SECRET”
<*> get “NETWORK_ID”
<*> fmap read (get “TIMEOUT”)