paperduck
paperduck

Reputation: 1195

Idiomatic way to share variables between functions in Haskell?

I have a situation where a recursive function makes a decision based on the command line arguments. The recursive function is not called directly by main. I'm wondering what the best way is to make the arguments available to the function. I do not want to call getArgs inside the recursive function, because that seems like it would add a lot of overhead.

However, it is awkward to call getArgs in main and then pass the arguments through a function that doesn't use them. This example is not recursive, but hopefully you get the concept.

import Data.Char
import System.Environment

main :: IO ()
main = do
    args <- getArgs  -- want to use these args in fun2
    traverse_ fun1 ["one", "two", "three"]

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

fun2 :: Char -> IO ()
fun2 c = do
    if "-u" `elem` args then print $ toUpper c  -- how to get args here?
    else print $ toLower c

Passing the arguments around seems like a bad idea:

import Data.Char
import System.Environment

main :: IO ()
main = do
    args <- getArgs -- want to use these args in fun2
    traverse_ (fun1 args) ["one", "two", "three"]

fun1 :: [String] -> String -> IO ()
fun1 args s = traverse_ (fun2 args) s

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

In an object-oriented language, you would just have a member variable in a class, or some sort of global variable.

Upvotes: 9

Views: 725

Answers (3)

chepner
chepner

Reputation: 531400

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.

Upvotes: 7

Mark Seemann
Mark Seemann

Reputation: 233162

While the answer by typedfern is good (upvoted), it'd be even more idiomatic to write as many pure functions as possible, and then defer the effects until the time where you can no longer postpone them. This enables you to create a pipeline of data instead of having to pass arguments around.

I understand that the example problem shown in the OP is simplified, possibly to the degree that it's trivial, but it'd be easier to compose if you separate the logic from its effects.

First, rewrite fun2 to a pure function:

fun2 :: Foldable t => t String -> Char -> Char
fun2 args c =
  if "-u" `elem` args then toUpper c else toLower c

If you partially apply fun2 with your arguments, you have a function with the type Char -> Char. The data (["one", "two", "three"]) you wish to print, however, has the type [[Char]]. You want to apply each of the Char values to fun2 args. That's essentially what the OP fun1 function does.

You can, however, instead flatten the [[Char]] value to [Char] with join (or concat).

*Q56438055> join ["one", "two", "three"]
"onetwothree"

Now you can simply apply each of the Char values in the flattened list to fun2 args:

*Q56438055> args = ["-u"]
*Q56438055> fmap (fun2 args) $ join ["one", "two", "three"]
"ONETWOTHREE"

This is still a pure result, but you can now apply the effect by printing each of the characters:

main :: IO ()
main = do
  args <- getArgs
  mapM_ print $ fmap (fun2 args) $ join ["one", "two", "three"]

By changing the function design so that you're passing data from function to function, you can often simplify the code.

Upvotes: 6

typedfern
typedfern

Reputation: 1257

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

Upvotes: 13

Related Questions