在现实世界应用中,基于模式匹配的逻辑是什么样子的?

5
我偶然发现了以下文章(在阅读PEP-3119时发现,但问题并不特定于语言)。 强调是我的。
特别是,经常需要以创建对象类的创作者未预料到的方式处理对象。 并不总是最好的解决方案将满足每个可能的对象用户需求的方法内置到每个对象中。 此外,有许多强大的分派哲学与经典OOP要求严格封装在对象内部的行为形成对比,例如基于规则或模式匹配驱动的逻辑。
我熟悉OOP:结构化代码围绕镜像概念或现实世界实体的对象,封装状态,并可以通过方法进行操作。
基于规则或模式匹配驱动的逻辑如何工作? 它是什么样子的?
现实世界的例子(也许在Web应用程序后端领域)将非常赞赏。 这里是一个相应的OOP示例。

3
或许?https://zh.wikipedia.org/wiki/%E6%A8%A1%E5%BC%8F%E5%8C%B9%E9%85%8D - Jordi Castilla
除了维基百科文章中的抽象一行示例之外,还有其他示例吗?我确保查找了该术语,但在这里提问是因为我没有找到答案。 :) - Anton Strogonoff
1
我在跌跌撞撞中到达这里,就像你强调的那一行一样 :) - iulian
1个回答

2
我相信PEP-3119这篇文章描述了表达式问题的解决方案。他们所描述的解决方案是抽象基类
为了理解抽象基类,首先有必要阐明抽象实体和具体实体之间的区别。抽象实体没有实现,而具体实体有实现。面向对象编程中的实体通常是属性或方法。
面向对象编程语言中的是一组具体实体。一些面向对象编程语言还有接口,它们是一组抽象实体。抽象基类是一个混合实体的集合。默认情况下,所有实体都是抽象的,但如果需要,它们可以通过提供默认实现来变为具体的,并且可以在需要时被覆盖。
Java中抽象基类的一个示例(如有错误请指正):
abstract class Equals<T> {
    public boolean equals(T x) {
        return !notEquals(x);
    }

    public boolean notEquals(T x) {
        return !equals(x);
    }
}

class Person extends Equals<Person> {
    public firstname;
    public lastname;

    public Person(String firstname, String lastname) {
        this.firstname = firstname;
        this.lastname  = lastname;
    }

    public boolean equals(Person x) {
        return x.firstname == firstname &&
               x.lastname  == lastname;
    }
}

无论如何,接下来是表达式问题。Philip Wadler 对此有以下看法:

表达式问题是一个旧问题的新名字。目标是通过案例定义数据类型,其中可以向数据类型添加新案例和新函数,而无需重新编译现有代码,并保持静态类型安全性(例如,没有强制转换)。

表达式问题涉及将所有数据类型分解为可管理的部分,同时仍允许任意扩展数据类型。数据类型可以被视为一个二维矩阵,包含案例和函数。例如,考虑 Document 数据类型:
            Text       Drawing   Spreadsheet
        +-----------+-----------+-----------+
draw()  |           |           |           |
        +-----------+-----------+-----------+
load()  |           |           |           |
        +-----------+-----------+-----------+
save()  |           |           |           |
        +-----------+-----------+-----------+

“Document”数据类型有三种情况(“Text”,“Drawing”和“Spreadsheet”)和三个函数(“draw”,“load”和“save”)。因此,它已被切成九个部分,可以在面向对象语言如Java中实现,具体如下:
public interface Document {
    void draw();
    void load();
    void save();
}

public class TextDocument implements Document {
    public void draw() { /* draw text doc... */ }
    public void load() { /* load text doc... */ }
    public void save() { /* save text doc... */ }
}

public class DrawingDocument implements Document {
    public void draw() { /* draw drawing... */ }
    public void load() { /* load drawing... */ }
    public void save() { /* save drawing... */ }
}

public class SpreadsheetDocument implements Document {
    public void draw() { /* draw spreadsheet... */ }
    public void load() { /* load spreadsheet... */ }
    public void save() { /* save spreadsheet... */ }
}

所以我们已经将“文档”数据类型切成了九个可管理的部分。然而,我们选择先将数据类型切成函数,然后再按情况切割它。因此,添加新情况很容易(我们只需创建一个实现“文档”接口的新类)。但是,我们无法向接口添加新函数。因此,我们的数据类型不是完全可扩展的。
然而,面向对象的方法并不是切割和切块数据类型的唯一方法。正如您强调的文本所说,还有另一种方法:
特别是,通常需要以未被对象类的创建者预期的方式处理对象。在每个对象中构建满足每个可能的用户需求的方法并不总是最好的解决方案。此外,“有许多强大的分派哲学与经典的OOP要求行为严格封装在对象内相对立,例如基于规则或模式匹配的逻辑”。
在面向对象的方式中,行为严格封装在一个对象内(即每个类实现一组方法,在上面的示例中,是相同的一组方法)。另一种选择是规则或模式匹配驱动的逻辑,其中数据类型首先按情况进行切片,然后再分解成函数。例如,在OCaml中:
type document
  = Text
  | Drawing
  | Spreadsheet

fun draw (Text)        = (* draw text doc... *)
  | draw (Drawing)     = (* draw drawing doc... *)
  | draw (Spreadsheet) = (* draw spreadsheet... *)

fun load (Text)        = (* load text doc... *)
  | load (Drawing)     = (* load drawing doc... *)
  | load (Spreadsheet) = (* load spreadsheet... *)

fun save (Text)        = (* save text doc... *)
  | save (Drawing)     = (* save drawing doc... *)
  | save (Spreadsheet) = (* save spreadsheet... *)

再次,我们已将Document数据类型切分成了九个可管理的部分。然而,我们首先按情况对数据类型进行了切分,然后将其切成了函数。因此,添加新函数很容易,但无法添加新情况。因此,该数据类型仍然不是完全可扩展的。
这就是表达式问题。如果我们首先将数据类型切成函数,则很容易添加新情况,但很难添加新函数。如果我们首先按情况切分数据类型,则很容易添加新函数,但很难添加新情况。
表达式问题的出现是由于需要扩展数据类型的固有需求。如果数据类型永远不需要扩展,则可以使用这两种方法中的任何一种(我将其称为面向对象方法和函数式方法)。但是,对于大多数实际目的,数据类型确实需要扩展。
如果您只需要通过添加新情况来扩展数据类型,则面向对象的方法很好(例如,在图形用户界面中,操作通常保持不变,但可能会添加新的视觉元素)。如果您只需要通过添加新函数来扩展数据类型,则函数式方法很好(例如,几乎所有我能想到的通用程序)。
现在,如果需要通过添加新的情况和新的函数来扩展数据类型,那将会是一个问题。然而,在像JavaScript和Python这样的动态语言中,可以使用“inspection”(PEP-3119文章使用的词)来实现。唯一的问题是,因为它是一种动态解决方案,编译器无法保证您已经实现了数据类型的所有部分,如果您回到表达式问题的定义,最后一个子句是“同时保持静态类型安全性”。因此,动态语言仍然无法解决表达式问题。
无论如何,PEP-3119文章都谈到了“invocation”和“inspection”作为选择数据类型的一部分的手段。调用更受欢迎,因为如果可以调用函数,那么也意味着它已经被实现。检查是一种动态解决方案,因此不总是正确的。
如果你想知道抽象基类如何解决表达式问题,我建议你阅读PEP-3119文章的其余部分。关于表达式问题的更多信息,我建议你阅读Bob Nystrom在“Solving the Expression Problem”博客文章中的内容。

1
谢谢你的全面回答,Aadit。虽然说实话,目前还不清楚在PEP-3119文档中,作者是否指的是你在这里描述的“表达式问题”。不过,这真是一个很好的回答。 - iulian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接