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.