这个程序编译没有问题:
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个案例不相同?
答案 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
个实例,因此使用MonadIO
将bar
值实例化成为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
。