类型和重载,连接是什么?

时间:2017-12-25 19:12:34

标签: haskell typeclass overloading

我目前正试图围绕类型类和实例进行思考,但我还不太明白它们的重点。到目前为止,我对此事有两个问题:

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在这里做事情的理论基础。

4 个答案:

答案 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}},fmappure。因此,我们应该能够实现一个适用于任何 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 bjoinwhenunlessmapM ,以及在定义两个操作时免费获得的所有其他好东西。

因此,在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坚持两个公理(其中包括):

  1. 每个变量本身都可用作表达式;
  2. 每个表达式都有一个精确指定允许使用它的类型。
  3. 如果你有

    f :: A -> A
    

    f :: B -> B
    

    然后,根据Haskell采用的原则,f仍然是一个有效的表达式,它本身仍然必须有类型。虽然使用子类型可以做到这一点,但它被认为比类型级解决方案复杂得多。

    同样,

    中需要Eq a
    (==) :: Eq a => a -> a -> Bool
    

    来自==的类型必须完全描述你可以用它做什么的事实。如果您只能在某些类型中调用它,则类型签名必须反映出来。