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.
It's an alias for mappend , from the Data. Monoid module.
We first note that variables in Haskell can only be assigned once, unlike in many imperative programming languages, where a variable can be overwritten with different values arbitrarily many times.
The ++ operator is the list concatenation operator which takes two lists as operands and "combine" them into a single list.
Let bindings let you bind to variables anywhere and are expressions themselves, but are very local, so they don't span across guards. Just like any construct in Haskell that is used to bind values to names, let bindings can be used for pattern matching.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With