Skip to main content

Immutable structs, controlled randomness, and trees. These are the core tools I use to build reliable software in Python.

Project description

structional

Immutable structs, controlled randomness, and trees. These are the core tools I use to build reliable software in Python.

Whilst this library does not depend on PyTorch and is broadly applicable across all of Python, the downstream use-case is to make it possible to 'write functional PyTorch'.

This is not a library implementing monads, foldl, and all that magical Haskell stuff.

Installation

pip install structional

What's in the box?

  • Struct:

    • is an immutable (frozen) dataclass, making writing safe code easy;
    • is an abstract base class, supporting the stdlib abstractmethod and our own extensions AbstractVar (declaring abstract attributes) and __check_init__ (post-initialization invariants);
    • enforces 'abstract/final rules': every class can either be subclassed (abstract) or instantiated (concrete) – but not both.
  • PRNGKey provides access to deterministic randomness.

    • Use-case in ML is perfectly reproducible training runs; awesome for catching bugs.
    • A function need no longer be 'secretly random' because it depends on a background stateful RNG.
    • Call key.some_distribution() to sample from a distribution.
    • Split a key via key.split() to create deterministic but statistically independent new sources of randomness.
    • Keys can only be used once (known as 'linear typing'), to prevent accidental reuse.
  • tree is a subpackage for manipulating immutable nested structures of tuples, Structs, etc.:

    • tree.map ('functors' for you programming geeks) applies a function to every leaf.
    • tree.update ('lenses' for the geeks) updates just part of a tree structure, returning a new object out-of-place.

FAQ

What are similar reference points amongst other languages?

Structs combine Rust-style traits with Julia-style 'abstract/final rules'.

Meanwhile in Python, we have several JAX/Equinox reference points:

  • Structs are inspired by equinox.Module;
  • PRNGKey is inspired by jax.random.key;
  • tree.map is inspired by jax.tree.map;
  • tree.replace is inspired by equinox.tree_at.

(Broadly speaking these ideas are pretty standard in functional programming.)

Aren't there a lot of 'functional Python' libraries?

Yup. Most of them tend to either implement Haskell-isms (foldl, monads) or use all those complicated functional programming words (functors, ...) 😄. We try to take a pragmatic approach, implementing the tools that can't easily be already expressed in normal Python code.

How do these integrate with PyTorch torch.nn.Modules?

The idea is to treat the PyTorch modules as an implementation detail.

A typical pattern looks something like this:

from structional import AbstractVar, PRNGKey, Struct
from jaxtyping import Float
from torch import from_numpy, nn, Tensor

class AbstractImageDiffusion(Struct):
    shape: AbstractVar[tuple[int, ...]]
    num_steps: AbstractVar[int]

    @abstractmethod
    def step(self, image: Float[Tensor, "*shape"]) -> Float[Tensor, "*shape"]: ...

class LinearImageDiffusion(AbstractImageDiffusion):  # State-of-the-art architecture.
    model: nn.Linear
    shape: tuple[int, ...]
    num_steps: int

    def __init__(self):
        self.model = nn.Linear(256*256, 256*256)
        self.shape = (256, 256)
        self.num_steps = 10

    def step(self, image: Float[Tensor, "*shape"]) -> Float[Tensor, "*shape"]:
        return self.model(image.reshape(-1)).reshape(self.shape)

def inference(model: AbstractImageDiffusion, key: PRNGKey) -> Float[Tensor, "*shape"]:
    x = key.normal(size=model.shape)  # Initial noise
    x = from_numpy(x)  # Zero-copy if we're on the CPU; efficient GPU is an exercise for the reader ;)
    for _ in range(model.num_steps):  # Denoise
        x = model.step(x)
    return x
Where should abstract base classes live?

When organizing code that's large enough to be split into multiple files, then ABCs could either be placed alongside their subclasses:

# bar.py
class AbstractFoo(Struct): ...

class ConcreteFoo(AbstractFoo): ...

# qux.py
def frobnicate(x: AbstractFoo): ...

or they could live alongside their consumers:

# baz.py
class AbstractFoo(Struct): ...

def frobnicate(x: AbstractFoo): ...

# fizzle.py
class ConcreteFoo(AbstractFoo): ...

There's no hard rule, but about 80% of the time I find it's more useful to keep the abstract class (AbstractFoo) next to its consumer (frobnicate). The other 20% of the time I find it's most useful to keep it next to its subclasses (ConcreteFoo).

The rationale for this is that typically it is the consumer (frobnicate) that is the 'first class citizen' of our code, and the ABC mostly just exists as a way to define the interface required by that consumer.

This approach also pairs well with how ABCs are often used to support extensibility: later authors can come along and define their own concrete subclasses.

This isn't a hard-and-fast rule. It matters most when the two files live in separately-versioned packages, and in this case it really is usually best to keep the ABC alongside its consumer.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

structional-0.4.0.tar.gz (27.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

structional-0.4.0-py3-none-any.whl (33.8 kB view details)

Uploaded Python 3

File details

Details for the file structional-0.4.0.tar.gz.

File metadata

  • Download URL: structional-0.4.0.tar.gz
  • Upload date:
  • Size: 27.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for structional-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d98188073c7c1db042dc65ff35f47657c192dc0742cf69ad647165f72328f7b4
MD5 22354391d39d3c3e540283c457c6670b
BLAKE2b-256 0224774ee783f6d34c66ebf1123d88ae8c77c063a8eecedb9e3365c028d978a7

See more details on using hashes here.

File details

Details for the file structional-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: structional-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 33.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for structional-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1acd4bbe16e5fc91e6f784c0381365646cb4265b9b56d31aa62e52802ab3e39d
MD5 a263d224fe308bf5cfbc7fe76a7f9fbb
BLAKE2b-256 a8ae9141ed59930c5afe080ec458eb67b17f0c16acea408b4b73dfa896fbe679

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page