python的代数数据类型
algebraic-data-types的Python项目详细描述
adt![circleci](https://warehouse-camo.cmh1.psfhosted.org/c483dd334350010049c1bcd41c926f87132eccc6/687470733a2f2f636972636c563692e66f6d2f67682f6a737061687273756d66572732f6164742e7376673f7374796c653d32335323433133363336623564663393264376972636c652d746e333662306231323864643439)
adt
是一个库,在python中提供代数数据类型,语法清晰直观,并支持通过amypy plugin输入mypy plugin
目录:
什么是代数数据类型?
代数数据类型(也称为ADT)是一种表示单个类型的多个变体的方法,每个变体都可以有一些与之相关的数据。这个想法非常类似于标记的联合和和和类型,在python中表示为枚举。
ADT对于各种数据结构都很有用,包括二进制树:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]
抽象语法树(就像您可以作为解析器、编译器或解释器的一部分实现):
@adtclassExpression:LITERAL:Case[float]UNARY_MINUS:Case["Expression"]ADD:Case["Expression","Expression"]MINUS:Case["Expression","Expression"]MULTIPLY:Case["Expression","Expression"]DIVIDE:Case["Expression","Expression"]
或变体类型的更一般的版本,如表示类型A或类型B的或
类型,但不能同时表示这两种类型:
L=TypeVar('L')R=TypeVar('R')@adtclassEither(Generic[L,R]):LEFT=Case[L]RIGHT=Case[R]
模式匹配
现在,定义一个类型本身并不那么有趣。当您在ADT上进行模式匹配(有时称为"解构")时,ADT的许多表现性就产生了。
例如,我们可以使用上面的adt来实现一种错误处理:
# Defined in some other module, perhapsdefsome_operation()->Either[Exception,int]# Run some_operation, and handle the success or failuredefault_value=5returnsome_operation().match(# In this case, we're going to ignore any exception we receiveleft=lambdaex:default_value,right=lambdaresult:result)
旁白:这与错误处理在语言中的实现方式非常相似,如haskell,因为它避免了引发和捕获异常的不可预测的控制流,并确保呼叫者需要明确地决定在错误情况下要做什么。
对于上面的表达式
类型,也可以执行同样的操作(只需要匹配更多的情况):
e:Expressionresult=e.match(literal=lambdan:...,unary_minus=lambdaexpr:...,add=lambdalhs,rhs:...,minus=lambdalhs,rhs:...,multiply=lambdalhs,rhs:...,divide=lambdalhs,rhs:...)
与枚举相比
adt与python标准库中的enum
s有些相似(事实上,大写的命名约定故意相似)。
例如,枚举
版本的表达式
可能看起来像:
classExpression(Enum):LITERAL=auto()UNARY_MINUS=auto()ADD=auto()MINUS=auto()MULTIPLY=auto()DIVIDE=auto()
但是,这不允许数据与这些枚举值中的每一个相关联。表达式的特定值将告诉您存在的表达式的类型,但表达式的操作数仍必须存储在其他视图中这里,
从这个角度来看,adt类似于enum
s,可以有选择地将数据与每个案例关联起来。
与继承相比
代数数据类型是面向对象编程语言的一个相对较新的介绍,原因很简单,继承可以复制相同的行为。
继续我们的示例,使用表达式
adt,下面介绍如何用python中的继承来表示它:
classExpression(ABC):passclassLiteralExpression(Expression):def__init__(self,value:float):passclassUnaryMinusExpression(Expression):def__init__(self,inner:Expression):passclassAddExpression(Expression):def__init__(self,lhs:Expression,rhs:Expression):passclassMinusExpression(Expression):def__init__(self,lhs:Expression,rhs:Expression):passclassMultiplyExpression(Expression):def__init__(self,lhs:Expression,rhs:Expression):passclassDivideExpression(Expression):def__init__(self,lhs:Expression,rhs:Expression):pass
这显然更加冗长,使用这些类型的代码也变得更加复杂:
e:Expressionifisinstance(e,LiteralExpression):result=# do something with e.valueelifisinstance(e,UnaryMinusExpression):result=# do something with e.innerelifisinstance(e,AddExpression):result=# do something with e.lhs and e.rhselifisinstance(e,MinusExpression):result=# do something with e.lhs and e.rhselifisinstance(e,MultiplyExpression):result=# do something with e.lhs and e.rhselifisinstance(e,DivideExpression):result=# do something with e.lhs and e.rhselse:raiseValueError(f'Unexpected type of expression: {e}')
ADT提供了一种简单的方法来定义一种类型,它是一组可能的案例中的一个,并且允许数据与每个案例关联,并与之打包/解包。
其他编程语言中的示例
代数数据类型在函数式编程语言中非常常见,如haskell或scala,但它们也越来越被"主流"编程语言所接受。
下面是几个例子。
生锈
rustenum
s实际上是成熟的adt。以下是如何定义或
ADT的方法:
enumEither<L,R>{Left(L),Right(R),}
雨燕
swift枚举与rust非常相似,通过支持"关联值",它们的行为类似于代数数据类型。
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]0
类型脚本
typescript通过一种称为"discriminated unions"的语言功能提供ADT
从他们的文档中可以看到这个示例:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]1
安装
要在python项目中添加adt
作为库,只需运行pip
(或pip3
,因为它可能在您的系统中命名):
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]2
这将安装来自pypi的最新版本。
mypy插件
该库还附带了一个用于mypy的插件,该插件允许对@adt
定义进行类型检查。如果您已经在使用mypy,则需要该插件以避免无意义的类型错误。
要启用adt
类型检查插件,请将以下内容添加到项目工作目录中的mypy.ini
文件中:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]3
定义ADT
要开始定义自己的数据类型,请导入@adt
decorator和case[…]
注释:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]4
然后,定义一个新的python类,在该类上应用@adt
decorator:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]5
对于ADT将具有的每个案例(变量),使用case
注释声明一个字段。用所有的大写名称声明字段是传统的做法,但唯一真正的限制是它们不能是小写的。
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]6
如果要将某些数据与特定案例关联,请在case
之后的括号中列出该数据的类型(类似于generic[…]
和tuple[…]
来自typing
的注释)。例如,要添加带有关联字符串的案例:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]7
只要列出所有类型,就可以用任意多个关联的数据片段构建案例:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]8
ADT也可以是递归的,也就是说,ADT本身可以存储在一个特定的case旁边,尽管类名必须用双引号提供(这个限制也可以是适用于键入
)。
递归adt的一个典型例子是链表。在这里,列表也被设置为泛型,覆盖类型t
:
@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]9
有关完整ADT定义的更多示例,请参见库中的测试。
生成的功能
给定ADT定义如下:
@adtclassExpression:LITERAL:Case[float]UNARY_MINUS:Case["Expression"]ADD:Case["Expression","Expression"]MINUS:Case["Expression","Expression"]MULTIPLY:Case["Expression","Expression"]DIVIDE:Case["Expression","Expression"]0
@adt
装饰程序将自动生成以下形式的访问器方法:
@adtclassExpression:LITERAL:Case[float]UNARY_MINUS:Case["Expression"]ADD:Case["Expression","Expression"]MINUS:Case["Expression","Expression"]MULTIPLY:Case["Expression","Expression"]DIVIDE:Case["Expression","Expression"]1
这些访问器可用于获取与ADT案例相关联的数据,但如果ADT不是用匹配的案例构造的,则访问器将引发异常。当您已经知道ADT对象的情况时,这是一个速记法。
@adt
还将自动生成一个模式匹配方法,当您事先不知道您有哪种情况时,可以使用该方法:
@adtclassExpression:LITERAL:Case[float]UNARY_MINUS:Case["Expression"]ADD:Case["Expression","Expression"]MINUS:Case["Expression","Expression"]MULTIPLY:Case["Expression","Expression"]DIVIDE:Case["Expression","Expression"]2
有关使用这些生成的方法的示例,请参见库的测试。