我目前正试图围绕类型类和实例进行思考,但我还不太明白它们的重点。到目前为止,我对此事有两个问题:
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在这里做事情的理论基础。
答案 0 :(得分:36)
我同意Willem Van Onsem’s answer的大部分内容,但我认为它忽略了类型类比真正的临时重载的主要优势之一:抽象。想象一下,我们使用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
和>>=
表示,如上所述,但我们也知道它们可以使用{等效表达{1}},fmap
和pure
。因此,我们应该能够实现一个适用于任何 monad的join
函数:
join
然而,现在我们遇到了问题。什么是join x = x >>= id
的类型?
显然,join
必须是多态的,因为它适用于任何monad设计。但是给它类型签名join
显然是错误的,因为它不适用于所有类型,只有monadic类型。因此,我们需要在我们的类型中表示需要存在某些操作forall m a. m (m a) -> m a
的东西,这正是类型类约束提供的。
鉴于此,很明显ad-hoc重载使得重载名称成为可能,但是不可能对这些重载名称进行抽象,因为不能保证不同的实现以任何方式相关。你可以定义没有类型类的monad,但是你无法定义(>>=) :: m a -> (a -> m b) -> m b
,join
,when
,unless
,mapM
,以及在定义两个操作时免费获得的所有其他好东西。
因此,在Haskell中必须使用类型类来实现代码重用并避免大量重复。但是你有两种类型类型重载和类型导向的特殊名称重载吗? 是,事实上,伊德里斯确实如此。但是Idris的类型推断与Haskell的类型推断非常不同,因此在Willem的答案中,由于许多原因,它比Haskell更可行。
答案 1 :(得分:16)
简而言之:因为这就是Haskell的设计方式。
为什么将
public function index() { $categories = Category::orderBy('name', 'asc')->get(); return view('categories')->with('categories',$categories); }
放入签名中。如果未定义(Eq a)
,那么为什么不在遇到==
时抛出错误?
为什么我们将类型放在C ++程序的签名中(而不仅仅是作为主体中的断言)?因为这就是C ++的设计方式。通常,关于构建什么编程语言的概念是“明确需要明确的内容”。
并不是说Haskell模块是开源的。这意味着我们只提供签名。因此,当我们写例如:
时a == b
我们经常在这里写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
类型类。因此,我们会得到很多错误,这些错误只能在编译时发现(或者如果Haskell在运行时是动态语言)。将Eq
放入类型签名的想法是我们可以提前查找Eq a
的签名,从而确保类型是类型类的实例。
请注意,您不必自己编写类型签名:Haskell通常可以派生函数的签名,但签名应包含所有必要的信息以便有效地调用和使用函数。通过添加类型约束,我们可以加快开发速度。
这是怎么回事?为什么我不能拥有两个具有相同名称但在不同类型上运行的函数。
再次:这就是Haskell的设计方式。函数式编程语言中的函数是“一等公民”。这意味着这些通常都有一个名称,我们希望尽可能避免名称冲突。就像C ++中的类通常具有唯一的名称(名称空间除外)。
假设你要定义两个不同的功能:
foo
然后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
和>>=
之间有一些合同),或者根本没有这些合同。
答案 2 :(得分:5)
这只回答问题1(直接,至少)。
类型签名/* Marking a state as visited */
Set<State> visited = new HashSet<>();
visited.put(currentState);
/* Checking if visited/retrieving */
if (visited.contains(currentState)) {
// already visited
} else {
// do something with 'currentState'
}
是f :: a -> a -> Bool
的简写。如果所有类型f :: forall a. a -> a -> Bool
仅适用于f
已定义a
的{{1}},a
将无法正常运行。对(==)
类型的限制是使用(==)
中的约束(Eq a)
表示的。
“For all”/通用量化是Haskell(参数)多态的核心,除其他外,还提供了parametricity的强大而重要的属性。
答案 3 :(得分:1)
Haskell坚持两个公理(其中包括):
如果你有
f :: A -> A
和
f :: B -> B
然后,根据Haskell采用的原则,f
仍然是一个有效的表达式,它本身仍然必须有单类型。虽然使用子类型可以做到这一点,但它被认为比类型级解决方案复杂得多。
同样,
中需要Eq a
(==) :: Eq a => a -> a -> Bool
来自==
的类型必须完全描述你可以用它做什么的事实。如果您只能在某些类型中调用它,则类型签名必须反映出来。