of harmony and stinginess: applicative, monad, and iterative library design
TRANSCRIPT
Of Harmony and StinginessApplicatives, Monads, and incremental library design
Joseph Tel Abrahamson @sdbo / tel / jspha
July 2015
$ 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 /
data Config = Config { clientId :: String , clientSecret :: String , networkId :: String } deriving ( Eq, Show )
getConfig :: IO Config getConfig = do id <- getEnv “CLIENT_ID” secret <- getEnv “CLIENT_SECRET” netid <- getEnv “NETWORK_ID” return (Config id secret netid)
main :: IO () main = do config <- getConfig server <- Server.start config
{- ... -}
Server.bind config
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.
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
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
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)
main :: IO () main = do config <- run getConfig server <- Server.start config
{- ... -}
Server.bind config
unwind :: Nat -> (r -> r) -> (r -> r)
:: (r -> r) -> r -> (Nat -> r) :: (r -> r, r) -> (Nat -> r)
unwind :: (r -> r, r) -> (Nat -> r) unwind (succ, zero) n = {- ... -}
-- 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 = {- ... -}
Things fit together
—- if we let (r ——> Nat)
unwind (wind, zero) :: Nat -> Nat unwind (wind, zero) == id
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.
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
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
$ 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 `)'
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.
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
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 }
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)
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)
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”)