Haskell 的类型系统是否遵循 Liskov 替换原则?

Does Haskell's type system honor the Liskov Substitution Principle?

我来自 Java 背景,我正在努力了解 Haskell 的类型系统。 在 Java 世界中,Liskov 替换原则是基本规则之一,我试图了解是否(如果是,如何)这也是适用于 Haskell 的概念(请原谅我对Haskell的有限理解,我希望这个问题能说得通。

例如,在Java中,公共基础class Object定义了方法boolean equals(Object obj),因此被所有Java [=51]继承=]es 并允许进行如下比较:

        String hello = "Hello";
        String world = "World";
        Integer three = 3;

        Boolean a = hello.equals(world);
        Boolean b = world.equals("World");
        Boolean c = three.equals(5);

不幸的是,由于 Liskov 替换原则,Java 中的子 class 在接受的方法参数方面不能比基 class 更具限制性,因此 Java 还允许进行一些永远不可能为真的无意义比较(并且可能导致非常微妙的错误):

        Boolean d = "Hello".equals(5);
        Boolean e = three.equals(hello);

另一个不幸的副作用是,正如 Josh Bloch 很久以前在 Effective Java 中指出的那样,实现 equals 方法在存在子类型的情况下根据其合同正确执行(如果在子class 中引入其他字段,该实现将违反合同的对称性and/or 传递性要求)。

现在,Haskell的Eq类型class是完全不同的动物:

Prelude> data Person = Person { firstName :: String, lastName :: String } deriving (Eq)
Prelude> joshua = Person { firstName = "Joshua", lastName = "Bloch"}
Prelude> james = Person { firstName = "James", lastName = "Gosling"}
Prelude> james == james
True
Prelude> james == joshua
False
Prelude> james /= joshua
True

在这里,不同类型的对象之间的比较被拒绝并出现错误:

Prelude> data PersonPlusAge = PersonPlusAge { firstName :: String, lastName :: String, age :: Int } deriving (Eq)
Prelude> james65 = PersonPlusAge {  firstName = "James", lastName = "Gosling", age = 65}
Prelude> james65 == james65
True
Prelude> james65 == james

<interactive>:49:12: error:
    • Couldn't match expected type ‘PersonPlusAge’
                  with actual type ‘Person’
    • In the second argument of ‘(==)’, namely ‘james’
      In the expression: james65 == james
      In an equation for ‘it’: it = james65 == james
Prelude>

虽然这个错误在直觉上比 Java 处理相等性的方式更有意义,但它似乎确实表明像 Eq 这样的类型 class 可以 在允许子类型方法的参数类型方面更加严格。在我看来,这似乎违反了 LSP。

我的理解是Haskell不支持面向对象意义上的“子类型化”,但这是否也意味着里氏替换原则不适用?

tl;dr: Haskell 的类型系统不仅 尊重 Liskov 替换原则 – 它 强制它!


Now, Haskell's Eq type class is a completely different animal

是的,一般来说 classes 是与 OO classes 完全不同的动物(或元动物?)。 Liskov 替换原则是关于 subclasses 作为 subtypes。所以首先 a class 需要定义一个类型,OO classes 做的(即使是抽象的/接口,对于那些值 必须 是在子 class) 中。但是 Haskell classes 根本不会做这样的事情!您不能有“class Eq 的值”。你实际拥有的是某种类型的值,那个类型可能是Eqclass的一个实例。因此,class 语义完全脱离了值语义。

让我们将该段也表述为并排比较:

  • OO: class包含
    • 值(更好地称为 对象
    • 子classes(其值也是父class的值)
  • Haskell: class包含
    • 类型(称为 class 的 实例
    • subclasses(其实例也是父实例 class)

请注意 Haskell class 的描述甚至没有以任何方式提及值。 (事实上​​ ,您可以拥有 class 完全没有与任何运行时值相关的方法的实体!)

所以,现在我们已经确定 Haskell 中的 subclassing 与 subclass 的 值无关,很明显,在那个层面上什至无法表述里氏原理。您可以 为类型制定类似的东西:

  • 如果 DC 的子 class,那么作为 D 实例的任何类型也可以用作 [=18= 的实例]

– 这是绝对正确的,尽管没有真正谈论过;实际上,编译器会强制执行此操作。它包含的是

  • 为了为您输入 T 写一个 instance Ord T,您还必须先写一个 instance Eq T(当然,它本身同样有效,无论Ord 实例是否也被定义)
  • 如果约束 Ord a 出现在函数的签名中,则该函数也可以自动假定类型 a 也有一个有效的 Eq 实例。

对于 Haskell.

Liskov 的问题,这可能不是一个真正有趣的答案

这里有一些让它变得更有趣的东西。我说过 Haskell 没有子类型吗?好吧,实际上它确实如此!不是普通的旧 Haskell98 类型,而是 通用量化类型 .

不要恐慌我会尝试用一个例子来解释这是什么:

{-# LANGUAGE RankNTypes, UnicodeSyntax #-}

type T = ∀ a . Ord a => a -> a -> Bool
type S = ∀ a . Eq a => a -> a -> Bool

声明:ST 的子类型。

– 如果您一直在关注,那么此时您可能正在思考 等等,等等,这是错误的方式。毕竟EqOrdsuperclass,不是class的subclass。
但是不是, S是子类型!

示范:

x :: S
x a b = a==b

y :: T
y = x

反过来是不可能的:

y' :: T
y' a b = a>b

x' :: S
x' = y'
error:
    • Could not deduce (Ord a) arising from a use of ‘y'’
      from the context: Eq a
        bound by the type signature for:
                   x' :: S
      Possible fix:
        add (Ord a) to the context of
          the type signature for:
            x' :: S
    • In the expression: y'
      In an equation for ‘x'’: x' = y'

正确解释 Rank-2 类型/通用量化在这里会有点过头,但我的观点是:Haskell 确实允许一种子类型化,对于它,Liskov 替换原则仅仅是编译器强制执行的必然结果typeclasses 中类型的“LSP”。

如果你问我的话,这相当整洁。

我们在Haskell中不称值为“对象”;对象对我们来说是 something different,这就是为什么我在 post. 中避免使用“对象”一词