Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic way to share variables between functions in Haskell?

Tags:

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.

like image 638
paperduck Avatar asked Jun 04 '19 05:06

paperduck


People also ask

What is <> called in Haskell?

It's an alias for mappend , from the Data. Monoid module.

Can you assign variables in Haskell?

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.

What does ++ mean in Haskell?

The ++ operator is the list concatenation operator which takes two lists as operands and "combine" them into a single list.

What does let do in Haskell?

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.


2 Answers

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

like image 83
typedfern Avatar answered Nov 01 '22 17:11

typedfern


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.

like image 27
chepner Avatar answered Nov 01 '22 18:11

chepner