Idiomatic way to share variables between functions in Haskell?

There is nothing awkward about passing arguments to fun1 - it does use them (passing them to func2 is using them).

What is awkward, is to have your fun1 or fun2's behavior depend on hidden variables, making their behaviors difficult to reason about or predict.

Another thing you can do: make fun2 an argument to fun1 (you can pass functions as parameters in Haskell!):

fun1 :: (Char -> IO ()) -> String -> IO ()
fun1 f s = traverse_ f s

Then, you can call it in main like this:

traverse_ (fun1 (fun2 args)) ["one", "two", "three"]

That way you can pass the arguments directly to fun2, then pass fun2 to fun1...


For cases when you really do need a shared, read-only environment, use the Reader monad, or in this case, the ReaderT monad transformer.

import Data.Char
import Data.Foldable
import System.Environment
import Control.Monad.Trans
import Control.Monad.Trans.Reader

main :: IO ()
main = do
    args <- getArgs
    -- Pass in the arguments using runReaderT
    runReaderT (traverse_ fun1 ["one", "two", "three"]) args

-- The type changes, but the body stays the same.
-- fun1 doesn't care about the environment, and fun2
-- is still a Kleisli arrow; traverse_ doesn't care if
-- its type is Char -> IO () or Char -> ReaderT [String] IO ()
fun1 :: String -> ReaderT [String] IO ()
fun1 s = traverse_ fun2 s

-- Get the arguments using ask, and use liftIO
-- to lift the IO () value produced by print
-- into monad created by ReaderT
fun2 :: Char -> ReaderT [String] IO ()
fun2 c = do
    args <- ask
    liftIO $ if "-u" `elem` args 
      then print $ toUpper c
      else print $ toLower c

As an aside, you can refactor fun2 slightly:

fun2 :: Char -> ReaderT [String] IO ()
fun2 c = do
    args <- ask
    let f = if "-u" `elem` args then toUpper else toLower
    liftIO $ print (f c)

In fact, you can select toUpper or toLower as soon as you get the arguments, and put that, rather than the arguments themselves, in the environment.

main :: IO ()
main = do
    args <- getArgs
    -- Pass in the arguments using runReaderT
    runReaderT 
      (traverse_ fun1 ["one", "two", "three"])
      (if "-u" `elem` args then toUpper else toLower)

fun1 :: String -> ReaderT (Char -> Char) IO ()
fun1 s = traverse_ fun2 s

fun2 :: Char -> ReaderT (Char -> Char) IO ()
fun2 c = do
    f <- ask
    liftIO $ print (f c)

The environment type can be any value. The above examples show a list of strings and a single Char -> Char as the environment. In general, you might want a custom product type that holds whatever values you want to share with the rest of your code, for example,

data MyAppConfig = MyAppConfig { foo :: Int
                               , bar :: Char -> Char
                               , baz :: [Strings]
                               }

main :: IO ()
main = do
    args <- getArgs
    -- Process arguments and define a value of type MyAppConfig
    runReaderT fun1 MyAppConfig

fun1 :: ReaderT MyAppConfig IO ()
fun1 = do
   (MyAppConfig x y z) <- ask  -- Get the entire environment and unpack it
   x' <- asks foo  -- Ask for a specific piece of the environment
   ...

You may want to read more about the ReaderT design pattern.

Tags:

Haskell