Haskell:函数签名

时间:2016-09-04 19:28:03

标签: haskell types

这个程序编译没有问题:

bar :: MonadIO m
    => m String
bar = undefined

run2IO :: MonadIO m
       => m String
       -> m String
run2IO foo  = liftIO bar

当我将bar更改为foo(参数名称)时,

run2IO :: MonadIO m
       => m String
       -> m String
run2IO foo  = liftIO foo

我明白了:

  

无法将类型'm'与'IO'匹配         'm'是绑定的刚性类型变量             run2IO的类型签名:: MonadIO m => m String - > m String   ...

     

预期类型:IO字符串   实际类型:m String ...

为什么这2个案例不相同?

2 个答案:

答案 0 :(得分:9)

请记住liftIO的类型:

liftIO :: MonadIO m => IO a -> m a

重要的是,第一个参数必须是具体的IO值。这意味着当您有表达式liftIO x时,x必须是IO a类型。

当Haskell函数被普遍量化时(使用隐式或显式forall),这意味着函数调用者选择替换类型变量的内容。例如,考虑id函数:它的类型为a -> a,但在评估表达式id True时,id采用类型Bool -> Bool,因为{ {1}}被实例化为a类型。

现在,再考虑一下你的第一个例子:

Bool

run2IO :: MonadIO m => m Integer -> m Integer run2IO foo = liftIO bar 参数在这里完全不相关,所以真正重要的是foo表达式。由于liftIO bar 要求其第一个参数属于liftIO类型,因此IO a 必须属于bar类型。但是,IO a是多态的:它实际上具有类型bar

幸运的是,MonadIO m => m Integer有一个IO个实例,因此使用MonadIObar实例化成为IO,没关系,因为IO Integer是普遍量化的,所以它的实例化是通过它的使用来选择的。

现在,考虑另一种使用bar的情况。这个似乎就像它一样,但它实际上根本不存在:这次,liftIO foo值是函数的参数,而不是单独的值。量化是在整个函数上,而不是单个值。要更直观地理解这一点,再次考虑MonadIO m => m Integer可能会有所帮助,但这一次,请考虑其定义:

id

在这种情况下,id :: a -> a id x = x 无法在其定义中实例化为x,因为这意味着Bool只能处理id值,这显然是错误的。实际上,在Bool的实现中,id必须完全一般地使用 - 它不能被实例化为特定类型,因为这会违反参数保证。

因此,在x函数中,run2IO必须完全一般地用作任意foo值,而不是特定的MonadIO实例。 MonadIO调用尝试使用不允许的特定liftIO实例,因为调用者可能不会提供IO值。

当然,您可能希望以与IO相同的方式量化函数的参数;也就是说,您可能希望实例选择其实例化,而不是调用者。在这种情况下,您可以使用bar语言扩展名使用显式RankNTypes指定其他类型:

forall

这将是类型检查,但它不是一个非常有用的功能。

答案 1 :(得分:5)

首先,您在liftIO上使用bar。这实际上需要bar :: IO String。现在,IO碰巧(通常)是MonadIO上的一个实例,所以这很有效 - 编译器只是抛弃了bar的多态性。

在第二种情况下,编译器无法决定使用哪个特定monad作为foo的类型:它是由环境修复的,即调用者可以决定什么它应该是MonadIO个实例。要再次获得选择IO作为monad的自由,您需要以下签名:

{-# LANGUAGE Rank2Types, UnicodeSyntax #-}

run2IO' :: MonadIO m
       => (∀ m' . MonadIO m' => m' String)
       -> m String
run2IO' foo  = liftIO foo

...但是我不认为你真的想要那样:你也可以写

run2IO' :: MonadIO m => IO String -> m String
run2IO' foo  = liftIO foo

或只是run2IO = liftIO