3

我想编写一个函数,可以在 Haskell 中以广度优先递归方式列出目录。如您所见,我需要一个可以将 a (a -> IO b) 转换为 IO (a->b) 的函数。看起来很简单,我做不到。我想知道该怎么做或是否有可能。

dirElem :: FilePath -> IO [FilePath]
dirElem dirPath = do
  getDirectoryContents'' <- theConvert getDirectoryContents'
  return $ takeWhile (not.null) $ iterate (concatMap getDirectoryContents'') [dirPath] where
    getDirectoryContents' dirPath = do
      isDir <- do doesDirectoryExist dirPath
      if isDir then dirContent else return [] where
        dirContent = do
          contents <- getDirectoryContents dirPath
          return.(map (dirElem</>)).tail.tail contents
    theConvert :: (a -> IO b) -> IO (a -> b)
    theConvert = ??????????
4

4 回答 4

6

通常,您不能以纯方式执行此操作,但是如果您可以枚举所有参数值,则可以预先执行所有 IO 并返回纯函数。就像是

cacheForArgs :: [a] -> (a -> IO b) -> IO (a -> b)
cacheForArgs as f = do
    bs <- mapM f as
    let abs = zip as bs
    return $ \ a -> fromMaybe (error "argument not cached") $ lookup a abs

cacheBounded :: (Enum a, Bounded a) => (a -> IO b) -> IO (a -> b)
cacheBounded = cacheForArgs [minBound .. maxBound]

但是这个函数在你的用例中并没有真正帮助你。

于 2013-01-21T08:30:48.850 回答
6

这是无法做到的。原因是该函数可以使用其类型参数a来确定IO执行什么操作。考虑

action :: Bool -> IO String
action True  = putStrLn "Enter something:" >> getLine
action False = exitFailure

现在,如果您以某种方式将其转换为IO (Bool -> String)并评估此操作,会发生什么?没有解决办法。我们无法决定是否应该读取字符串或退出,因为我们还不知道Bool参数(如果结果函数没有在参数上调用,我们可能永远都不知道)。

约翰的回答是个主意。它只是让 IO 动作逃逸到纯粹的计算中,这将使您的生活变得悲惨,并且您将失去 Haskell 的引用透明性!例如运行:

main = unsafe action >> return ()

即使调用了 IO 操作,也不会执行任何操作。此外,如果我们稍微修改一下:

main = do
   f <- unsafe action
   putStrLn "The action has been called, calling its pure output function."
   putStrLn $ "The result is: " ++ f True

您会看到action请求输入的f. 您无法保证何时(如果有的话)执行该操作!

编辑:正如其他人指出的那样,它不仅仅针对IO. 例如,如果 monad 是Maybe,则您无法实现(a -> Maybe b) -> Maybe (a -> b). 或者Either,您无法实施(a -> Either c b) -> Either c (a -> b). 关键始终是,a -> m b我们可以根据 选择不同的效果a,而m (a -> b)效果必须是固定的。

于 2013-01-21T21:16:25.473 回答
4

您无法以安全的方式创建此类功能。假设我们有f :: a -> IO bg = theConvert f :: IO (a -> b)。它们是两个非常不同的函数f,一个函数接受一个类型的参数a并返回一个IO带有结果的动作b,其中 io-action 可能取决于给定的参数。g另一方面是一个IO作为结果函数a->b的动作,io-action 不能依赖于任何参数。现在来说明这个问题让我们看看

theConvert putStr :: IO (String -> ())

现在它运行时应该做什么,它当然不能打印给定的参数,因为它没有参数。因此,与 putStr 不同的是,它只能执行一项操作,然后返回一些 type 的函数String -> (),该函数只有一个选项const ()(假设不使用erroror undefined)。


就像一面不一样,也可以反过来做,它增加了结果动作取决于论点的概念,而实际上并非如此。它可以写成

theOtherConvert :: IO (a -> b) -> (a -> IO b)
theOtherConvert m x = m >>= \f -> return $ f x

尽管它适用于任何 monad,或以应用形式theOtherConvert m x = m <*> pure x

于 2013-01-21T10:47:32.737 回答
3

Petr Pudlák 的回答非常好,但我觉得它可以通过从 中抽象出来,并从和类型类IO的角度来看待它。ApplicativeMonad

Applicative考虑来自和的“组合”操作的类型Monad

(<*>) :: Applicative m => m (a -> b) -> m a -> m b
(>>=) :: Monad m => m a -> (a -> m b) -> m b

所以你可以说你的类型a -> IO b是“monadic”而IO (a -> b)“applicative”——这意味着你需要monadic操作来组成看起来像的类型a -> IO b,但只有适用于IO (a -> b)

Monad和之间的“功率”差异有一个众所周知的直观陈述Applicative

  • 应用计算具有固定的静态结构;将执行哪些操作、执行的顺序以及组合结果的方式是提前知道的。
  • 一元计算没有这样固定的静态结构;一元计算可以检查其子操作之一的结果值,然后在执行时在不同的结构之间进行选择。

彼得的回答就是对这一点的具体说明。我将重复他的定义action

action :: Bool -> IO String
action True  = putStrLn "Enter something:" >> getLine
action False = exitFailure

假设我们有foo :: IO Bool. 然后,当我们编写将' 参数foo >>= action绑定到' 结果时,生成的计算不亚于我的第二个要点描述的内容;它检查执行结果,并根据其值在替代操作之间进行选择。这正是使您无法做到的事情之一。除非您同时预先确定要采用哪个分支,否则您不能 Petr进入。actionfoofooMonadApplicativeactionIO (Bool -> String)

类似的评论适用于augustss 的回应。通过要求提前指定值列表,它所做的是让您提前选择要采用的分支,将它们全部取出,然后允许您在它们的结果之间进行选择。

于 2013-01-21T23:56:14.850 回答