我目前正在尝试围绕类型类和实例,我还不太明白它们的意义。到目前为止,我对此事有两个问题:
1)当函数使用该类型类中的某些函数时,为什么必须在函数签名中使用类型类。例:
f :: (Eq a) => a -> a -> Bool
f a b = a == b
为什么把(Eq a)
放在签名中。如果没有为==
定义a
那么为什么不在遇到a == b
时抛出错误?必须提前声明类型类有什么意义?
2)类型类和函数重载如何相关?
这样做是不可能的:
data A = A
data B = B
f :: A -> A
f a = a
f :: B -> B
f b = b
但是可以这样做:
data A = A
data B = B
class F a where
f :: a -> a
instance F A where
f a = a
instance F B where
f b = b
怎么了?为什么我不能拥有两个具有相同名称但在不同类型上运行的函数...来自C ++我觉得很奇怪。但我可能对这些事情到底有什么错误的概念。但是一旦我将它们包装在这些类型类实例中,我就可以。
也可以随意向我输入类别或输入理论词汇,因为我正在学习Haskell的同时学习这些科目,我怀疑Haskell如何在这里做事情的理论基础。
我同意Willem Van Onsem’s answer的大部分内容,但我认为它忽略了类型类比真正的ad-hoc重载的一个主要优点:抽象。想象一下,我们使用ad-hoc重载而不是类型来定义Monad
操作:
-- Maybe
pure :: a -> Maybe a
pure = Just
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just x >>= f = f x
Nothing >>= _ = Nothing
-- Either
pure :: a -> Either e a
pure = Right
(>>=) :: Either e a -> (a -> Either e b) -> Either e b
Right x >>= f = f x
Left err >>= _ = Left err
现在,我们知道每个monad可以用pure
和>>=
表示,如上所述,但我们也知道它们可以用fmap
,pure
和join
等效表达。因此,我们应该能够实现适用于任何monad的join
函数:
join x = x >>= id
但是,现在我们遇到了问题。什么是join
的类型?
显然,join
必须是多态的,因为它适用于任何monad设计。但是给它类型签名forall m a. m (m a) -> m a
显然是错误的,因为它不适用于所有类型,只有monadic类型。因此,我们需要在我们的类型中表示需要存在一些操作(>>=) :: m a -> (a -> m b) -> m b
,这正是类型类约束提供的。
鉴于此,很明显ad-hoc重载使得重载名称成为可能,但是不可能对这些重载名称进行抽象,因为不能保证不同的实现以任何方式相关。你可以定义没有类型类的monad,但是你不能定义join
,when
,unless
,mapM
,sequence
,以及你定义两个操作时免费获得的所有其他好东西。
因此,在Haskell中必须使用类型类来实现代码重用并避免大量重复。但是,您是否同时具有类型类型重载和类型定向的特殊名称重载?是的,事实上,伊德里斯确实如此。但是Idris的类型推断与Haskell的类型推断非常不同,因此在Willem的答案中,由于许多原因,它比Haskell更可行。
简而言之:因为这就是Haskell的设计方式。
为什么把
(Eq a)
放在签名中。如果==
没有为a定义那么为什么不在遇到a == b
时抛出错误?
为什么我们将类型放在C ++程序的签名中(而不仅仅是作为主体中的断言)?因为这就是C ++的设计方式。通常,关于构建什么编程语言的概念是“明确需要明确的内容”。
并不是说Haskell模块是开源的。这意味着我们只提供签名。因此,当我们写例如:
Prelude> foo A A
<interactive>:4:1: error:
• No instance for (Eq A) arising from a use of ‘foo’
• In the expression: foo A A
In an equation for ‘it’: it = foo A A
我们经常在这里写foo
类型没有Eq
类型类。因此,我们会得到很多错误,这些错误只能在编译时发现(或者如果Haskell在运行时是动态语言)。将Eq a
放入类型签名中的想法是我们可以提前查找foo
的签名,从而确保类型是类型类的实例。
请注意,您不必自己编写类型签名:Haskell通常可以派生函数的签名,但签名应包含调用和有效使用函数所需的所有必要信息。通过添加类型约束,我们加快了开发速度。
怎么了?为什么我不能拥有两个具有相同名称但在不同类型上运行的函数。
再说一遍:这就是Haskell的设计方式。函数式编程语言中的函数是“一等公民”。这意味着这些通常都有一个名称,我们希望尽可能避免名称冲突。就像C ++中的类通常具有唯一的名称(名称空间除外)。
假设您将定义两个不同的功能:
incr :: Int -> Int
incr = (+1)
incr :: Bool -> Bool
incr _ = True
bar = incr
然后哪个incr
bar
必须选择?当然我们可以使类型明确(即incr :: Bool -> Bool
),但通常我们想避免这种工作,因为它会引入很多噪音。
我们不这样做的另一个好理由是因为通常类型类不仅仅是函数的集合:它将合约添加到这些函数中。例如,Monad
类型类必须满足函数之间的某些关系。例如,(>>= return)
应与id
等效。换句话说,类型类:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
没有描述两个独立的函数(>>=)
和return
:这是一组函数。你有两个(通常在特定的>>=
和return
之间有一些合同),或者根本没有。
这只回答问题1(直接,至少)。
类型签名f :: a -> a -> Bool
是f :: forall a. a -> a -> Bool
的简写。 f
不会真正适用于所有类型a
,如果它只适用于a
定义的(==)
s。对具有(==)
的类型的这种限制使用(Eq a)
中的约束f :: forall a. (Eq a) => a -> a -> Bool
表示。
“对于所有人来说”/通用量化是Haskell(参数)多态性的核心,除其他外,还提供了parametricity强大而重要的属性。
Haskell坚持两个公理(其中包括):
如果你有
f :: A -> A
和
f :: B -> B
然后,根据Haskell采用的原则,f
仍然是一个有效的表达式,它本身仍然必须有一个类型。虽然使用子类型可以做到这一点,但它被认为比类型级解决方案复杂得多。
同样,需要Eq a
(==) :: Eq a => a -> a -> Bool
来自==
的类型必须完全描述你可以用它做什么的事实。如果您只能在某些类型中调用它,则类型签名必须反映出来。