of harmony and stinginess: applicative, monad, and iterative library design

58
Of Harmony and Stinginess Applicatives, Monads, and incremental library design Joseph Tel Abrahamson @sdbo / tel / jspha July 2015

Upload: jspha

Post on 14-Aug-2015

117 views

Category:

Software


0 download

TRANSCRIPT

Of Harmony and StinginessApplicatives, Monads, and incremental library design

Joseph Tel Abrahamson @sdbo / tel / jspha

July 2015

A story

First Iteration

$ 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 /

We have got to be able to do better, right?

Transparent abstraction, to draw apart and disentangle

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 library

First Iteration

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

Applicative Monad<=

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

Is this the right design to offer?

Let’s talk about types

zero :: Nat

succ :: Nat -> Nat

A type

An operation

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 = {- ... -}

data Nat = Zero | Wind Nat

-- 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

Logical harmony

Gentzen Wittgenstein Dummett

Intros Elims

{Intros, elims, data decls} fit together

Harmony

{Intros, elims, data decls} of known data types fit together

Let’s talk about unknown types

Lets talk about libraries

Second Iteration

Library design is often API-first

concrete = we know everything

abstract = some parts are unknown

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.

get run

E a

Is this the right design to offer?

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

Another story

Second Iteration

$ 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 }

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)

-- No monad.

E a ~ (Const [String] a, IO a)

(>>=) :: Const e a -> (a -> Const e b) -> Const e b

Const e >>= _ = Const e

Applicative Monad<=

Applicative T Monad T<=

Given a type T

Applicative Monad<

Monad run

examine

A library

Third Iteration

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”)

Harmony

Thank you

“Of Harmony and Stinginess”Joseph Tel Abrahamson @sdbo / tel / jspha.com

Questions?