Practical functional programming primitives for Python: Monads, Transformers, and DSL operators for safe business logic
Project description
darkcore
darkcore is a lightweight functional programming toolkit for Python.
It brings Functor / Applicative / Monad abstractions, classic monads like Maybe, Either/Result, Reader, Writer, State,
and an expressive operator DSL (|, >>, @) that makes Python feel almost like Haskell.
✨ Features
- Functor / Applicative / Monad base abstractions
- Core monads implemented:
Maybe— handle missing valuesEither/Result— safe error handlingValidation— accumulate multiple errorsReader— dependency injection / environmentWriter— accumulate logsState— stateful computations
- Monad transformers:
MaybeT,ResultT,ReaderT,StateT,WriterT - Utilities:
traverse/sequence, Applicative combinators - Advanced monads:
RWST(Reader-Writer-State) - Operator overloads for concise DSL-style code:
|→fmap(map)>>→bind(flatMap)@→ap(applicative apply)
- High test coverage, Monad law tests included
🚀 Installation
pip install darkcore
(or use Poetry)
🧪 Quick Examples
Maybe
from darkcore.maybe import Maybe
m = Maybe(3) | (lambda x: x+1) >> (lambda y: Maybe(y*2))
print(m) # Just(8)
n = Maybe(None) | (lambda x: x+1)
print(n) # Nothing
Result
from darkcore.result import Ok, Err
def parse_int(s: str):
try:
return Ok(int(s))
except ValueError:
return Err(f"invalid int: {s}")
res = parse_int("42") >> (lambda x: Ok(x * 2))
print(res) # Ok(84)
res2 = parse_int("foo") >> (lambda x: Ok(x * 2))
print(res2) # Err("invalid int: foo")
Validation: accumulate errors via Applicative
from darkcore.validation import Success, Failure
def positive(x: int):
return Failure(["non-positive"]) if x <= 0 else Success(x)
v = Success(lambda a: lambda b: a + b).ap(positive(-1)).ap(positive(0))
print(v) # Failure(['non-positive', 'non-positive'])
# Result would stop at the first failure
Validation is primarily intended for Applicative composition; bind short-circuits like Result and is not recommended for error accumulation scenarios.
Choosing between Result, Either, and Validation
| Type | Error shape | Behavior on bind (>>) |
Best use case |
|---|---|---|---|
Result |
Typically string/Exception-like | Short-circuits | IO boundaries, failing effects |
Either |
Domain-typed error | Short-circuits | Domain errors with rich types |
Validation |
Accumulates via Applicative | Short-circuits monadically | Form-style multi-error accumulation |
Note:
Validationaccumulates errors inApplicativeflows (@/ap,traverse,sequence_*), but monadically (>>) it short-circuits.
Equality of ReaderT / StateT
These transformers represent computations. Equality is extensional: compare results of run under the same environment/state, not object identity.
Reader
from darkcore.reader import Reader
get_user = Reader(lambda env: env["user"])
greet = get_user | (lambda u: f"Hello {u}")
print(greet.run({"user": "Alice"})) # "Hello Alice"
Writer
from darkcore.writer import Writer
# list log by default
w = Writer.pure(3).tell(["start"]) >> (lambda x: Writer(x + 1, ["inc"]))
print(w) # Writer(4, log=['start', 'inc'])
# for non-``list`` logs, pass ``empty`` and ``combine`` explicitly
# ``empty`` provides the identity element and ``combine`` appends logs
w2 = Writer("hi", empty=str, combine=str.__add__).tell("!")
print(w2) # Writer('hi', log='!')
# omitting these for a non-``list`` log raises ``TypeError``
try:
Writer("hi", "!") # missing empty/combine
except TypeError:
print("expected TypeError")
State
from darkcore.state import State
inc = State(lambda s: (s, s+1))
prog = inc >> (lambda x: State(lambda s: (x+s, s)))
print(prog.run(1)) # (3, 2)
Traverse utilities
from darkcore.traverse import traverse_result
from darkcore.result import Ok, Err
def parse_int(s: str):
try:
return Ok(int(s))
except ValueError:
return Err(f"bad: {s}")
print(traverse_result(["1", "2"], parse_int)) # Ok([1, 2])
print(traverse_result(["1", "x"], parse_int)) # Err("bad: x")
Result short-circuits on the first Err in traverse_* / sequence_*, whereas Validation accumulates errors under Applicative composition.
RWST
from darkcore.rwst import RWST
from darkcore.result import Ok
combine = lambda a, b: a + b
action = RWST.ask(Ok.pure, combine=combine, empty=list).bind(
lambda env: RWST.tell([env], Ok.pure, combine=combine, empty=list)
)
print(action(1, 0)) # Ok(((None, 0), [1]))
Operator DSL
from darkcore.maybe import Maybe
mf = Maybe(lambda x: x * 2)
mx = Maybe(4)
print((mf @ mx) | (lambda x: x + 1)) # Just(9)
Pattern Matching
from darkcore.result import Ok, Err
from darkcore.maybe import Maybe
from darkcore.either import Right, Left
from darkcore.writer import Writer
def classify(r):
match r:
case Ok(v) if v > 10:
return ("big", v)
case Ok(v):
return ("ok", v)
case Err(e):
return ("err", e)
def maybe_demo(m):
match m:
case Maybe(value=None):
return "nothing"
case Maybe(value=v):
return v
def either_demo(x):
match x:
case Right(v):
return v
case Left(e):
return e
w = Writer(3, ["a"], empty=list, combine=lambda a, b: a + b)
match w:
case Writer(v, log=ls):
print(v, ls)
📖 Integration Example
from darkcore.reader import Reader
from darkcore.writer import Writer
from darkcore.state import State
from darkcore.result import Ok, Err
# Reader: get user from environment
get_user = Reader(lambda env: env.get("user"))
# Result: validate existence
to_result = lambda user: Err("no user") if user is None else Ok(user)
# Writer: log user
log_user = lambda user: Writer(user, [f"got user={user}"])
# State: update counter
update_state = lambda user: State(lambda s: (f"{user}@{s}", s+1))
env = {"user": "alice"}
user = get_user.run(env)
res = to_result(user) >> (lambda u: Ok(log_user(u)))
writer = res.value
print(writer.log) # ['got user=alice']
out, s2 = update_state(writer.value).run(42)
print(out, s2) # alice@42 43
Why?
- Safer business code
- Avoid nested
try/exceptandif Nonechecks - Express computations declaratively with monads
- Avoid nested
- Educational value
- Learn Haskell/FP concepts hands-on in Python
- Expressive DSL
|,>>,@make pipelines concise and clear
Development
git clone https://github.com/minamorl/darkcore
cd darkcore
poetry install
poetry run pytest -v --cov=darkcore
License
MIT
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 darkcore-0.5.1.tar.gz.
File metadata
- Download URL: darkcore-0.5.1.tar.gz
- Upload date:
- Size: 14.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.12.3 Linux/6.11.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c6012fa4777aaaacd5d8324fd83dec6f03884011ca91dbbeaf348e9e64bb41f
|
|
| MD5 |
337bec9aae464f4a88be6ae8491be1f4
|
|
| BLAKE2b-256 |
e9f004f74570e993c0f26b7759e29ed7a8b14076e159c30b6cd3e73f6d86bc8a
|
File details
Details for the file darkcore-0.5.1-py3-none-any.whl.
File metadata
- Download URL: darkcore-0.5.1-py3-none-any.whl
- Upload date:
- Size: 20.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.12.3 Linux/6.11.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa685ce672b4dbdc981186bf0a9ca7814340313554b6e9ef3399ef9100dbe782
|
|
| MD5 |
da997147fd01d0a00b58419258c25a63
|
|
| BLAKE2b-256 |
83bf71ab9891170fc7d7efc314c678047370bfa7120fe1d97322effa5d6a411b
|