{ "info": { "author": "Justin Spahr-Summers", "author_email": "justin@jspahrsummers.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed" ], "description": "# adt [![CircleCI](https://circleci.com/gh/jspahrsummers/adt.svg?style=svg&circle-token=2652421c13c636b5da0c992d77ec2fb0b128dd49)](https://circleci.com/gh/jspahrsummers/adt)\n\n`adt` is a library providing [algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type) in Python, with a clean, intuitive syntax, and support for [`typing`](https://docs.python.org/3/library/typing.html) through a [mypy plugin](#mypy-plugin).\n\n**Table of contents:**\n\n1. [What are algebraic data types?](#what-are-algebraic-data-types)\n 1. [Pattern matching](#pattern-matching)\n 1. [Compared to Enums](#compared-to-enums)\n 1. [Compared to inheritance](#compared-to-inheritance)\n 1. [Examples in other programming languages](#examples-in-other-programming-languages)\n1. [Installation](#installation)\n 1. [mypy plugin](#mypy-plugin)\n1. [Defining an ADT](#defining-an-adt)\n 1. [Generated functionality](#generated-functionality)\n 1. [Custom methods](#custom-methods)\n\n# What are algebraic data types?\n\nAn [algebraic data type](https://en.wikipedia.org/wiki/Algebraic_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 to [tagged unions and sum types](https://en.wikipedia.org/wiki/Tagged_union), which in Python are represented as [Enums](#compared-to-enums).\n\nADTs are useful for a variety of data structures, including binary trees:\n\n```python\n@adt\nclass Tree:\n EMPTY: Case\n LEAF: Case[int]\n NODE: Case[\"Tree\", \"Tree\"]\n```\n\nAbstract syntax trees (like you might implement as part of a parser, compiler, or interpreter):\n\n```python\n@adt\nclass Expression:\n LITERAL: Case[float]\n UNARY_MINUS: Case[\"Expression\"]\n ADD: Case[\"Expression\", \"Expression\"]\n MINUS: Case[\"Expression\", \"Expression\"]\n MULTIPLY: Case[\"Expression\", \"Expression\"]\n DIVIDE: Case[\"Expression\", \"Expression\"]\n```\n\nOr more generic versions of a variant type, like an `Either` type that represents a type A or a type B, but not both:\n\n```python\nL = TypeVar('L')\nR = TypeVar('R')\n\n@adt\nclass Either(Generic[L, R]):\n LEFT = Case[L]\n RIGHT = Case[R]\n```\n\n## Pattern matching\n\nNow, defining a type isn't that interesting by itself. A lot of the expressivity of ADTs arises when you [pattern match](https://en.wikipedia.org/wiki/Pattern_matching) over them (sometimes known as \"destructuring\").\n\nFor example, we could use the `Either` ADT from above to implement a sort of error handling:\n\n```python\n# Defined in some other module, perhaps\ndef some_operation() -> Either[Exception, int]\n\n# Run some_operation, and handle the success or failure\ndefault_value = 5\nreturn some_operation().match(\n # In this case, we're going to ignore any exception we receive\n left=lambda ex: default_value,\n right=lambda result: result)\n```\n\n_Aside: this is very similar to how error handling is implemented in languages like [Haskell](https://www.haskell.org/), 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._\n\nOne can do the same thing with the `Expression` type above (just more cases to match):\n\n```python\ne: Expression\nresult = e.match(\n literal=lambda n: ...,\n unary_minus=lambda expr: ...,\n add=lambda lhs, rhs: ...,\n minus=lambda lhs, rhs: ...,\n multiply=lambda lhs, rhs: ...,\n divide=lambda lhs, rhs: ...)\n```\n\n## Compared to Enums\n\nADTs are somewhat similar to [`Enum`s](https://docs.python.org/3/library/enum.html) from the Python standard library (in fact, the uppercase naming convention is purposely similar).\n\nFor example, an `Enum` version of `Expression` might look like:\n\n```python\nclass Expression(Enum):\n LITERAL = auto()\n UNARY_MINUS = auto()\n ADD = auto()\n MINUS = auto()\n MULTIPLY = auto()\n DIVIDE = auto()\n```\n\nHowever, this doesn't allow data to be associated with each of these enum values. A particular value of `Expression` will tell you about a _kind_ of expression that exists, but the operands to the expressions still have to be stored elsewhere.\n\nFrom this perspective, ADTs are like `Enum`s that can optionally have data associated with each case.\n\n## Compared to inheritance\n\nAlgebraic data types are a relatively recent introduction to object-oriented programming languages, for the simple reason that inheritance can replicate the same behavior.\n\nContinuing our examples with the `Expression` ADT, here's how one might represent it with inheritance in Python:\n\n```python\nclass Expression(ABC):\n pass\n\nclass LiteralExpression(Expression):\n def __init__(self, value: float):\n pass\n\nclass UnaryMinusExpression(Expression):\n def __init__(self, inner: Expression):\n pass\n\nclass AddExpression(Expression):\n def __init__(self, lhs: Expression, rhs: Expression):\n pass\n\nclass MinusExpression(Expression):\n def __init__(self, lhs: Expression, rhs: Expression):\n pass\n\nclass MultiplyExpression(Expression):\n def __init__(self, lhs: Expression, rhs: Expression):\n pass\n\nclass DivideExpression(Expression):\n def __init__(self, lhs: Expression, rhs: Expression):\n pass\n```\n\nThis is noticeably more verbose, and the code to consume these types gets much more complex as well:\n\n```python\ne: Expression\n\nif isinstance(e, LiteralExpression):\n result = # do something with e.value\nelif isinstance(e, UnaryMinusExpression):\n result = # do something with e.inner\nelif isinstance(e, AddExpression):\n result = # do something with e.lhs and e.rhs\nelif isinstance(e, MinusExpression):\n result = # do something with e.lhs and e.rhs\nelif isinstance(e, MultiplyExpression):\n result = # do something with e.lhs and e.rhs\nelif isinstance(e, DivideExpression):\n result = # do something with e.lhs and e.rhs\nelse:\n raise ValueError(f'Unexpected type of expression: {e}')\n```\n\nADTs offer a simple way to define a type which is _one of a set of possible cases_, and allowing data to be associated with each case and packed/unpacked along with it.\n\n## Examples in other programming languages\n\nAlgebraic data types are very common in functional programming languages, like [Haskell](https://www.haskell.org/) or [Scala](https://www.scala-lang.org/), but they're gaining increasing acceptance in \"mainstream\" programming languages as well.\n\nHere are a few examples.\n\n### [Rust](https://www.rust-lang.org/)\n\nRust `enum`s are actually full-fledged ADTs. Here's how an `Either` ADT could be defined:\n\n```rust\nenum Either {\n Left(L),\n Right(R),\n}\n```\n\n### [Swift](https://developer.apple.com/swift/)\n\n[Swift enumerations](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html) are very similar to Rust's, and behave like algebraic data types through their support of \"associated values.\"\n\n```swift\nenum Either {\n case Left(L)\n case Right(R)\n}\n```\n\n### [TypeScript](https://en.wikipedia.org/wiki/Microsoft_TypeScript)\n\n[TypeScript](https://en.wikipedia.org/wiki/Microsoft_TypeScript) offers ADTs through a language feature known as [\"discriminated unions\"](https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions).\n\nSee this example from their documentation:\n\n```typescript\ninterface Square {\n kind: \"square\";\n size: number;\n}\ninterface Rectangle {\n kind: \"rectangle\";\n width: number;\n height: number;\n}\ninterface Circle {\n kind: \"circle\";\n radius: number;\n}\n\ntype Shape = Square | Rectangle | Circle;\n```\n\n# Installation\n\nTo add `adt` as a library in your Python project, simply run `pip` (or `pip3`, as it may be named on your system):\n\n```\npip install algebraic-data-types\n```\n\nThis will install [the latest version from PyPI](https://pypi.org/project/algebraic-data-types/).\n\n## mypy plugin\n\nThe library also comes with a plugin for [mypy](http://mypy-lang.org/) that enables typechecking of `@adt` definitions. **If you are already using mypy, the plugin is required to avoid nonsensical type errors.**\n\nTo enable the `adt` typechecking plugin, add the following to a `mypy.ini` file in your project's working directory:\n\n```\n[mypy]\nplugins = adt.mypy_plugin\n```\n\n# Defining an ADT\n\nTo begin defining your own data type, import the `@adt` decorator and `Case[\u2026]` annotation:\n\n```python\nfrom adt import adt, Case\n```\n\nThen, define a new Python class, upon which you apply the `@adt` decorator:\n\n```python\n@adt\nclass MyADT:\n pass\n```\n\nFor each case (variant) that your ADT will have, declare a field with the `Case` annotation. It's conventional to declare your fields with ALL_UPPERCASE names, but the only true restriction is that they _cannot_ be lowercase.\n\n```python\n@adt\nclass MyADT:\n FIRST_CASE: Case\n SECOND_CASE: Case\n```\n\nIf you want to associate some data with a particular case, list the type of that data in brackets after `Case` (similar to the `Generic[\u2026]` and `Tuple[\u2026]` annotations from `typing`). For example, to add a case with an associated string:\n\n```python\n@adt\nclass MyADT:\n FIRST_CASE: Case\n SECOND_CASE: Case\n STRING_CASE: Case[str]\n```\n\nYou can build cases with arbitrarily many associated pieces of data, as long as all the types are listed:\n\n```python\n@adt\nclass MyADT:\n FIRST_CASE: Case\n SECOND_CASE: Case\n STRING_CASE: Case[str]\n LOTS_OF_DATA_CASE: Case[int, str, str, Dict[int, int]]\n```\n\nADTs can also be recursive\u2014i.e., an ADT can itself be stored alongside a specific case\u2014though the class name has to be provided in double quotes (a restriction which also applies to `typing`).\n\nA typical example of a recursive ADT is a linked list. Here, the list is also made generic over a type `T`:\n\n```python\nT = TypeVar('T')\n\n@adt\nclass LinkedList(Generic[T]):\n NIL: Case\n CONS: Case[T, \"LinkedList[T]\"]\n```\n\nSee the library's [tests](tests/) for more examples of complete ADT definitions.\n\n## Generated functionality\n\nGiven an ADT defined as follows:\n\n```python\n@adt\nclass ExampleADT:\n EMPTY: Case\n INTEGER: Case[int]\n STRING_PAIR: Case[str, str]\n```\n\nThe `@adt` decorator will automatically generate accessor methods of the following form:\n\n```python\n def empty(self) -> None:\n return None\n\n def integer(self) -> int:\n # unpacks int value and returns it\n\n def string_pair(self) -> Tuple[str, str]:\n # unpacks strings and returns them in a tuple\n```\n\nThese accessors can be used to obtain the data associated with the ADT case, but **accessors 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.\n\n`@adt` will also automatically generate a pattern-matching method, which can be used when you _don't_ know which case you have ahead of time:\n\n```python\n Result = TypeVar('Result')\n\n def match(self,\n empty: Callable[[], Result],\n integer: Callable[[int], Result],\n string_pair: Callable[[str, str], Result]) -> Result:\n if ... self was constructed as EMPTY ...:\n return empty()\n elif ... self was constructed as INTEGER ...:\n return integer(self.integer())\n elif ... self was constructed as STRING_PAIR ...:\n return string_pair(*self.string_pair())\n\n # if pattern match is incomplete, an exception is raised\n```\n\nSee the library's [tests](tests/) for examples of using these generated methods.\n\n`@adt` will also generate `__repr__`, `__str__`, and `__eq__` methods (only if they are not [defined already](#custom-methods)), to make ADTs convenient to use by default.\n\n## Custom methods\n\nArbitrary methods can be defined on ADTs by simply including them in the class definition as normal.\n\nFor example, to build \"safe\" versions of the default accessors on `ExampleADT`, which return `None` instead of throwing an exception when the case is incorrect:\n\n```python\n@adt\nclass ExampleADT:\n EMPTY: Case\n INTEGER: Case[int]\n STRING_PAIR: Case[str, str]\n\n @property\n def safe_integer(self) -> Optional[int]:\n return self.match(empty=lambda: None,\n integer=lambda n: n,\n string_pair=lambda _, _: None)\n\n @property\n def safe_string_pair(self) -> Optional[Tuple[str, str]]:\n return self.match(empty=lambda: None,\n integer=lambda: None,\n string_pair=lambda a, b: tuple(a, b))\n```\n\nHowever, additional fields _must not_ be added to the class, as the decorator will attempt to interpret them as ADT `Case`s (which will fail).\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/jspahrsummers/adt", "keywords": "adt algebraic data types sum union tagged", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "algebraic-data-types", "package_url": "https://pypi.org/project/algebraic-data-types/", "platform": "", "project_url": "https://pypi.org/project/algebraic-data-types/", "project_urls": { "Homepage": "https://github.com/jspahrsummers/adt" }, "release_url": "https://pypi.org/project/algebraic-data-types/0.1.1/", "requires_dist": null, "requires_python": "", "summary": "Algebraic data types for Python", "version": "0.1.1" }, "last_serial": 5832873, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "18b67133987107c898122e12c2698014", "sha256": "0a8458e17bb770f79b233b74dcb1a2c4a800ae386e9bbeda95b928e430cca17c" }, "downloads": -1, "filename": "algebraic_data_types-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "18b67133987107c898122e12c2698014", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 17126, "upload_time": "2019-07-10T17:58:35", "url": "https://files.pythonhosted.org/packages/95/df/80c4b04ac37b03d89d6faa80e354432e955478aca21b4890af9a07947d1b/algebraic_data_types-0.1.0-py3-none-any.whl" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "35d87a198ca6029b61c6c9a46b6b506f", "sha256": "fb5559921e5037a261ad1625bbb14ab7dcfade42f59727879c29d46db0e7c5ee" }, "downloads": -1, "filename": "algebraic_data_types-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "35d87a198ca6029b61c6c9a46b6b506f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 17280, "upload_time": "2019-09-15T19:42:25", "url": "https://files.pythonhosted.org/packages/33/de/e6fa9c1e1988aef133661d362d97a94882d2839fdb78565c2e508b34c132/algebraic_data_types-0.1.1-py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "35d87a198ca6029b61c6c9a46b6b506f", "sha256": "fb5559921e5037a261ad1625bbb14ab7dcfade42f59727879c29d46db0e7c5ee" }, "downloads": -1, "filename": "algebraic_data_types-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "35d87a198ca6029b61c6c9a46b6b506f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 17280, "upload_time": "2019-09-15T19:42:25", "url": "https://files.pythonhosted.org/packages/33/de/e6fa9c1e1988aef133661d362d97a94882d2839fdb78565c2e508b34c132/algebraic_data_types-0.1.1-py3-none-any.whl" } ] }