Haskell:函数签名

Haskell:函数签名,haskell,types,Haskell,Types,此程序编译时没有问题: bar :: MonadIO m => m String bar = undefined run2IO :: MonadIO m => m String -> m String run2IO foo = liftIO bar 当我将bar更改为foo(参数名称)时 我得到: 无法将类型“m”与“IO”匹配 “m”是一个刚性类型变量,由 run2IO::MonadIO m=>m String->m String的类

此程序编译时没有问题:

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

run2IO :: MonadIO m
       => m String
       -> m String
run2IO foo  = liftIO bar
当我将
bar
更改为
foo
(参数名称)时

我得到:

无法将类型“m”与“IO”匹配 “m”是一个刚性类型变量,由 run2IO::MonadIO m=>m String->m String的类型签名

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


为什么这两种情况不相同?

在第一种情况下,您使用的是
bar
上的
liftIO
。这实际上需要
bar::IO字符串
。现在,
IO
恰好是
MonadIO
上的一个实例,因此这是可行的–编译器只需丢弃
bar
的多态性即可

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

{-# 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
记住
liftIO的类型:

liftIO :: MonadIO m => IO a -> m a
重要的是,第一个参数必须是一个具体的
IO
值。这意味着当您有一个表达式
liftIO x
,那么
x
必须是
IO a
类型

当Haskell函数被普遍量化(使用隐式或显式的
forall
)时,这意味着函数调用方选择类型变量的替换对象。作为一个例子,考虑<代码> ID>代码>函数:它具有类型<代码> A- > A/<代码>,但是当您对表达式<代码> id“真/ <代码>进行评估时,则<代码> ID 采用类型<代码>布尔-> BOOL < /代码>,因为<代码> A<代码>被实例化为<代码> BOOL < /代码>类型。< /P>

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

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

幸运的是,
IO
有一个
MonadIO
实例,因此使用
IO
bar
值实例化为
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
指定不同的类型:

{-# LANGUAGE RankNTypes #-}

run3IO :: MonadIO m => (forall m1. MonadIO m1 => m1 Integer) -> m Integer
run3IO foo = liftIO foo

这将进行类型检查,但它不是一个非常有用的函数。

我可以问一下,从哪里可以获得有关编译器行为的信息?我已经在Haskell中编写了大约6个月的代码,我想进一步了解在幕后发生了什么。呃,你所说的“关于编译器行为的信息”是什么意思?您当然可以通过查看了解GHC的工作。事实上,我从来没有读过太多的东西——我发现写很多代码,看看会发生什么更有用。最好不要把学习这样的行为看作是学习编译器,因为这种行为是由语言本身指定的。由类型签名提供的量化类型并不是一个罕见的触发点,它是一件值得深入研究的事情,而不是用“嗯,总有一天我会理解”之类的响应。它可能值得一提,也可能不值得一提
primitive
中的
PrimBase
类。
{-# LANGUAGE RankNTypes #-}

run3IO :: MonadIO m => (forall m1. MonadIO m1 => m1 Integer) -> m Integer
run3IO foo = liftIO foo