Metadata-Version: 2.1
Name: adt-decorators
Version: 0.1.0
Summary: Algebraic Data Types via Class Decorators
Home-page: https://github.com/m0rphism/adt-decorators
Author: Hannes Saffrich
Author-email: saffrich@informatik.uni-freiburg.de
License: BSD 2-clause
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Description-Content-Type: text/markdown
License-File: LICENSE

# Overview

This package provides class-decorators for defining
*[Algebraic Data Types (ADTs)](https://en.wikipedia.org/wiki/Algebraic_data_type)*.

ADTs are mostly known from

- functional languages such as
[Haskell](https://wiki.haskell.org/Algebraic_data_type) or
[OCaml](https://cs3110.github.io/textbook/chapters/data/algebraic_data_types.html); and
- [Rust](https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html), where they are called `enum`s.

Working with ADTs is most pleasant in the presence of
[pattern matching](https://peps.python.org/pep-0636/),
so we recommend to use Python >= 3.10.

This package exports exactly two definitions:

- the [`adt`](reference.md#adt.adt) decorator introduces the constructor classes in the namespace of the decorated class; and
- the [`adt_export`](reference.md#adt.adt_export) decorator introduces the constructor classes in the global namespace.

## Related packages

There are a few packages which aim to provide similar functionality:

- [`algebraic-data-types`](https://pypi.org/project/algebraic-data-types/)
  also describes ADTs via class decorators and
  annotations, but is aimed at older python version and does not work
  with pattern matching.

- [`algebraic-data-type`](https://pypi.org/project/algebraic-data-type/)
  does not support a concise definition via decorators.

## Example

The following example defines the syntax tree for the 
[lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)
as an ADT `Expr` together with a `__str__`-method, which pretty prints
the syntax tree.

```python
from adt import adt

@adt
class Expr:
    Var: str                          # Constructor with one unnamed argument
    Abs: [str, 'Expr']                # Constructor with two unnamed arguments
    App: {'e1': 'Expr', 'e2': 'Expr'} # Constructor with two named arguments

    def __str__(self) -> str:
        match self:
            case Expr.Var(x):      return x
            case Expr.Abs(x, e):   return f"(λ{x}. {e})"
            case Expr.App(e1, e2): return f"({e1} {e2})"

id_expr = Expr.Abs("x", Expr.Var("x"))

assert str(id_expr) == '(λx. x)'
```

As we used [`adt`](../reference/#adt.adt) instead of [`adt_export`](../reference/#adt.adt_export),
the constructors `Var`, `Abs`, and `App` are defined in the namespace
of the base class `Expr`, and hence have to be accessed as `Expr.Var`, `Expr.Abs`, `Expr.App`.

The code generated by the `adt` decorator in the above example is equivalent to:
```python
from dataclasses import dataclass

class Expr:
    def __init__(self, *args, **kwargs):
        raise TypeError(
            "Tried to construct an ADT instead of one of it's constructors.")
    
    def __str__(self) -> str:
        match self:
            case Expr.Var(x):      return x
            case Expr.Abs(x, e):   return f"(λ{x}. {e})"
            case Expr.App(e1, e2): return f"({e1} {e2})"

    def is_var(self) -> bool:
        return isinstance(self, Var)

    def is_abs(self) -> bool:
        return isinstance(self, Abs)

    def is_app(self) -> bool:
        return isinstance(self, App)

@dataclass
class Var(Expr):
  _1: str

@dataclass
class Abs(Expr):
  _1: str
  _2: 'Expr'

@dataclass
class App(Expr):
  e1: str
  e2: 'Expr'

# Those statements are not really executed, as the `Var`, `Abs` and
# `App` classes are created anonymously, so in the real implementation
# the global namespace is *not* even temporarily polluted.
Expr.Var = Var
Expr.Abs = Abs
Expr.App = App
del Var
del Abs
del App
```
