我遇到了一种情况,即递归函数根据命令行参数做出决定。 main
不会直接调用递归函数。我想知道最好的方法是使参数对函数可用。我不想在递归函数中调用getArgs
,因为这似乎会增加很多开销。
但是,在getArgs
中调用main
然后通过不使用参数的函数传递参数是很尴尬的。这个例子不是递归的,但是希望您能理解这个概念。
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
传递参数似乎是个坏主意:
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
在面向对象的语言中,您只会在一个类中有一个成员变量,或者某种全局变量。
答案 0 :(得分:14)
将参数传递给fun1并没有什么尴尬-它确实使用了参数(将参数传递给func2使用的是它们)。
的尴尬之处在于让您的fun1或fun2的行为取决于隐藏变量,使它们的行为难以推理或预测。
您可以做的另一件事:将fun2设为fun1的参数(您可以在Haskell中将函数作为参数传递!):
fun1 :: (Char -> IO ()) -> String -> IO ()
fun1 f s = traverse_ f s
然后,您可以像这样在main
中调用它:
traverse_ (fun1 (fun2 args)) ["one", "two", "three"]
这样,您可以将参数直接传递给fun2,然后将fun2传递给fun1 ...
答案 1 :(得分:8)
对于确实需要共享的只读环境的情况,请使用Reader
monad,在这种情况下,请使用ReaderT
monad转换器。
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
顺便说一句,您可以稍微重构fun2
:
fun2 :: Char -> ReaderT [String] IO ()
fun2 c = do
args <- ask
let f = if "-u" `elem` args then toUpper else toLower
liftIO $ print (f c)
实际上,您可以在获取参数后立即选择toUpper
或toLower
,然后将那个而不是参数本身放到环境中。 / p>
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)
环境类型可以是任何值。上面的示例显示了一个字符串列表和一个Char -> Char
作为环境。通常,您可能需要一种自定义产品类型,该产品类型可以包含要与其余代码共享的任何值,例如
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
...
您可能想详细了解ReaderT design pattern。
答案 2 :(得分:7)
虽然 typedfern 的答案很好(建议),但编写尽可能多的纯函数然后推迟效果,直到无法再将其推迟为止。这样,您就可以创建数据管道,而不必传递参数。
我知道OP中显示的示例问题已简化,可能只是微不足道的程度,但是如果将逻辑与其作用分开,则编写起来会更容易。
首先,将fun2
重写为纯函数:
fun2 :: Foldable t => t String -> Char -> Char
fun2 args c =
if "-u" `elem` args then toUpper c else toLower c
如果您对参数部分地应用fun2
,则您有一个类型为Char -> Char
的函数。您希望["one", "two", "three"]
的数据(print
)具有类型[[Char]]
。您想要将每个Char
值应用于fun2 args
。本质上,这就是OP fun1
函数的作用。
但是,您可以使用[[Char]]
(或[Char]
)将join
的值展平为concat
。
*Q56438055> join ["one", "two", "three"]
"onetwothree"
现在,您只需将扁平化列表中的每个Char
值应用于fun2 args
:
*Q56438055> args = ["-u"]
*Q56438055> fmap (fun2 args) $ join ["one", "two", "three"]
"ONETWOTHREE"
这仍然是纯粹的结果,但是您现在可以通过打印每个字符来应用效果:
main :: IO ()
main = do
args <- getArgs
mapM_ print $ fmap (fun2 args) $ join ["one", "two", "three"]
通过更改函数设计,以便在函数之间传递数据,通常可以简化代码。