如何轻松应对 Haskell 上的类型系统?
How to comfortably deal with the type system on Haskell?
Haskell的类型系统功能强大,因其数学上的严谨性和逻辑上的稳健性而受到人们的喜爱,另一方面,像下面这样幼稚的东西让我想知道为什么它没有按照直觉预期的那样工作?
例如为什么 Int
不能在 x3
上转换为 Num
但 f1
接受 Int
反对签名 Num
?
Prelude> let x1 = 1
Prelude> :t x1
x1 :: Num a => a
Prelude> let x2 = 1 :: Int
Prelude> :t x2
x2 :: Int
Prelude> let x3 = (1 :: Int) :: Num a => a
Couldn't match expected type ‘a1’ with actual type ‘Int’
Prelude> let f1 :: Num a => a -> a; f1 = id
Prelude> :t f1 (1 :: Int)
f1 (1 :: Int) :: Int
Prelude> let f2 :: Int -> Int; f2 = id
Prelude> :t f2 1
f2 1 :: Int
Prelude> let f3 :: Num a => a -> Int; f3 = id
Prelude> let f4 :: Num a => Int -> a; f4 = id
Couldn't match type ‘a’ with ‘Int’
我知道人们最终应该学习基础理论,例如HM type system to comfortably deal with the type system, and even found some nice writings e.g. 1, 2, 3 and 4 揭开它的神秘面纱。如果您遇到并克服了这个挑战,您还有什么想推荐的?
@EDIT
Prelude> let f5 x5 = x5::Int
Prelude> :t f5
f5 :: Int -> Int
Prelude> let f6 x6 = x6::Num a => a
Couldn't match expected type ‘a1’ with actual type ‘t’
首先,x6
一定是 Num
的超类型,当 x6
被类型注释为 Num
时,它就是 Num
本身。但是,如果我们随后将 x6
从 Num
向下转换为 Int
,Int
之后 (x6::Int)::Num a => a
的 Num
的连接类型注释将不会合并.因此,
x6
的第一个推断类型 Num
在这里不满足。
why can't Int
be converted to Num
on x3
Int
无法转换为 Num
因为 Int
是一个类型而 Num
是一个 类型 class。这两种实体之间的区别有望在下文中变得清楚。
Int
无法转换为其他任何内容,因为 Haskell 没有您在此处使用的意义上的转换。没有隐式转换。确实发生的是多态类型 specialised 到某种确定的类型;但是,确定的类型永远不会自动变成其他类型。
考虑到这一点,让我们考虑一下您的示例。
Prelude> let x1 = 1
Prelude> :t x1
x1 :: Num a => a
x1
这里是 polymorphic,这意味着它可以根据您的使用方式采用不同的类型。这种不确定性可以通过类型变量 a
的存在来识别(类型变量与具体类型不同,没有大写)。 x1
的类型虽然是多态的,但在某种程度上受到约束Num a
的限制。 Num a => a
可以读作 "any type that has an instance of the type class Num
",而普通的 a
则表示 "any type whatsoever"。
Prelude> let x2 = 1 :: Int
Prelude> :t x2
x2 :: Int
引入类型注解:: Int
意味着要求Int
与1
、Num a => a
类型统一。在这种情况下,这只是意味着用 Int
替换类型变量 a
。鉴于 Int
确实有一个 Num
的实例,这是一个有效的移动,并且类型检查器很乐意接受它。类型注释 将 的多态类型 1
特化为 Int
。
Prelude> let x3 = (1 :: Int) :: Num a => a
Couldn't match expected type ‘a1’ with actual type ‘Int’
1 :: Int
的类型是Int
。第二个注释要求将它与 Num a => a
统一起来。然而,这是不可能的。一旦类型被专门化,您就不能 "forget" 类型并仅通过提供类型注释来恢复专门化。也许您正在考虑 OOP 向上转型;这根本不是一回事。顺便说一下,如果类型检查器接受了 x3
,您就可以编写 x4 = ((1 :: Int) :: Num a => a) :: Double
,从而将 Int
转换为 Double
。但是,在一般情况下,这种转换不可能像那样发生,因为您没有告诉 如何 转换要完成;至于特殊情况,没有。 (将 Int
转换为 Double
当然是可能的,但它需要一个适当的函数。例如,您可能会发现考虑 fromIntegral
的类型如何与其作用相关.)
Prelude> let f1 :: Num a => a -> a; f1 = id
Prelude> :t f1 (1 :: Int)
f1 (1 :: Int) :: Int
此处的原则保持不变。唯一的区别是你必须考虑参数和结果的类型是如何相互关联的。 id
的类型是 a -> a
。它专门针对 Num a => a -> a
。传递 Int
参数进一步将其特化为 Int -> Int
,因此您会得到类型为 Int
.
的结果
Prelude> let f2 :: Int -> Int; f2 = id
Prelude> :t f2 1
f2 1 :: Int
f1
有一个多态类型,你可以通过给它一个 Int
参数来特化它,而 f2
有一个单态类型,所以不需要特化它。 id
从 a -> a
直接特化到 Int -> Int
,而 1
从 Num a => a
特化到 Int
因为你将它提供给一个函数需要一个 Int
参数。
Prelude> let f3 :: Num a => a -> Int; f3 = id
Couldn't match type ‘a’ with ‘Int’
这里,你想统一a -> a
,id
的类型,和Num a => a -> Int
。但是,如果您将 a
替换为 Num a => a -> Int
中的 Double
,您将得到 Double -> Int
,它不可能与 a -> a
统一,因为它改变类型而 a -> a
不改变类型。 (这就是上面 Thomas M. DuBuisson 评论的要点:您的实现类型与 id
的类型不兼容,因为 id
无法更改任何类型。)
Prelude> let f4 :: Num a => Int -> a; f4 = id
Couldn't match type ‘a’ with ‘Int’
最后,这与 f3
类似,只是不匹配发生在结果类型上而不是参数类型上。这次换一种说法,您不能通过使用 Num
实例确定特定类型来实现 Num a => Int -> a
函数(无论是 Int
、Double
、等),然后 "upcasting" 到 Num a => a
,因为没有向上转换这样的东西。相反 Num a => Int -> a
必须为 任何 选择 a
任何具有 Num
.
实例的选择
Haskell的类型系统功能强大,因其数学上的严谨性和逻辑上的稳健性而受到人们的喜爱,另一方面,像下面这样幼稚的东西让我想知道为什么它没有按照直觉预期的那样工作?
例如为什么 Int
不能在 x3
上转换为 Num
但 f1
接受 Int
反对签名 Num
?
Prelude> let x1 = 1
Prelude> :t x1
x1 :: Num a => a
Prelude> let x2 = 1 :: Int
Prelude> :t x2
x2 :: Int
Prelude> let x3 = (1 :: Int) :: Num a => a
Couldn't match expected type ‘a1’ with actual type ‘Int’
Prelude> let f1 :: Num a => a -> a; f1 = id
Prelude> :t f1 (1 :: Int)
f1 (1 :: Int) :: Int
Prelude> let f2 :: Int -> Int; f2 = id
Prelude> :t f2 1
f2 1 :: Int
Prelude> let f3 :: Num a => a -> Int; f3 = id
Prelude> let f4 :: Num a => Int -> a; f4 = id
Couldn't match type ‘a’ with ‘Int’
我知道人们最终应该学习基础理论,例如HM type system to comfortably deal with the type system, and even found some nice writings e.g. 1, 2, 3 and 4 揭开它的神秘面纱。如果您遇到并克服了这个挑战,您还有什么想推荐的?
@EDIT
Prelude> let f5 x5 = x5::Int
Prelude> :t f5
f5 :: Int -> Int
Prelude> let f6 x6 = x6::Num a => a
Couldn't match expected type ‘a1’ with actual type ‘t’
首先,x6
一定是 Num
的超类型,当 x6
被类型注释为 Num
时,它就是 Num
本身。但是,如果我们随后将 x6
从 Num
向下转换为 Int
,Int
之后 (x6::Int)::Num a => a
的 Num
的连接类型注释将不会合并.因此,
x6
的第一个推断类型 Num
在这里不满足。
why can't
Int
be converted toNum
onx3
Int
无法转换为Num
因为Int
是一个类型而Num
是一个 类型 class。这两种实体之间的区别有望在下文中变得清楚。Int
无法转换为其他任何内容,因为 Haskell 没有您在此处使用的意义上的转换。没有隐式转换。确实发生的是多态类型 specialised 到某种确定的类型;但是,确定的类型永远不会自动变成其他类型。
考虑到这一点,让我们考虑一下您的示例。
Prelude> let x1 = 1
Prelude> :t x1
x1 :: Num a => a
x1
这里是 polymorphic,这意味着它可以根据您的使用方式采用不同的类型。这种不确定性可以通过类型变量 a
的存在来识别(类型变量与具体类型不同,没有大写)。 x1
的类型虽然是多态的,但在某种程度上受到约束Num a
的限制。 Num a => a
可以读作 "any type that has an instance of the type class Num
",而普通的 a
则表示 "any type whatsoever"。
Prelude> let x2 = 1 :: Int
Prelude> :t x2
x2 :: Int
引入类型注解:: Int
意味着要求Int
与1
、Num a => a
类型统一。在这种情况下,这只是意味着用 Int
替换类型变量 a
。鉴于 Int
确实有一个 Num
的实例,这是一个有效的移动,并且类型检查器很乐意接受它。类型注释 将 的多态类型 1
特化为 Int
。
Prelude> let x3 = (1 :: Int) :: Num a => a
Couldn't match expected type ‘a1’ with actual type ‘Int’
1 :: Int
的类型是Int
。第二个注释要求将它与 Num a => a
统一起来。然而,这是不可能的。一旦类型被专门化,您就不能 "forget" 类型并仅通过提供类型注释来恢复专门化。也许您正在考虑 OOP 向上转型;这根本不是一回事。顺便说一下,如果类型检查器接受了 x3
,您就可以编写 x4 = ((1 :: Int) :: Num a => a) :: Double
,从而将 Int
转换为 Double
。但是,在一般情况下,这种转换不可能像那样发生,因为您没有告诉 如何 转换要完成;至于特殊情况,没有。 (将 Int
转换为 Double
当然是可能的,但它需要一个适当的函数。例如,您可能会发现考虑 fromIntegral
的类型如何与其作用相关.)
Prelude> let f1 :: Num a => a -> a; f1 = id
Prelude> :t f1 (1 :: Int)
f1 (1 :: Int) :: Int
此处的原则保持不变。唯一的区别是你必须考虑参数和结果的类型是如何相互关联的。 id
的类型是 a -> a
。它专门针对 Num a => a -> a
。传递 Int
参数进一步将其特化为 Int -> Int
,因此您会得到类型为 Int
.
Prelude> let f2 :: Int -> Int; f2 = id
Prelude> :t f2 1
f2 1 :: Int
f1
有一个多态类型,你可以通过给它一个 Int
参数来特化它,而 f2
有一个单态类型,所以不需要特化它。 id
从 a -> a
直接特化到 Int -> Int
,而 1
从 Num a => a
特化到 Int
因为你将它提供给一个函数需要一个 Int
参数。
Prelude> let f3 :: Num a => a -> Int; f3 = id
Couldn't match type ‘a’ with ‘Int’
这里,你想统一a -> a
,id
的类型,和Num a => a -> Int
。但是,如果您将 a
替换为 Num a => a -> Int
中的 Double
,您将得到 Double -> Int
,它不可能与 a -> a
统一,因为它改变类型而 a -> a
不改变类型。 (这就是上面 Thomas M. DuBuisson 评论的要点:您的实现类型与 id
的类型不兼容,因为 id
无法更改任何类型。)
Prelude> let f4 :: Num a => Int -> a; f4 = id
Couldn't match type ‘a’ with ‘Int’
最后,这与 f3
类似,只是不匹配发生在结果类型上而不是参数类型上。这次换一种说法,您不能通过使用 Num
实例确定特定类型来实现 Num a => Int -> a
函数(无论是 Int
、Double
、等),然后 "upcasting" 到 Num a => a
,因为没有向上转换这样的东西。相反 Num a => Int -> a
必须为 任何 选择 a
任何具有 Num
.