Skip to main content

Clojure-style classless map-based multimethods and dispatch system for Python

Project description

PyClosure

A minimalist library to bring Clojure-style multimethods, method combinations, and tag-based polymorphism into Python — without using classes.

Features

  • Single and multiple argument dispatch
  • Hierarchical dispatch using derive and isa
  • Extensible method registration with defmethod
  • Compositional hooks with :before, :after, and :around
  • Class-free, functional programming style

Installation

Just clone or copy the pyclosure/ folder into your project.

# if you still use `pip`:
pip install pyclosure

# but we highly recommend to use `uv`:
uv add pyclosure

# and after installation via uv you can run the examples by:
pyclosure-zoo


Quickstart

Define animals as dictionaries

simba = {"name": "Simba", "type": "lion"}
dumbo = {"name": "Dumbo", "type": "elephant"}

Create a hierarchy

from pyclosure import Hierarchy, MultiMethod

h = Hierarchy()
h.derive("lion", "big-cat")
h.derive("big-cat", "animal")
h.derive("elephant", "animal")

Define a multimethod

feed = MultiMethod(lambda a: a["type"], hierarchy=h)

Register methods

def lion_feed(a):
    print(f"{a['name']} eats meat")

def elephant_feed(a):
    print(f"{a['name']} eats bananas")

feed.defmethod("lion", lion_feed)
feed.defmethod("elephant", elephant_feed)

Use it

feed(simba)   # Simba eats meat
feed(dumbo)   # Dumbo eats bananas

Real Multiple Dispatch

You can dispatch on multiple arguments by making the dispatch function return a tuple:

interact = MultiMethod(lambda a, b: (a["type"], b["type"]), hierarchy=h)

def lion_vs_elephant(a, b):
    print(f"{a['name']} chases {b['name']}")

interact.defmethod(("lion", "elephant"), lion_vs_elephant)

interact(simba, dumbo)  # Simba chases Dumbo

Method Combinations

:before – run logic before the main method

def open_cage(a):
    print(f"Opening cage for {a['name']}")

feed.defmethod("lion", open_cage, kind=":before")

:after – run logic after the main method

def cleanup(a):
    print(f"Cleaning up after {a['name']}")

feed.defmethod("lion", cleanup, kind=":after")

:around – wrap the primary method

def log_feed(inner_fn, a):
    print(f"[LOG] Start {a['name']}")
    result = inner_fn(a)
    print(f"[LOG] End {a['name']}")
    return result

feed.defmethod("lion", log_feed, kind=":around")

Method Registration via Decorators

You can also define methods using decorators instead of manual registration.

Primary method:

@feed.defmethod("lion")
def lion_feed(a):
    print(f"{a['name']} eats meat")

With hooks:

@feed.defmethod("lion", kind=":before")
def open_cage(a):
    print(f"Opening cage for {a['name']}")

@feed.defmethod("lion", kind=":after")
def cleanup(a):
    print(f"Cleaning cage after {a['name']}")

@feed.defmethod("lion", kind=":around")
def logger(inner_fn, a):
    print("[LOG] Start")
    result = inner_fn(a)
    print("[LOG] End")
    return result

this is fully equivalent to manually registering via feed.defmethod(...) and is great for organizeing logic clearly and idiomatically.


Why PyClosure?

This library helps you:

  • Write open, extensible, polymorphic code
  • Avoid deep class hierarchies
  • Dispatch based on dynamic rules
  • Cleanly separate data from behavior

Inspired by:


Example Output

Opening cage for Simba
[LOG] Start Simba
Simba eats meat
[LOG] End Simba
Cleaning up after Simba

Coming Soon

  • Decorator-based syntax (@defmethod)
  • Predicate-based dispatch
  • Package on PyPI
  • Integration with dataclasses or Pydantic (optional)

Philosophy

“Data is just data. Behavior is just behavior. Dispatch is glue.”
– Not Rich Hickey, but close enough.


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

mapdispatch-0.1.0.tar.gz (6.1 kB view details)

Uploaded Source

Built Distribution

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

mapdispatch-0.1.0-py3-none-any.whl (6.4 kB view details)

Uploaded Python 3

File details

Details for the file mapdispatch-0.1.0.tar.gz.

File metadata

  • Download URL: mapdispatch-0.1.0.tar.gz
  • Upload date:
  • Size: 6.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.14

File hashes

Hashes for mapdispatch-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4db14d700dbcb881be87670a073eaf61a1ae9a390e5eee92f30e71de8601e0e3
MD5 03ad00f5acb0abbd6c703b0a4c457f34
BLAKE2b-256 03c605c49a94b27fe5eda852820e05aea5d1b50ffc62c5a131480713863e5019

See more details on using hashes here.

File details

Details for the file mapdispatch-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: mapdispatch-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 6.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.14

File hashes

Hashes for mapdispatch-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7dfc7adf2200a922d2477fb985216b28644018a8da712faaef358403602a36f5
MD5 2d22ece6f22ef0c5613f4975a2862ac1
BLAKE2b-256 5b68c3d62d6fe8ac138d8e4e86bc2ac1e3eed6ec939c5fe2486e158ebd21b5df

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