Package: algebraic-data-types
adtadtis a library providingalgebraic data typesin Python, with a clean, intuitive syntax, and support fortypingthrough amypy plugin.NOTE:This project is experimental, and not actively maintained by the author. Contributions and forking are more than welcome.Table of contents:What are algebraic data types?Pattern matchingCompared to EnumsCompared to inheritanceExamples in other programming languagesInstallationmypy pluginDefining an ADTGenerated functionalityCustom methodsWhat are algebraic data types?Analgebraic data type(also known as an ADT) is a way to represent multiple variants of a single type, each of which can have some data associated with it. The idea is very similar totagged unions and sum types, which in Python are represented asEnums.ADTs are useful for a variety of data structures, including binary trees:@adtclassTree:EMPTY:CaseLEAF:Case[int]NODE:Case["Tree","Tree"]Abstract syntax trees (like you might implement as part of a parser, compiler, or interpreter):@adtclassExpression:LITERAL:Case[float]UNARY_MINUS:Case["Expression"]ADD:Case["Expression","Expression"]MINUS:Case["Expression","Expression"]MULTIPLY:Case["Expression","Expression"]DIVIDE:Case["Expression","Expression"]Or more generic versions of a variant type, like anEithertype that represents a type A or a type B, but not both:L=TypeVar('L')R=TypeVar('R')@adtclassEither(Generic[L,R]):LEFT:Case[L]RIGHT:Case[R]Pattern matchingNow, defining a type isn't that interesting by itself. A lot of the expressivity of ADTs arises when youpattern matchover them (sometimes known as "destructuring").For example, we could use theEitherADT from above to implement a sort of error handling:# Defined in some other module, perhapsdefsome_operation()->Either[Exception,int]:returnEither.RIGHT(22)# Example of building a constructor# Run some_operation, and handle the success or failuredefault_value=5unpacked_result=some_operation().match(# In this case, we're going to ignore any exception we receiveleft=lambdaex:default_value,right=lambdaresult:result)Aside: this is very similar to how error handling is implemented in languages likeHaskell, because it avoids the unpredictable control flow of raising and catching exceptions, and ensures that callers need to make an explicit decision about what to do in an error case.One can do the same thing with theExpressiontype above (just more cases to match):defhandle_expression(e:Expression):returne.match(literal=lambdan:...,unary_minus=lambdaexpr:...,add=lambdalhs,rhs:...,minus=lambdalhs,rhs:...,multiply=lambdalhs,rhs:...,divide=lambdalhs,rhs:...)Compared to EnumsADTs are somewhat similar toEnumsfrom the Python standard library (in fact, the uppercase naming convention is purposely similar).For example, anEnumversion ofExpressionmight look like:fromenumimportEnum,autoclassEnumExpression(Enum):LITERAL=auto()UNARY_MINUS=auto()ADD=auto()MINUS=auto()MULTIPLY=auto()DIVIDE=auto()However, this doesn't allow data to be associated with each of these enum values. A particular value ofExpressionwill tell you about akindof expression that exists, but the operands to the expressions still have to be stored elsewhere.From this perspective, ADTs are likeEnums that can optionally have data associated with each case.Compared to inheritanceAlgebraic data types are a relatively recent introduction to object-oriented programming languages, for the simple reason that inheritance can replicate the same behavior.Continuing our examples with theExpressionADT, here's how one might represent it with inheritance in Python:fromabcimportABCclassABCExpression(ABC):passclassLiteralExpression(ABCExpression):def__init__(self,value:float):passclassUnaryMinusExpression(ABCExpression):def__init__(self,inner:ABCExpression):passclassAddExpression(ABCExpression):def__init__(self,lhs:ABCExpression,rhs:ABCExpression):passclassMinusExpression(ABCExpression):def__init__(self,lhs:ABCExpression,rhs:ABCExpression):passclassMultiplyExpression(ABCExpression):def__init__(self,lhs:ABCExpression,rhs:ABCExpression):passclassDivideExpression(ABCExpression):def__init__(self,lhs:ABCExpression,rhs:ABCExpression):passThis is noticeably more verbose, and the code to consume these types gets much more complex as well:e:ABCExpression=UnaryMinusExpression(LiteralExpression(3))# Example of creating an 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}')ADTs offer a simple way to define a type which isone of a set of possible cases, and allowing data to be associated with each case and packed/unpacked along with it.Examples in other programming languagesAlgebraic data types are very common in functional programming languages, likeHaskellorScala, but they're gaining increasing acceptance in "mainstream" programming languages as well.Here are a few examples.RustRustenums are actually full-fledged ADTs. Here's how anEitherADT could be defined:enumEither<L,R>{Left(L),Right(R),}SwiftSwift enumerationsare very similar to Rust's, and behave like algebraic data types through their support of "associated values."enumEither<L,R>{caseLeft(L)caseRight(R)}TypeScriptTypeScriptoffers ADTs through a language feature known as"discriminated unions".See this example from their documentation:interfaceSquare{kind:"square";size:number;}interfaceRectangle{kind:"rectangle";width:number;height:number;}interfaceCircle{kind:"circle";radius:number;}typeShape=Square|Rectangle|Circle;InstallationTo addadtas a library in your Python project, simply runpip(orpip3, as it may be named on your system):pip install algebraic-data-typesThis will installthe latest version from PyPI.mypy pluginThe library also comes with a plugin formypythat enables typechecking of@adtdefinitions.If you are already using mypy, the plugin is required to avoid nonsensical type errors.To enable theadttypechecking plugin, add the following to amypy.inifile in your project's working directory:[mypy]
plugins = adt.mypy_pluginDefining an ADTTo begin defining your own data type, import the@adtdecorator andCase[…]annotation:fromadtimportadt,CaseThen, define a new Python class, upon which you apply the@adtdecorator:@adtclassMyADT1:passFor each case (variant) that your ADT will have, declare a field with theCaseannotation. It's conventional to declare your fields with ALL_UPPERCASE names, but the only true restriction is that theycannotbe lowercase.@adtclassMyADT2:FIRST_CASE:CaseSECOND_CASE:CaseIf you want to associate some data with a particular case, list the type of that data in brackets afterCase(similar to theGeneric[…]andTuple[…]annotations fromtyping). For example, to add a case with an associated string:@adtclassMyADT3:FIRST_CASE:CaseSECOND_CASE:CaseSTRING_CASE:Case[str]You can build cases with arbitrarily many associated pieces of data, as long as all the types are listed:@adtclassMyADT4:FIRST_CASE:CaseSECOND_CASE:CaseSTRING_CASE:Case[str]LOTS_OF_DATA_CASE:Case[int,str,str,Dict[int,int]]ADTs can also be recursive—i.e., an ADT can itself be stored alongside a specific case—though the class name has to be provided in double quotes (a restriction which also applies totyping).A typical example of a recursive ADT is a linked list. Here, the list is also made generic over a typeT:T=TypeVar('T')@adtclassLinkedList(Generic[T]):NIL:CaseCONS:Case[T,"LinkedList[T]"]See the library'stestsfor more examples of complete ADT definitions.Generated functionalityGiven an ADT defined as follows:@adtclassMyADT5:EMPTY:CaseINTEGER:Case[int]STRING_PAIR:Case[str,str]The@adtdecorator will automatically generate accessor methods of the following form:defempty(self)->None:returnNonedefinteger(self)->int:...# unpacks int value and returns itdefstring_pair(self)->Tuple[str,str]:...# unpacks strings and returns them in a tupleThese accessors can be used to obtain the data associated with the ADT case, butaccessors will throw an exception if the ADT was not constructed with the matching case. This is a shorthand when you already know the case of an ADT object.@adtwill also automatically generate a pattern-matching method, which can be used when youdon'tknow which case you have ahead of time:Result=TypeVar('Result')defmatch(self,empty:Callable[[],Result],integer:Callable[[int],Result],string_pair:Callable[[str,str],Result])->Result:if...selfwasconstructedasEMPTY...:returnempty()elif...selfwasconstructedasINTEGER...:returninteger(self.integer())elif...selfwasconstructedasSTRING_PAIR...:returnstring_pair(*self.string_pair())# if pattern match is incomplete, an exception is raisedSee the library'stestsfor examples of using these generated methods.@adtwill also generate__repr__,__str__, and__eq__methods (only if they are notdefined already), to make ADTs convenient to use by default.Custom methodsArbitrary methods can be defined on ADTs by simply including them in the class definition as normal.For example, to build "safe" versions of the default accessors onExampleADT, which returnNoneinstead of throwing an exception when the case is incorrect:@adtclassExampleADT:EMPTY:CaseINTEGER:Case[int]STRING_PAIR:Case[str,str]@propertydefsafe_integer(self)->Optional[int]:returnself.match(empty=lambda:None,integer=lambdan:n,string_pair=lambda_a,_b:None)@propertydefsafe_string_pair(self)->Optional[Tuple[str,str]]:returnself.match(empty=lambda:None,integer=lambdan:None,string_pair=lambdaa,b:(a,b))However, additional fieldsmust notbe added to the class, as the decorator will attempt to interpret them as ADTCases (which will fail).