Haskell:将函数作为参数传递时的刚性类型变量错误

Mar*_*oni 2 haskell types hindley-milner

GHC说我的功能过于笼统,不能作为论据传递.

这是一个重现错误的简化版本:

data Action m a = SomeAction (m a)


runAction :: Action m a -> m a
runAction (SomeAction ma) =  ma

-- Errors in here
actionFile :: (Action IO a -> IO a) -> String -> IO ()
actionFile actionFunc fileName = do
    actionFunc $ SomeAction $ readFile fileName
    actionFunc $ SomeAction $ putStrLn fileName


main :: IO ()
main =
    actionFile runAction "Some Name.txt"
Run Code Online (Sandbox Code Playgroud)

这就是错误所说的:

 • Couldn't match type ‘a’ with ‘()’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          actionFile :: forall a. (Action IO a -> IO a) -> String -> IO ()
        at src/Lib.hs:11:15
      Expected type: Action IO a
        Actual type: Action IO ()
Run Code Online (Sandbox Code Playgroud)

编译器希望我在我的类型签名中更具体,但我不能,因为我需要使用不同类型的参数的参数函数.就像在我的例子中我传递它Action IO ()和一个Action IO String.

如果我取代(Action IO a -> IO a) -> String -> IO ()(Action IO () -> IO ()) -> String -> IO (),像编译器要求,与调用readFile错误,因为它输出IO String.

为什么会发生这种情况,我该怎么做才能将此函数作为参数传递?

我知道如果我只是runAction在我的actionFile函数内部使用一切都会工作,但在我的实际代码中runAction是一个部分应用的函数,它是从IO计算的结果构建的,所以它在编译时不可用.

chi*_*chi 6

这是一个量词问题.类型

actionFile :: (Action IO a -> IO a) -> String -> IO ()
Run Code Online (Sandbox Code Playgroud)

意味着,如GHC错误所报告的那样,

actionFile :: forall a. (Action IO a -> IO a) -> String -> IO ()
Run Code Online (Sandbox Code Playgroud)

其中陈述如下:

  • 调用者必须选择一种类型 a
  • 调用者必须提供一个功能 g :: Action IO a -> IO a
  • 来电者必须提供 String
  • 最后,actionFile必须回答IO ()

请注意,a调用者选择,而不是actionFile.从这个角度来看actionFile,这种类型变量被一个固定的未知类型绑定,由其他人选择:这是GHC在错误中提到的"刚性"类型变量.

但是,actionFile正在调用g传递Action IO ()参数(因为putStrLn).这意味着actionFile想要选择a = ().由于调用者可以选择不同a的类型,因此会引发类型错误.

此外,actionFile还想调用g传递Action IO String参数(因为readFile),所以我们也想选择a = String.这意味着g必须接受a我们所希望的任何选择.

正如Alexis King所提到的,解决方案可能是移动量词并使用rank-2类型:

actionFile :: (forall a. Action IO a -> IO a) -> String -> IO ()
Run Code Online (Sandbox Code Playgroud)

这种新类型意味着:

  • 调用者必须提供一个功能 g :: forall a. Action IO a -> IO a
    • g(即actionFile)的来电者必须选择a
    • g(即actionFile)的来电者必须提供Action IO a
    • 最后,g必须提供一个IO a
  • 来电者必须提供 String
  • 最后,actionFile必须回答IO ()

这使得可以根据需要actionFile进行选择a.

  • @MarceloLazaroni不是真实的.你可以制作`actionFile :( Action IO() - > IO()) - >(Action IO String - > IO String) - > String - > IO()`但它会很奇怪.另请注意,现代库和应用程序通常使用许多扩展是很常见的.我甚至说没有人在普通的Haskell中编写严肃的代码.大多数扩展都是无害的,并且可以说其中大量扩展应该只包含在修订的Haskell报告中,只是因为它们变得如此受欢迎.不要害怕使用它们,每个人都已经这样做了. (2认同)