表达问题
表达问题是编程语言中一个具有挑战性的问题,它涉及静态键入数据抽象的扩展性和模块化。目的是定义一个数据抽象,该数据抽像在其表示形式和行为中都可以扩展,在该数据抽像中,人们可以在数据抽像中添加新的表示和新的行为,而无需重新编译现有的代码,并且在保留静态类型的同时(例如,没有铸件) 。该问题的陈述揭示了编程范式和编程语言中的缺陷,但截至2023年,尽管有许多建议的解决方案,但仍未解决。
历史
菲利普·沃德勒(Philip Wadler)提出了挑战,并将其命名为“表达问题”,以回应与赖斯大学的编程语言团队(PLT)的讨论。他还引用了三个来源,这些资料为他的挑战定义了背景:
该问题首先是约翰·雷诺兹(John Reynolds)在1975年观察到的。雷诺兹(Reynolds)讨论了两种形式的数据抽象:用户定义的类型,这些类型现在称为抽像数据类型(ADT)(不应与代数数据类型混淆)和程序数据结构,现在仅使用一种方法将其理解为一种原始的对象形式。他认为它们是互补的,因为可以通过新的行为扩展用户定义的类型,并且可以通过新表示形式扩展程序数据结构。他还讨论了可以追溯到1967年的相关工作。十五年后的1990年,威廉·库克(William Cook)在对象和抽像数据类型的背景下应用了雷诺的想法,这些想法都广泛地增长。库克确定了在数据抽像中隐含的表示和行为的矩阵,并讨论了ADT基于行为轴的方式,而对象基于表示轴。他对与问题相关的ADT和对象的工作进行了广泛的讨论。他还审查了两种样式的实现,讨论了两个方向的可扩展性,并确定了静态键入的重要性。最重要的是,他讨论了比雷诺所考虑的更灵活的情况,包括方法的内在化和优化。
在98年的Ecoop', Shriram Krishnamurthi等。提出了一个设计模式解决方案,以同时扩展面向表达式的编程语言及其工具集的问题。他们将其称为“表达性问题”,因为他们认为编程语言设计师可以使用该问题来展示其创作的表现力。对于PLT而言,这个问题已经出现在现为drracket的Drscheme的构建中,他们通过重新发现Mixins解决了问题。为了避免在有关编程语言的论文中使用编程语言问题,Krishnamurthi等人。使用旧的几何编程问题来解释其面向模式的解决方案。在Ecoop演示后与Felleisen和Krishnamurthi的对话中,Wadler理解了以PL为中心的问题,他指出,Krishnamurthi的解决方案使用了铸件来绕过Java的类型系统。讨论继续在类型的邮件列表中,Corky Cartwright(大米)和Kim Bruce (Williams)展示了OO语言类型的类型系统如何消除这种演员。作为回应,瓦德勒(Wadler)提出了他的论文,并指出了挑战:“一种语言能否解决表达问题是其表达能力的显著指标。”标签上的“表达式问题”在表达式=“您的语言express”和表达式=“您试图代表的术语是语言表达式”上的标签。
其他人则与莱斯大学的PLT,尤其是托马斯·库恩(ThomasKühne)的论文中的同时,在同一时间,在他的论文中进行了同时发现的变体,在平行的Ecoop 98文章中, Smaragdakis和batory的杂种。
一些后续工作使用了表达问题来展示编程语言设计的力量。
表达问题也是多维软件产品线设计中的基本问题,尤其是作为FOSD程序立方体的应用程序或特殊情况。
解决方案
表达问题有多种解决方案。每个解决方案都在用户必须编写的代码量以及所需的语言功能方面有所不同。
- 多次调度
- 红宝石语法§开放课程
- 函子的共同生物
- 类型类
- 无标记的最终 /对象代数
- 多态性变体
例子
问题描述
我们可以想像,我们没有以C#编写的以下库的源代码,我们希望扩展:
public interface IEvalExp
{
int Eval();
}
public class Lit: IEvalExp
{
public Lit(int n)
{
N = n;
}
public int N { get; }
public int Eval()
{
return N;
}
}
public class Add: IEvalExp
{
public Add(IEvalExp left, IEvalExp right)
{
Left = left;
Right = right;
}
public IEvalExp Left { get; }
public IEvalExp Right { get; }
public int Eval()
{
return Left.Eval() + Right.Eval();
}
}
public static class ExampleOne
{
public static IEvalExp AddOneAndTwo() => new Add(new Lit(1), new Lit(2));
public static int EvaluateTheSumOfOneAndTwo() => AddOneAndTwo().Eval();
}
使用此库,我们可以像ExampleOne.AddOneAndTwo()
那样表达算术表达式1 + 2
,并且可以通过调用.Eval()
来评估表达式。现在想像一下,我们希望扩展此库,添加新类型很容易,因为我们正在使用面向对象的编程语言。例如,我们可能会创建以下类:
public class Mult: IEvalExp
{
public Mult(IEvalExp left, IEvalExp right)
{
Left = left;
Right = right;
}
public IEvalExp Left { get; }
public IEvalExp Right { get; }
public int Eval()
{
return Left.Eval() * Right.Eval();
}
}
但是,如果我们希望在类型(C#术语中的新方法)上添加一个新功能,则必须更改IEvalExp
接口,然后修改实现接口的所有类。另一种可能性是创建一个扩展IEvalExp
接口的新接口,然后创建用于Lit
, Add
和Mult
类的子类型,但是返回的表达式ExampleOne.AddOneAndTwo()
已经编译了,因此我们将无法使用该表达式。旧类型的新功能。这个问题是用功能编程语言(如f#)逆转的,在给定类型上添加函数很容易,但是很难扩展或添加类型。
使用对象代数的问题解决方案
让我们使用群众的纸张可扩展性来重新设计原始库,并牢记可扩展性。
public interface ExpAlgebra<T>
{
T Lit(int n);
T Add(T left, T right);
}
public class ExpFactory: ExpAlgebra<IEvalExp>
{
public IEvalExp Lit(int n)
{
return new Lit(n);
}
public IEvalExp Add(IEvalExp left, IEvalExp right)
{
return new Add(left, right);
}
}
public static class ExampleTwo<T>
{
public static T AddOneToTwo(ExpAlgebra<T> ae) => ae.Add(ae.Lit(1), ae.Lit(2));
}
我们使用与第一个代码示例中相同的实现,但是现在添加一个新接口,其中包含该类型上的功能以及代数的工厂。请注意,我们现在使用Expalgebra <t>接口而不是直接从类型中使用ExpAlgebra<T>
接口在ExampleTwo.AddOneToTwo()
中生成表达式。现在,我们可以通过扩展ExpAlgebra<T>
接口来添加功能,我们将添加功能以打印表达式:
public interface IPrintExp: IEvalExp
{
string Print();
}
public class PrintableLit: Lit, IPrintExp
{
public PrintableLit(int n): base(n)
{
N = n;
}
public int N { get; }
public string Print()
{
return N.ToString();
}
}
public class PrintableAdd: Add, IPrintExp
{
public PrintableAdd(IPrintExp left, IPrintExp right): base(left, right)
{
Left = left;
Right = right;
}
public new IPrintExp Left { get; }
public new IPrintExp Right { get; }
public string Print()
{
return Left.Print() + " + " + Right.Print();
}
}
public class PrintFactory: ExpFactory, ExpAlgebra<IPrintExp>
{
public IPrintExp Add(IPrintExp left, IPrintExp right)
{
return new PrintableAdd(left, right);
}
public new IPrintExp Lit(int n)
{
return new PrintableLit(n);
}
}
public static class ExampleThree
{
public static int Evaluate() => ExampleTwo<IPrintExp>.AddOneToTwo(new PrintFactory()).Eval();
public static string Print() => ExampleTwo<IPrintExp>.AddOneToTwo(new PrintFactory()).Print();
}
请注意,在ExampleThree.Print()
中,我们正在打印一个已经在ExampleTwo
中编译的表达式,我们不需要修改任何现有代码。还请注意,这仍然是强烈键入的,我们不需要反射或铸造。如果我们将PrintFactory()
替换为ExampleThree.Print()
中的ExpFactory()
),我们会遇到汇编错误,因为在这种情况下不存在.Print()
方法。