纯C语言的代数数据类型和类型自省

2020-06-19 00:02:26

唯一的依赖项是Boost/预处理器。如果您在类UNIX系统上,只需运行以下脚本:

由于poica是一个仅包含头文件的库,因此您可以随意将必要的文件复制到您的项目中,并使用#include<;poica.h>;导出其API(使用-i编译器选项)。就这些。

通常在C语言中,我们使用联合来告诉编译器我们将以不同的方式解释单个内存区域。为了决定如何解释并集,我们赋予它一个标记,并得到一个带标记的并集。

tyecif struct{enum{OUR_TAGGED_UNION_STATE_1,OUR_TAGGED_UNION_STATE_2,OUR_TAGGED_UNION_3,}STATE;UNION{INT STATE_1;CONST char*STATE_2;DOUBLE STATE_3;}DATA;}OurTaggedUnion;

更糟糕的是,这种方法是不安全的,这意味着我们可以构造无效的OurTaggedUnion(I),或者,例如,(Ii)当实际状态为OUR_TAGGED_UNION_STATE_3时,(Ii)访问data.state_1:

//(I)OurTaggedUnion res1={.。STATE=OUR_TAGGED_UNION_STATE_2,。数据。STATE_1=123};//(Ii)OurTaggedUnion res2={。STATE=OUR_TAGGED_UNION_STATE_3,。数据。STATE_3=。99};SOME_PROCEDURE(res2.data.state_1);

POICA通过引入代数数据类型(将在下一节讨论)解决了这两个问题。这就是POICA如何做到这一点的:

SUM(OurTaggedUnion,Variant(MkState1 Of Int)Variant(MkState2 of Const char*)Variant(MkState3 Of Double));//(I)编译失败!OurTaggedUnion res1=MkState2(123);OurTaggedUnion res2=MkState3(.。99);SOME_PROCEDURE(/*无法传递STATE_1!*/);

ADT(代数数据类型)提供了一种组合、析构和自省数据类型的便捷方法。它们主要有两种:总和型和产品型。

简单地说,SUM类型是T1、.、TN中的一个,而产品类型是T1、.、TN。SUM类型的另一个名称是标记并集,产品类型对应于C中的结构。

模式匹配是检查SUM类型的每个变量,如果匹配的变量是实际变量,则触发一些操作。它们类似于IF语句,但适用于SUM类型,而不适用于布尔表达式。

可以方便地表示为SUM类型,并使用模式匹配进一步操作。在下面的代码中,我们首先构造这个二叉树,然后将它的所有元素打印到stdout:

#include<;poica.h>;#include<;stdio.h>;sum(Tree,Variant(MkEmpty)Variant(MkLeaf Of Int)Variant(多个字段的MkNode(结构树左侧*)字段(Number Of Int)字段(结构树右侧*));void print_tree(Const Tree*tree){Match(Tree){case(MkEmpty){return;}case(MkLeaf。,*number);}case(MkNode,More(Left,Number,Right)){PRINT_TREE(*LEFT);printf(";%d\n";,*Number);PRINT_TREE(*RIGHT);}#定义树(树)obj(树的树)#定义节点(左,数,右)树(MkNode(左,数,右))#定义叶(数)树(MkLeaf(数))int main(Void){const Tree*tree=node(node(叶(81),456,node(叶(90),7,叶(111),57,叶(123);print_tree(Tree);}#undef tree#undef tree。

如果我们在C中有结构,为什么我们需要产品类型呢?嗯,因为产品类型提供了一些额外的特性,比如类型自检(在下一节中讨论)和字段提取。

它计算三角形的面积,如果三条边都已知的话。这可以通过产品类型实现,如下所示:

#include<;math.h>;#include<;stdio.h>;#include<;poica.h>;product(三角形,字段(A Of Double)字段(B Of Double)字段(C Of Double));DOUBLE COMPUTE_AREA(三角形){Extract((a,b,c)from(&;三角形of Triangle));const Double p=(a+b+c)/2;const Double Area=sqrt(p*(p-a)*(p-b)*(p-c));Return Area;}int main(Void){三角形三角形={4,13,15};printf(";%f\n";,computer_area(三角形));}

Extract只创建适当类型的新变量,并从三角形中为它们赋值。将以上代码与此版本的COMPUTE_AREA进行比较:

DOUBLE COMPUTE_AREA(三角形){CONST DOUBLE P=(三角形。A+三角形。B+三角形。c)/2;常数倍面积=sqrt(p*(p-三角形。a)*(p-三角形。b)*(p-三角形。c));返回区;}。

这就是字段提取可以使我们的代码更干净的方式。通常,当对单个变量的字段有大量重复访问时,字段提取是首选的,并且字段对应的变量是显而易见的。

支持类型自检,因为您可以在编译时查询ADT的类型属性,然后在手写宏中以某种方式处理它们。

#include<;poica.h>;#include<;stdio.h>;#include<;Boost/preprocessor.hpp>;#定义MY_SUM\SOURCE,\VARIANT(MKA)\VARIANT(int的MKB)\VARIANT(MKC of MAND字段(DOUBLE的c1)字段(CHAR的C2))\SUM(MY_SUM);#定义Something_Introspect SUM_Introspect(MY_SUM)int main(。

#include<;poica.h>;#include<;stdio.h>;#include<;Boost/preprocessor.hpp>;#定义MY_PRODUCT\Something,\field(A Of Int)\field(b of const char*)\field(C Of DOUBLE)\product(MY_PRODUCT);#Define Something_Introspect product_Introspect(MY_PRODUCT)int main(Void){put(BOOST_PP_STRING。

也就是说,关于类型的元信息实际上是Boost/PreProcessor术语中的一个序列。因此,可以进一步使用BOOST_PP_SEQ_*宏,以及POICA中的其他实用程序宏。

通过类型自检,可以实现类型驱动(反)序列化、声明性命令行参数解析、修饰器模式等。

ADT提供了一种安全、一致的错误处理方法。可能失败的过程返回一个SUM类型,指定一个成功或失败的值,如下所示:

SUM(RecvMsgErrKind,Variant(MkBadConnection)Variant(MkNoSuchUser).);Sum(RecvMsgRes,Variant(MkRecvMsgOk of char*)Variant(RecvMsgErrKind的MkSendMsgErr);RecvMsgRes recv_msg(.){.}。

然后,可以匹配RecvMsgRes以决定在MkRecvMsgOk和MkSendMsgErr的情况下执行什么操作:

RecvMsgRes res=recv_msg(.);Match(&;res){case(MkRecvMsgOk,msg){.}case(MkSendMsgErr,Err_Kind){.}}。

可读性。像MkRecvMsgOk和MkSendMsgErr这样的标识符更适合人类,因此,很难将它们相互混淆。与此相反,在C中确定错误的通常方法是使用幻数范围(例如,<;0或-1)。

一致性。不需要发明不同的策略来处理不同类型的错误(例如,对不太可能的错误使用异常,对正常控制流使用int代码,等等);ADT通常解决错误处理的问题。

穷尽性检查(案例分析)。智能编译器和静态分析工具可确保RecvMsgRes的所有变体都得到匹配处理,因此我们不能忘记处理错误,让应用程序在没有错误的情况下工作,从而避免可能出现的严重错误。

ADT甚至比异常更有优势:它们不使用程序堆栈执行转换,因为它们只是值,没有任何可能影响性能的隐式逻辑。

ADT在实际编程中有大量的应用。例如,将令牌编码为SUM类型、将消息从一个进程传递到另一个进程、计算AST是非常自然的。

答:POICA没有运行时,也不执行系统调用,宏扩展为纯C代码。以下是示例的反汇编版本:

答:到目前为止,我发现的唯一陷阱是可怕的宏观错误,描述的是后果,而不是原因。但是,在用户代码中,只需仔细查看触发错误的行号就可以解决这些问题。