【问题标题】:How to make deeply nested function call polymorphic?如何使深度嵌套的函数调用多态?
【发布时间】:2021-06-05 05:18:48
【问题描述】:

所以我有一种自定义编程语言,并且我正在其中进行一些数学形式化/建模。在这种情况下,我基本上是这样做的(伪javascript表示):

isIntersection([1, 2, 3], [1, 2], [2, 3]) // => true
isIntersection([1, 2, 3], [1, 2, 3], [3, 4, 5]) // => false

function isIntersection(setTest, setA, setB) {
  i = 0
  while (i < setTest.length) {
    let t = setTest[i]
    if (includes(setA, t) || includes(setB, t)) {
      i++
    } else {
      return false
    }
  }
  return true
}

function includes(set, element) {
  for (x in set) {
    if (isEqual(element, x)) {
      return true
    }
  }
  return false
}

function isEqual(a, b) {
  if (a is Set && b is Set) {
    return isSetEqual(a, b)
  } else if (a is X... && b is X...) {
    return isX...Equal(a, b)
  } ... {
    ...
  }
}

function isSetEqual(a, b) {
  i = 0
  while (i < a.length) {
    let x = a[i]
    let y = b[i]
    if (!isEqual(x, y)) {
      return false
    }
    i++
  }
  return true
}

isIntersection 正在检查isEqualisEqual 被配置为能够处理各种相等检查情况,从集合比较集合、对象到对象、X 到 X 等。

问题是,我们怎样才能让isEqual 不知何故对实现细节一无所知?现在,您必须为每种可能的对象类型设置一个大的 if/else/switch 语句。如果我们添加一个新类型,我们必须修改这个巨大的isEqual 方法来添加对它的支持。我们如何才能避免这种情况,而只需分别明确地定义它们?

我最初想用类方法使对象成为“类的实例”。但我喜欢让一切都只是函数和结构(没有方法的对象)的纯粹性。有没有什么方法可以在不使用带有方法的类的情况下实现这种事情,而只保留函数和对象?

如果不是,那么您将如何使用类来实现它?会是这样吗?

class Set {
  isEqual(set) {
    i = 0
    while (i < this.length) {
      let x = this[i]
      let y = set[i]
      if (!x.isEqual(y)) {
        return false
      }
      i++
    }
    return true
  }
}

这意味着每个对象都必须定义一个isEqual。 Haskell 如何处理这样的系统?基本上是在寻找如何最干净地完成这项工作的灵感。理想情况下,我希望避免使用带有方法的类。

注意:您不能只委托给 == 本地实现(就像假设这是在 JavaScript 中一样)。我们使用的是自定义编程语言,基本上是首先尝试定义== 的含义。

另一种方法是以某种方式传递isEqual 函数以及所有内容,尽管我真的不知道如何做到这一点,如果可能的话,它会很笨重。所以不确定最好的方法是什么。

【问题讨论】:

  • 在 Haskell 中你有参数化,即你必须统一对待每一种类型。尽管参数化带有很好的属性,但它限制了你可以对类型做什么,因为你对它一无所知。类型类是一种让您了解类型或其在某些方面的行为方式的知识的方法。相等形成了这样一个类型类。我既不是类型理论家,也不是精通 Haskell,但我认为类型类是类型级别的第一个类名重载。这都是 Hindley-Milner 类型系统特有的,但可能对您没有多大帮助。

标签: oop functional-programming refactoring


【解决方案1】:

Haskell 利用其类型和类型类系统来处理多态相等性。

相关代码是

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

英文翻译是:a 类型实现了Eq 类当且仅当它定义了一个函数(==),该函数接受两个a 类型的输入并输出一个Bool

通常,我们声明类型类应该遵守的某些“法则”。例如,x == y 在所有情况下都应该与y == x 相同,而x == x 绝不应该是False。编译器无法检查这些规律,因此通常只需将它们写入文档即可。

一旦我们以上述方式定义了类型类Eq,我们就可以访问(==) 函数(可以使用中缀表示法调用它——即,我们可以编写(==) x yx == y)。这个函数的类型是

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

换句话说,对于实现类型类Eq的每个a(==)的类型都是a -&gt; a -&gt; Bool

考虑一个示例类型

data Boring = Dull | Uninteresting

Boring 类型有两个正确的值,DullUninteresting。我们可以如下定义Eq的实现:

instance Eq Boring where
   Dull == Dull                   = True
   Dull == Uninteresting          = False
   Uninteresting == Uninteresting = True
   Uninteresting == Dull          = False

现在,我们将能够评估 Boring 类型的两个元素是否相等。

ghci> Dull == Dull
True
ghci> Dull == Uninteresting
False

请注意,这与 Javascript 的平等概念非常不同。无法使用(==) 比较不同类型的元素。例如,

ghci> Dull == 'w'
<interactive>:146:9: error:
    * Couldn't match expected type `Boring' with actual type `Char'
    * In the second argument of `(==)', namely 'w'
      In the expression: Dull == 'w'
      In an equation for `it': it = Dull == 'w'

当我们尝试将Dull 与字符'w' 进行比较时,会出现类型错误,因为BoringChar 是不同的类型。

我们可以这样定义

includes :: Eq a => [a] -> a -> Bool
includes [] _           = False
includes (x:xs) element = element == x || includes xs element

我们将这个定义解读如下:

includes 是一个函数,对于任何实现相等性测试的a 类型,它接受as 和单个a 的列表并检查该元素是否在列表中。

如果列表为空,则includes list element 将评估为False

如果列表不为空,我们将列表写为x : xs(第一个元素为x,其余元素为xs的列表)。然后x:xs 包含element 且当x 等于elementxs 包含element

我们也可以定义

instance Eq a => Eq [a] where
   []     == []     = True
   []     == (_:_)  = False
   (_:_)  == []     = False
   (x:xs) == (y:ys) = x == y && xs == ys

这段代码的英文翻译是:

考虑任何类型a,这样a 实现Eq 类(换句话说,(==) 是为类型a 定义的)。那么[a] 也实现了Eq 类型类——也就是说,我们可以在[a] 类型的两个值上使用(==)

[a]实现typeclass的方式如下:

空列表等于它自己。

空列表不等于非空列表。

要判断两个非空列表(x:xs)(y:ys) 是否相等,请检查它们的第一个元素是否相等(也就是x == y 是否相等)。如果第一个元素相等,则递归检查其余元素是否相等(是否xs == ys)。如果这两个都为真,则两个列表相等。否则,它们不相等。

请注意,在Eq [a] 的实现中,我们实际上使用了两个不同的==s。相等x == y 使用Eq a 实例,而相等xs == ys 递归使用Eq [a] 实例。

实际上,定义Eq 实例通常非常简单,Haskell 让编译器完成这项工作。例如,如果我们改为写

data Boring = Dull | Uninteresting deriving (Eq)

Haskell 会自动为我们生成 Eq Boring 实例。 Haskell 还允许我们派生其他类型类,例如 Ord(其中定义了函数 (&lt;)(&gt;))、show(允许我们将数据转换为 Strings)和 read(这允许我们将Strings 转换回我们的数据类型)。

请记住,这种方法严重依赖静态类型和类型检查。 Haskell 确保我们在比较相同类型的元素时只使用(==) 函数。编译器在编译类型时也总是知道在任何给定情况下使用 (==) 的哪个定义,因为它知道要比较的值的类型,因此不需要进行任何类型的动态调度(尽管在某些情况下编译器会选择做动态调度)。

如果您的语言使用动态类型,则此方法将不起作用,并且如果您希望能够定义新类型,您将不得不使用某种动态分派。如果你使用静态类型,你绝对应该研究一下 Haskell 的类型类系统。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-11-17
    • 2019-04-03
    • 1970-01-01
    • 1970-01-01
    • 2020-02-13
    • 2020-01-10
    • 2019-12-14
    • 2020-07-24
    相关资源
    最近更新 更多