Saurabh Nanda
Saurabh Nanda

Reputation: 6793

Does Haskell have "variables"? Or, easiest way to read configuration data?

I have written my medium-sized Haskell app with hard-coded config variables (like Google OAuth ClientId & ClientSecret). Now that I'm prepping the app for a production deployment, I need to move all these config variable out of the source, to either: (a) environment variables, or (b) a plain-text config file.

Here's what the code, currently, look likes:

googleClientId :: T.Text
googleClientId = "redacted"

googleClientSecret :: T.Text
googleClientSecret = "redacted"

generateOAuthUserCode :: IO (OAuthCodeResponse)
generateOAuthUserCode = do
  r <- asJSON =<< post "https://accounts.google.com/o/oauth2/device/code" ["client_id" := googleClientId, "scope" := ("email profile" :: T.Text)]
  return $ r ^. responseBody

What's the fastest/easiest way to get googleClientId and googleClientSecret from an environment variable (or config file)? I tried the following:

googleClientId :: T.Text
googleClientId = undefined

googleClientSecret :: T.Text
googleClientSecret = undefined

main :: IO()
main = do 
  googleClientId <- getEnv "GOOGLE_CLIENT_ID"
  googleClientSecret <- getENV "GOOGLE_CLIENT_SECRET"
  -- Start the main app, which internally will call generateOAuthUserCode at some point.

The expectation was that the global googleClientId and googleClientSecret will be re-bound, but my editor immediately started showing a warning that "the binding shadows an existing binding", indicating that Haskell is creating a new binding, instead of changing the existing one.

So, two questions here:

  1. First, the pragmatic one. How to solve the problem at hand, without getting into the Reader monad, which might involve changing a lot of function signatures across my app.
  2. Second, the one oriented to learning. Haskell has immutable values, which is understood and appreciated. Does it even have immutable variable binding? Is it not possible to get dynamic variable bindings, like in Common Lisp?

Edit: What about the following approach?

what about the following approach?

outerFunc :: String -> String -> IO ()
outerFunc googleClientId googleClientSecret = do
  -- more code comes here

  where
    generateOAuthUserCode :: IO (OAuthCodeResponse)
    generateOAuthUserCode = do
      r <- asJSON =<< post "https://accounts.google.com/o/oauth2/device/code" ["client_id" := googleClientId, "scope" := ("email profile" :: T.Text)]
      return $ r ^. responseBody

    -- more functions depending upon the config variables

Upvotes: 2

Views: 483

Answers (1)

stholzm
stholzm

Reputation: 3455

I assumed that you rely on global variables like googleClientId often in your codebase.

You might want to try and at least estimate the cost of the Reader approach before you go the "technical debt" route. Global variables are a bad practice anyways, @Carsten has proposed an alternative refactoring. But since you are asking for pragmatic help...

Question 1: The fastest/easiest way is to use the frowned-upon unsafePerformIO. Like this:

googleClientId = unsafePerformIO $ getEnv "GOOGLE_CLIENT_ID"
main = putStrLn googleClientId

This basically lets you ignore the safety of IO and puts the desired value into a global variable as if it was a plain string. Please note that getEnv crashes if the environment variable does not exist.

Question 2: One cannot "update" variables in Haskell. If one creates another variable with the same name in a nested scope, that binding will shadow the outer one in the inner scope, leaving the outer binding intact. That is somewhat confusing, hence the warning.

Upvotes: 4

Related Questions