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
abstractmethodand our own extensionsAbstractVar(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.
-
PRNGKeyprovides 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.
-
treeis 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 byequinox.Module;PRNGKeyis inspired byjax.random.key;tree.mapis inspired byjax.tree.map;tree.replaceis inspired byequinox.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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file structional-0.3.0.tar.gz.
File metadata
- Download URL: structional-0.3.0.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
552e0d329d61ddf77ac7fd7f357ef275f46716b0c7bfbe5af09258d03352d5d2
|
|
| MD5 |
dcd7b2807d8ea9c69bb8baeef25d2fab
|
|
| BLAKE2b-256 |
c0e579e1659aa22f8cfc710fd1189bdc275661f4ad3d4bffbb12fdcdef5cf877
|
File details
Details for the file structional-0.3.0-py3-none-any.whl.
File metadata
- Download URL: structional-0.3.0-py3-none-any.whl
- Upload date:
- Size: 33.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4c290d752c6170d343fe16c2e84eb5ffc76a87df77a6ce74cf6d6b9e044273f
|
|
| MD5 |
2341fb687310c428e99b993b357799c7
|
|
| BLAKE2b-256 |
d336d9da8b52ff06f2b4c6a97889c50a9d67d8f2c0bd24e4d2d83994e90eb67e
|