[翻译]relay中的代数数据类型— tvm 0.7.dev0文档
代数数据类型(ADT)是函数式编程语言(尤其是从ML派生的语言)的主要功能,因为它们以易于推理的方式表示数据结构,从而可以编写递归计算。由于递归旨在成为Relay中控制流的主要机制之一,因此Relay包含ADT以最佳表示循环和必须使用递归实现的其他控制流结构非常重要。## 在ADT上定义和匹配¶
注意:文本格式目前不支持ADT。这里的语法是推测性的,基于其他语言的ADT。_ADT可以理解为类C语言的通用版本enum
和struct
类型。像C一样struct:
,ADT实例是指定类型的字段的容器,但是类型系统允许相同的类型以系统的方式对不同的可能字段分组进行编码,类似于enum
使用有限类型的C定义的C类型。用户命名的可能值。具体来说,ADT被定义为一组命名的构造函数,每个构造函数都是一个函数,该函数将指定类型的值作为参数并返回命名ADT的实例。ADT实例仅包含传递给用于生成它的构造函数调用的参数值。在解构之前,ADT值是不透明的,从而允许再次访问构造函数的参数并用于计算新值。因为一个特定的ADT可以具有多个具有不同签名的构造函数,所以通常有必要在不同的可能构造函数上分支,从而导致ADT的匹配语法。因此,ADT有时被称为“标记联合”,因为ADT实例由用于生成该实例的构造函数的名称进行了标记,并且以后可以基于该标记进行检查。由于每个ADT都有一组有限的构造函数,因此可以直接确定处理ADT实例的函数是否正在处理所有可能的情况。特别是,与C中的类型相比,类型系统可以确保在解构ADT实例时在所有情况下都正确分配union
类型。因此,通常很容易推断出ADT。实现细节:继电器ADT定义是全局的,与全局功能定义类似,存储在模块中。实际上,ADT名称是全局类型变量(就像全局函数名称是全局变量一样)。该模块将ADT名称(全局类型变量)映射到该ADT的构造函数列表。_以下是定义ADT并通过match表达式在函数中使用它的简单示例:
请注意,ADT由名称标识,这意味着从类型检查器的角度来看,两个结构相同的构造函数的ADT仍将是不同的数据类型。dataNumbers2{Empty2:()->Numbers2Single2:(Tensor[(),int32])->Numbers2Pair2:(Tensor[(),int32],Tensor[(),int32])->Numbers2
## 类型检查ADT和多态性¶
本节将详细介绍ADT的类型。涉及的大多数复杂性是由于与函数一样,ADT可以是多态的并带有类型参数。例如,函数编程语言中常用的标准ADT之一是可选类型,在此定义:dataOptional{None:()->OptionalSome:(a)->Optional可选类型通常用作涉及查询数据结构的任何操作的返回类型(返回Some(v)
是否找到值None
)。在定义中使用类型参数允许在各种情况下使用相同的可选类型,而不必为其中可能包含的每种不同类型定义唯一的ADT。但是,重要的是要确保类型系统仍可以区分其内容属于不同类型的选项类型,因为如果期望包含a的选项的函数接收到包含a的选项,则会违反类型安全性。正如此示例可能暗示的那样,因此为ADT实例提供了一个包含该实例的具体类型参数的类型,从而确保了信息的保存。让以下示例说明:Tensor[(),int32]Tensor[(3,4),float32]
上面示例中带注释的类型参数(例如)的语法称为“类型调用”,将多态ADT定义视为类型级别的函数(采用类型参数并返回类型,即ADT)。任何出现在类型注释或函数签名中的ADT都必须使用类型实参进行注释(非多态ADT必须在没有参数的类型调用中)。Optional[Tensor[(),int32]]因此,我们可以在一般的说,如果构造器C
,它利用类型参数是用于ADT构造即采用类型参数(其中可能包含的任何的),则具有类型。这意味着构造函数的类型类似于普通函数,因此出现在调用节点内部,并且可以传递给其他函数或由其他函数返回。特别是,上面的示例具有签名,而具有。T1,…,TnD
v1,…,vnT1,…,Tnv1,…,vnC
funSome
funNone
fun()->Optional[a]## 使用ADT递归¶
允许ADT定义是递归的,即,名为ADT的定义D
可以假定类型的存在D
并将其用作构造函数的参数。递归允许ADT表示复杂的结构,例如列表或树;它是函数式编程中ADT强大功能的源泉,因为适当设计的数据结构可以轻松地用递归函数简洁地表示计算。许多常用的ADT涉及递归;其中一些在ADT的常见用法中给出。作为此处的示例,我们将检查功能语言中普遍存在的列表ADT:dataList{Nil:()->ListCons:(a,List[a])->List(请注意,List
即使在构造函数中,对的递归引用也包装在类型调用中。)上面的定义意味着可以通过嵌套Cons
构造函数来表示特定类型的值的列表,直到到达列表的末尾为止,这可以用Nil
(表示空列表)来表示。以这种方式表示的列表可以轻松地递归处理。例如,以下函数求和一个整数列表:碰巧的是,列表上的许多递归函数(如刚刚给出的共享结构)可以分解为通用的,易于使用的函数,这些将在“常见ADT用法”下进行讨论。## 在匹配表达式模式匹配¶
与其他功能语言中一样,Relay中的match表达式具有更多的模式匹配功能,而不仅仅是每个构造函数都有一个要解构的值的数据类型的情况。特别是,可以以递归方式建立比赛案例中的模式:
- 构造器模式与特定的ADT构造器匹配。如果值与构造函数匹配,则构造函数的每个参数都将与嵌套模式匹配。
- 通配符模式将匹配任何值,并且不会绑定到变量。
- 变量模式将匹配任何值,并将其绑定到范围为match子句的局部变量。
在上述简单情况下
@list_sum
,第一个匹配项具有一个Nil
构造函数模式(没有嵌套参数),第二个匹配项具有一个构造Cons
函数模式,该模式对的每个参数都使用可变模式Cons
。下面的示例使用通配符模式来忽略以下参数之一Cons
:这里,一个构造函数模式嵌套在另一个构造函数模式中,以避免为list选项嵌套嵌套的匹配表达式。顶级通配符模式还用于处理与第一个子句不匹配的所有情况:
请注意,匹配表达式按照列出案例的顺序检查其模式:与输入值匹配的模式的第一个子句是被求值的子句。在这里,顶级变量模式绑定了整个输入值:## 常见的ADT用法¶ 在函数式编程语言中,某些ADT提供了用于编写通用程序的有用工具。参数多态性和高阶函数使这些ADT易于重用,并允许通用函数在常见情况下对其进行操作。relay包括某些预定义的ADT的“序言”,以及对应于其他语言必不可少的ADT的功能。在“类型检查ADT和多态”下定义的选项类型就是这样的一种ADT,只要它在某种意义上使函数仅在某些情况下返回值,就可以使用该类型。具有选项类型使类型系统可以跟踪哪些函数始终返回某个类型的值而不是返回该类型的选项,从而确保始终显式检查所有选项(与返回空指针或抛出异常一样)解决该问题的方法)。列表(在带有ADT的递归中定义)可以通过泛型函数以类似于列表理解和Python中某些库函数的方式进行操作。以下是用于遍历列表的非常常用的功能,这些功能包含在Relay的Prelude中。(在函数式编程文献中,所有这些特征都得到了广泛的描述,我们不会尝试在本文档中复制该著作。)
使用这些迭代构造,可以紧凑地表达对列表的许多常见操作。例如,以下映射将列表的所有成员加倍:
以下右折叠连接了两个列表:
下面的左折将列表列表变平(使用串联):
请注意,这些迭代构造可以直接在Relay的源语言中实现,并且可以轻松定义更多(并且对于更多数据类型,如树),而不是在语言中内置的构造(例如MXNet中的“ foreach”)。ADT及其可扩展性允许在Relay中表达并由类型系统支持各种迭代和数据结构,而无需修改语言实现。## 使用ADT实现神经网络¶
在2015年的博客文章中,克里斯托弗·奥拉(Christopher Olah)指出,使用通用的函数式编程结构可以轻松表达许多神经网络。relay的ADT允许将这些示例直接在TVM中实现。首先,让我们假设我们有一个对应于经过训练的递归神经网络(RNN)单元的函数,该单元接收过去的状态和输入值,并返回新的状态和输出值。在Relay中,它将具有以下签名:@cell:fn
按照Olah的示例,我们可以对输入的序列(列表)进行左折:使用展开的迭代器(来自Haskell的标准库),可以使用相同的单元来生成生成器网络(该生成器网络接受单个输入并生成一系列输出):
累加映射(同时更新累加器值和输出列表的折叠)可用于编写通用RNN(每个输入都有一个输出):
Olah还给出了双向神经网络的示例,其中两组单元格(可能具有不同的权重)在两个方向上处理输入并产生一组输出。以下是该示例的Relay实现: