运行中的Codata,或者如何连接FP和OOP

2020-08-10 13:38:06

我的朋友Juan Paucar向我介绍了第28届欧洲编程研讨会(ESOP 2019)中与Codata相关的一篇论文。我对codata非常感兴趣,主要是因为它以一种范畴论的方式与数据相关,但是,像往常一样,我并不理解其中的许多单词。这篇论文帮助我理解了很多,也帮助我理解和塑造了如何连接FP和OOP世界的想法。但是,在此之前,我们必须对一些理论进行巡回演出。

数据,更好地理解为Haskell的数据声明,只不过是一种类型和一系列创建该类型的值的方法(函数)。我将使用GADT语法,因为它使函数可见。下面是一个例子:

Data Foo WHERE Foo1::int->;String->;Foo2::Bool->;Foo。

但是使用GADT语法,我们可以看到函数。Foo1是一个构造函数,但也是一个函数。Foo1接收一个Int和一个字符串,并构造一个foo类型的值。Foo2也发生了完全相同的情况。

我们刚才谈到了构造函数:构造函数只不过是一个从其他值产生值的函数。但这在很大程度上将每个函数定义为构造函数,这并没有什么帮助。我们将更进一步:构造函数是生成T类型的值的原语函数,而不是由生成T类型的值的其他函数构建的。此外,构造函数是您在编写数据声明时定义的函数。

如果有构造函数,就应该有拆分数据的东西,因为数据似乎是通过聚合值来构造的。这种解构数据的东西叫做消除器。Haskell中的通用消除符是case语句:

Case语句接受一个参数(放在case和of之间的参数)并返回某种类型的值。这就是为什么Case语句中的所有分支必须具有相同的类型,因为当您编写Case语句时,您编写的是MyCase::Foo->;X这样的函数,但是使用了更好的语法。

这意味着消除符并不总是以case语句的形式出现。实际上,消除器只是函数,Haskell为这些函数提供了特殊的语法,以便以更好的方式编写。但是,如果我们使用Lisp,例如,我们也有构造器和消除器。

在Lisp中,您可以通过使用构造函数cons(这就是为什么这些数据结构被称为cons-cell)来生成具有新头部的列表。Cons只是另一个带有两个参数的函数:要构造的列表的头部和尾部,并返回该列表。要分解cons列表,需要使用两个消除器:car和cdr。Car获取列表并返回头部,Cdr获取列表并返回尾部。

Haskell也提供了消除符,但是如果您过度使用它们,每个人都会皱起眉头:当您声明一个记录数据类型时,Haskell会很有帮助地创建函数来访问每个字段:

Data FooRecord=FooRecord{fooField1::int,fooField2::String}--Both--fooField1::FooRecord->;Int--fooField2::FooRecord->;String--现在存在。

当您使用这些访问器时,每个人都会皱起眉头,因为如果FooRecord有多个构造函数,而您对另一个构造函数使用了消除器,那么您的程序就会付之一炬。这与将Car和Cdr作为参数传递为空列表时爆炸成火焰没有什么不同。

所有这一切的要点是,数据是一种声明,它提供了一系列不同的方法来创建值,您的程序会选择您对当前案例感兴趣的一种方法。

范畴论是数学的一个分支,主要被haskler用来惹恼其他所有人。但是,信不信由你,对项目进行推理也很有用。在范畴论中,我们有对象,我们有相互连接对象的箭头。对于一个类别来说,它的对象和箭头必须遵守一些规则。

范畴理论的一个结构是共范畴。同义范畴是箭头颠倒的范畴。如果在类别X中,箭头从A到B,在类别co-X中,箭头从B到A。

在这一点上,您应该会怀疑";codata";。它看起来完全像";co-data&34;,是";data";的共同类别。但是,它实际上是什么样子的";数据";类别?

数据类别作为数据声明显示给我们。让我们再看一看foo的例子:

Data Foo WHERE Foo1::int->;String->;Foo2::Bool->;Foo。

在本例中(但不是在所有类别中),类型是对象,函数是箭头。如果我们稍微修改一下上面的代码,通过使用uncurrling,我们可以得到一种不那么像箭头的数据格式,并且更容易达到我们的目的:

现在,Foo1获取一对Int和String并返回Foo,而Foo2获取Bool并返回Foo。这几乎是每个haskler都知道的:除非同时提供Int和String,无论中间有多少个箭头,都不会有foo via Foo1。

Foo1(i,s)的案例myfoo->;如果Foo1 I s Foo2 b->;如果Foo2 b怎么办_Do_If_Foo1;What_To_Do_If_Foo2 b。

脚部消除器::(foo,((Int,String)->;a),(Bool->;a)->;a脚部消除器(myfoo,What_to_do_if_Foo1,What_to_do_if_Foo2)=case myfoo of Foo1(i,s)->;What_to_do_if_Foo1(i,s)Foo2 b->。

既然我们已经将case转换为普通的旧函数,我们就不需要手动放弃和魔术了,因为现在一切都是普通的旧函数组合和应用了。此外,所有haskeller即将抱怨的所有令人不快的事情都将对我们有所帮助。

我按摩了所有这些表情,然后把它们解开了,因为,否则会让人迷惑。我的意思是,共同范畴是一个箭头翻转的范畴。这就提出了一个问题:哪支箭?所有人?他们中的一些人?如果只有一个箭头,一切都会变得更容易:这意味着我们不必去找出是哪一个。

因此,让我们翻转Foo数据声明的箭头,看看会有什么结果。

--这不是真正的Haskell,在Haskell codata Foo中(尚不存在)`codata`,其中Foo1::(int,string)<;-Foo Foo2::Bool<;-Foo。

我很高兴地将数据重命名为Codata,并翻转了每行中的单箭头。让我们将参数的顺序颠倒一下,使箭头指向右侧,因为我们习惯于看到箭头是这样起作用的:

这是什么意思?嗯,同时有很多事情要做,但是让我们一个接一个地去吧。

我们有两个与codata foo相关的函数。第一个,称为Foo1,接受Foo并制造一对Int和String。第二个,叫做Foo2,它做了一个傻瓜,制造了一个布尔(Bool)。我们有两个函数,它们接受似乎是复合值foo,并从中提取Int、String和Bool类型的简单值。

就像我们可以调用data Foo声明中的两个函数中的任何一个一样,我们也可以调用codata Foo中的两个函数中的任何一个,并为第一种情况获得一个(Int,String),为第二种情况获得一个Bool。没有什么能强迫我们给两个都打电话,我们可以只给他们中的任何一个打电话。

如果我们有一个foo,通过调用foo1,我们可以从中获得简单的值,这看起来很像一个消除器。但我们也可以调用Foo2,它看起来像另一个消除器。

数据是关于如何调用合成值的许多不同构造函数中的任何一个。

CODATA是关于我们如何调用合成值的许多不同消除器中的任何一个。

Codata看起来很像对象和方法,其中codata值是对象,而消除器是方法。可能有一些定理可以将codata作为一个类别映射到OO程序中的对象。

这并不是说函数式编程优于面向对象编程。但是,通常的函数式程序员以前都是面向对象的程序员。正因为如此,他很可能知道来自OO的codata,现在开始理解来自FP的数据。因此,他比面向对象的程序员装备得更好,因为面向对象的程序员只知道codata。

如果我们找到一个纯粹的FP程序员和一个纯粹的OO程序员,他们可能会有类似的技能水平,但在完全不同的维度上。这将使得很难真正计算出谁是最熟练的,因为他们的技能组合不能与之相媲美!您如何将数据中的技能集与Codata中的技能集进行比较?

数据(和函数编程)涉及如何使用构造函数从简单数据类型构造复杂数据类型。Codata(和面向对象编程)涉及如何通过调用对象的方法从对象中提取组件。

表达式问题指出,您应该能够向数据类型添加新用例和新函数,而不会损失类型安全性,也不需要重新编译。

在面向对象语言中,您可以通过创建另一个子类并实现方法来添加新案例。但是,如果要向数据类型添加新函数,则必须将其添加到超类中,并在所有现有的子类上实现它,这意味着修改大量文件并重新编译大部分程序。

在FP语言中,只需添加一个函数,就可以将新函数添加到现有的数据类型中。但是,如果要添加新案例,则必须添加构造函数并修改所有使用该数据类型的函数,这意味着修改大量文件并重新编译大部分程序。

事实证明,有了我们新的数据协数据知识,我们对表达式问题有了更深的洞察力,以及为什么大多数解决方案感觉都是半途而废。Haskell中的类型类感觉就像螺栓固定在";中的&#OO,与C#扩展方法感觉像螺栓固定在";中的";函数一样。在一个数据为王的世界里,协同数据的东西让人感觉被锁住了;就像在数据为王的世界里一样,数据的东西感觉被锁住了。这两个类别是相反的,这意味着潜在的数学正在与我们想要添加的内容进行斗争。如果没有数学方面的合作,我们最多只能得到一个半途而废的结果。

如果我们不能用它做点什么,所有这些理论都将毫无用处(就像这篇论文一样)。结果,这篇论文展示了一些非常有趣的东西:在codata世界中实现数据的一种方法,以及在数据世界中实现codata的一种方法。这是一件很棒的事情,因为这意味着我们将来可能会有既支持数据又支持协同数据的编程语言!

我们可以实现的第一个转换是在函数式编程中创建对象。Haskell已经提出了一种实现它们的方法:类型类。当您读到有关类型类的内容时,文献开始抛出隐式传递的函数字典。这提供了一种实现对象的方法:作为对象的消除器(方法)的产物。因此:

Class{int fooInt;fooString;public(int x,y){fooInt=x;fooString=y;}public int getTheInt(){return fooInt;}public DoSomething(Extra){return fooString。Concat(fooInt.。ToString())。Concat(额外);}}。

Data foo=foo{getTheInt::int,DoSomething::string->;string}--构造函数mkFoo::int->;string->;foo mkFoo fooInt fooString=foo{getTheInt=fooInt,DoSomething=\Extra->;fooString<;>;show fooInt<;>;Extra}。

如果这不能用于继承,所有的面向对象人员都会抱怨,他们是对的,因为否则它就不能在表达式问题上发挥作用。因此,让我们扩展这两个示例:

类扩展{bool mfooBool;public(int x,y,bool z){Super(x,y);mfooBool=z;}public DoSomething(Extra){if(MfooBool){return";no&34;;}Else{return super.。做某事(额外);}。

MkModifiedFoo::int->;String->;Bool->;Foo mkModifiedFoo fooInt fooString fooBool=Foo{getTheInt=getTheInt Super,DoSomething=\Extra->;if mfooBool Then";Nope&34;Else DoSomething Super Extra}其中Super=mkFoo fooInt fooString

Haskell方法将属性存储在闭包中,而不是声明它们。我可以创建一个特定的对象来存储它们,但是我认为闭包已经足够好了。

奇怪的是,这两本书之间的翻译大多是机械化的,这也是论文作者用来将一本书机械地转换成另一本书的方法。

奇怪的是,在OOP语言中实现ADT有很多名字,但看起来很可怕,至少最初是这样。它有一个非常奇特的名字:教堂编码,还有一个不那么奇特的名字:访客模式,但它们几乎是一样的。此技术基于将case语句转换为对象,并将不同的构造函数转换为接受器类的实例。让我们看一个例子:

Data Foo WHERE Foo1::(int,String)->;Foo Foo2::Bool->;Foo FooEviinator:(Foo,((Int,String)->;a),(Bool->;a)->;a Foo消除符(myfoo,What_to_Do_if_Foo1,What_to_do_if_Foo2)=case Foomyfoo of Foo2(myfoo,What_to_do_if_Foo1,What_to_do_if_Foo2)=case myfoo of Foo2。如果Foo1(i,s)Foo2 b->;如果Foo2 b怎么办。

抽象类{public消除(<;>;fooEmininator);}//表示Foo1的最终类扩展{int i;s;public消除(<;>;fooEmininator){return fooEmininator。What_to_do_if_foo1(i,s);}}//表示Foo2最终类的类扩展{bool b;public消除(<;>;fooEmininator){return fooEmininator。What_to_do_if_Foo2(B);}}抽象类<;>;{public What_to_do_if_Foo1(int i,s);public What_to_do_if_Foo2(Bool B);}。

在Haskell示例中,What_to_do_if_foox是我们需要一起传递的两个函数,因为它们成为同一个case语句的一部分。在Java版本中,我们决定将它们一起作为FooEmininator的实例进行传递,因此可以一起传递,因为它们是同一对象的方法。

按照表达式问题的要求,向foo添加新操作在这两种实现中都非常容易。在Haskell中,我们只使用不同的参数调用fooEmininator:

对于Java的实现也很容易。我们派生出一种新型的FooEmininator,我们用它来命名为Foo.exemination:

类扩展<;>;{public What_to_do_if_Foo1(int i,s){return";某个不同的";。Concat(i.。ToString())。Conat(S);}public What_do_if_foo2(Bool B){return";一些不同的东西";。Concat(b.。ToString();}}myFoo=new(3,";asdf";);<;>;liminator=new();.out。Println(myfoo.。消除(消除器);

实际上,构造代数数据类型变体所需的值成为消除器方法的参数。

与前面的转换情况类似,这个转换几乎是机械的,这意味着我们将来可能会看到它在编程语言中自动完成。

像往常一样,这篇论文对类型论、范畴论和大量的数学符号进行了更多的研究,这使得它对我在这里写的东西更加准确。不幸的是,所有这些精确度和数学符号可能会让初学者不太容易理解。我希望这篇文章对初学者更友好一些,同时仍然传达了主要观点。

最后,我要感谢Paul Downen、Zachary Sullivan、Zena M.Ariola和Simon Peyton Jones写下的极具洞察力的论文,以及我的好朋友Juan Paucar,感谢他们发现并告诉我论文的存在,以及校对这篇文章。