【问题标题】:Typeclasses and overloading, what is the connection?类型类和重载,有什么联系?
【发布时间】:2017-12-25 19:12:33
【问题描述】:

我目前正试图围绕类型类和实例展开思考,但我还不太了解它们的意义。到目前为止,我对此事有两个问题:

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

【问题讨论】:

  • “只是抛出错误”在运行时正是静态类型语言试图避免的。拥有类型系统的一个要点是在编译时及早检测某些类型的错误。
  • 不是,我的意思是在编译的时候遇到未定义的函数就抛出错误。
  • 如果我写了f :: a -> a -> Bool,然后将其应用于无法比较相等的东西,比如f (1+) (1-),编译器应该如何检测到我们现在需要一个@987654330 @为了抛出错误而定义?答:编译器记录了一个关于f 的小事实,它将(==) 应用于其参数,以便它知道何时看到对f 的调用以检查(==) 是否可用。我们如何记下这个事实?答:我们把它放在类型中,为f :: Eq a => a -> a -> Bool
  • @lotolmencre 啊,也许我开始明白你的意思了。由于您习惯于 C++ 模板,因此您可能会感到困惑。 C++ 模板或多或少地为模板参数的每个实例化编译(包括类型检查)。 Haskell 的(有界)多态函数只编译一次,并且没有“模板实例化”(类似于 Java 的泛型)。在 C++ 中,您需要头文件来包含模板源。在 Haskell 中,编译后不需要源码。
  • 如果我必须进行强制比较,类型类与 C++20 概念的关系模糊,除了在 Haskell 中没有 SFINAE:函数是提前编译的(与模板不同),并且约束必须保证所有类型的函数定义都可以(提供约束)。

标签: haskell typeclass overloading


【解决方案1】:

我同意Willem Van Onsem’s answer 的大部分观点,但我认为它忽略了类型类相对于真正的临时重载的主要优势之一:抽象。想象一下,我们使用临时重载而不是类型类来定义 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 等价表示,@ 987654327@ 和 join。因此,我们应该能够实现一个适用于 any monad 的join 函数:

join x = x >>= id

但是,现在我们遇到了一个问题。 join的类型是什么?

很明显,join 必须是多态的,因为它在设计上适用于任何 monad。但是给它类型签名forall m a. m (m a) -> m a 显然是错误的,因为它并不适用于所有类型,只适用于一元类型。因此,我们需要在我们的类型中表达需要存在一些操作(>>=) :: m a -> (a -> m b) -> m b,这正是类型类约束提供的。

鉴于此,很明显,即席重载可以重载名称,但不可能对这些重载名称进行抽象,因为不能保证不同的实现以任何方式相关。你可以定义没有类型类的 monad,但是你不能定义 joinwhenunlessmapMsequence,以及你得到的所有其他好东西只需定义两个操作即可免费使用。

因此,Haskell 中需要类型类来实现代码重用并避免大量重复。但是你能both 类型类风格的重载和类型导向的临时名称重载吗? 是的,事实上,Idris 确实如此。但是 Idris 的类型推断与 Haskell 的非常不同,因此由于 Willem 的回答中的许多原因,支持它比在 Haskell 中更可行。

【讨论】:

  • 我们不能直接将>>= 明确传递给join 吗?我们将失去的只是隐式约束..
【解决方案2】:

简而言之:因为 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

我们经常会在这里用没有Eq 类型类的类型写foo。结果,我们会得到很多只在编译时发现的错误(或者如果 Haskell 是一种动态语言,在运行时)。将Eq a放入类型签名的想法是,我们可以提前查找foo的签名,从而确保类型是typeclass的实例。

请注意,您不必自己编写类型签名:Haskell 通常可以派生函数的签名,但签名应包含有效调用和使用函数所需的所有信息。通过添加类型约束,我们加快了开发速度。

这是怎么回事?为什么我不能有两个同名但在不同类型上运行的函数。

再说一遍:这就是 Haskell 的设计方式。函数式编程语言中的函数是“一等公民”。这意味着这些通常有一个名称,我们希望尽可能避免名称冲突。就像 C++ 中的类通常具有唯一的名称(命名空间除外)。

假设您将定义两个不同的函数:

incr :: Int -> Int
incr = (+1)

incr :: Bool -> Bool
incr _ = True

bar = incr

那么incr 必须选择哪个bar?当然我们可以明确类型(即incr :: Bool -&gt; Bool),但通常我们希望避免这种工作,因为它会引入很多噪音。

我们不这样做的另一个很好的原因是,通常类型类不仅仅是函数的集合:它向这些函数添加契约。例如Monad 类型类必须满足函数之间的某些关系。例如(&gt;&gt;= return) 应该与id 等价。换句话说,类型类:

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

没有描述两个独立函数(&gt;&gt;=)return:这是一组函数。您同时拥有它们(通常在特定的 &gt;&gt;=return 之间有一些合同),或者根本没有。

【讨论】:

  • “因为它就是这样设计的。”好的,当然,但为什么要这样设计?
  • @DanielWagner:这在答案的其余部分有争议。例如,明确需要明确的内容等。据我所知,并非所有理由都由设计师完全记录,如果有些理由是“故意的”或更多是“(幸运的)巧合”,则可能会造成混淆: )。
  • 但是这些合约不是由类型系统强制执行的,对吧?
  • @bob:究竟是​​什么合同?
  • 身份/关联性
【解决方案3】:

这仅回答问题 1(至少直接回答)。

类型签名f :: a -&gt; a -&gt; Boolf :: forall a. a -&gt; a -&gt; Bool 的简写。如果f 仅适用于定义了(==)as,则f 不会真正适用于所有 类型a。这种对具有(==) 的类型的限制是使用f :: forall a. (Eq a) =&gt; a -&gt; a -&gt; Bool 中的约束(Eq a) 来表达的。

“For all”/通用量化是 Haskell(参数)多态性的核心,除其他外,还提供了 parametricity 的强大而重要的属性。

【讨论】:

  • 啊,所以(Eq a) =&gt; a -&gt; a -&gt; Bool的意思是forall a. a in Eq =&gt; a -&gt; a -&gt; Bool
  • @lotolmencre 如果我的理解正确的话,大致意思是这样的(a 必须是“类型类”Eq 的“成员”)......有一点更重要的是,因为从技术上讲 (Eq a)Eq 对类型的(类型级)应用程序,但要获得约束。 Eq 接受一种类型并返回一个约束。这很重要,因为例如,某些类型类接受不止一种类型(您必须启用扩展才能做到这一点,但它是一个常见的扩展)。
  • 一种看待它的方式是单参数类型类是类型的谓词(所以Eq a =&gt; ... 表示“a 有一个Eq 的实例”)。这很好地扩展到了多参数类型类,因为它让您可以将它们视为类型之间的关系。这可能有点超前,但最好了解事情的发展方向。
【解决方案4】:

Haskell 坚持两个公理(除其他外):

  1. 每个变量都可以单独用作表达式;
  2. 每个表达式都有一个类型,精确地指定你可以用它做什么。

如果你有

f :: A -> A

f :: B -> B

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

同样需要Eq a in

(==) :: Eq a => a -> a -> Bool

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

【讨论】:

    猜你喜欢
    • 2017-02-07
    • 2021-12-18
    • 1970-01-01
    • 1970-01-01
    • 2019-07-14
    • 2011-07-05
    • 1970-01-01
    • 2015-10-24
    • 2010-10-02
    相关资源
    最近更新 更多