Expression
repository·main·Indexed 20 days ago
https://github.com/dbrattli/expressionA pragmatic, type-safe functional programming library for Python 3.10+ that brings F#-inspired features such as pipelining, computational expressions, and Railway Oriented Programming to Python. It provides types like Option, Result, Try, and AsyncResult for error handling and optional values, as well as a Seq type for functional sequence operations. The library supports both fluent and functional syntax and is designed to be Pydantic-friendly.
What's inside Expression
- Expression is a library designed for frictionless and practical functional programming in Python 3.10+. It focuses on providing solid, type-safe, and high-performance abstractions that enable productive functional programming without the overhead of complex category theory tutorials. The library leverages Python's native support for functions, higher-order functions, lambdas, and composition to provide a pragmatic developer experience.
Overview of Expression goals and design philosophy
mainExpression is a pragmatic functional programming library for Python 3.10+ designed to provide F#-inspired features without creating an obscure DSL.
Key design principles include:
- Pythonic feel: Adheres to PEP-8 and avoids concepts that are non-standard in Python, such as currying or heavy operator overloading (e.g., avoiding
|or>>for pipes). - Composition over inheritance: Focuses on pipelining and dot-chaining as primary ways to compose logic.
- Type Safety: Provides full type-hints for all functions and methods, designed to pass strict static type checking with tools like Pylance.
- Low Cognitive Load: Avoids explicit recursion (hiding it within the SDK) and avoids operator overloading to prevent confusion.
- Data Integration: Built to be Pydantic-friendly, allowing Expression types to be used within Pydantic models for JSON (de)serialization.
- Pythonic feel: Adheres to PEP-8 and avoids concepts that are non-standard in Python, such as currying or heavy operator overloading (e.g., avoiding
Use Option as an effect with @effect.option
mainThe
expressionlibrary implementsOptionas an effect using decorated coroutines. By using the@effect.option[T]()decorator, you can useyieldandyield fromto consume or generate optional values.Short-circuiting behavior: This implements "railway oriented programming". If any part of the function
yield froms aNothingvalue, the entire function short-circuits. The remaining code in the function will never be executed, and the final result of the expression will beNothing.from expression import effect, Some, Nothing @effect.option[int]() def fn(): # If this yields Nothing, the function stops here x = yield from Nothing # This line will never be reached if the previous line was Nothing y = yield from Some(43) return x + y # The result of calling fn() will be Nothing result = fn()from expression import effect, Some, Nothing @effect.option[int]() def fn(): x = yield from Nothing # -- The rest of the function will never be executed -- y = yield from Some(43) return x + y result = fn()Apply Alpha Conversion (α-conversion)
mainAlpha-conversion is the process of renaming bound variables within a function. It ensures that the logic of a function remains identical regardless of the specific names used for its parameters.
# These two expressions are functionally identical via alpha-conversion (lambda x: x)(42) == (lambda y: y)(42)Use Option types instead of None
mainIn functional programming, the
Option(orMaybe) type is used to represent values that might be missing. Unlike Python'sNone, which can lead toNameErrororAttributeErrorwhen dereferenced, anOptionexplicitly wraps a value in aSome(value)container or represents its absence withNothing.To use these types, import them from the
expressionpackage:from expression import Option, option, Some, Nothingfrom expression import Option, option, Some, NothingApply Beta Reduction (β-reduction)
mainBeta-reduction is the process of calculating a result by applying a function to an argument. It is the fundamental mechanism of function execution (substitution).
# Applying the function (lambda n: n*2) to the argument 7 (lambda n: n*2)(7) == 7*2Understand Pull vs Push collections
mainThe tutorial distinguishes between two ways of handling data flow:
Pull Collections (Spatial)
These are standard collections like
List,Iterable,Mapping, andstr. You actively pull values out of them, typically by callingnext()on anIterator.Push Collections (Temporal)
These are known as
Observables. Instead of you pulling data, the collection pushes values to you. AnObservableis the dual of anIterable:- An
Iterableprovides a getter for anIterator(__iter__). - An
Observableprovides a setter for anObserver(subscribe). - An
Iteratorprovides a getter for the next value (__next__). - An
Observerprovides a setter for the next value (on_nextorsend).
- An
Create single-case tagged unions
mainA single-case tagged union is useful for creating a distinct type that wraps an underlying type (e.g., a
SecurePasswordwrapper for astr). This prevents accidental assignment of the raw type to the wrapped type and provides a layer of abstraction.For single-case unions, you do not need to define a
tagfield; you only define the single case usingcase().from expression import case, tagged_union @tagged_union(frozen=True, repr=False) class SecurePassword: password: str = case() password = SecurePassword(password="secret") match password: case SecurePassword(password=p): assert p == "secret"Use the Try type for simplified error handling
mainIf you do not need to specify a custom error type and are satisfied with using standard PythonExceptionobjects, use theTrytype.Tryis a specialized version ofResultpinned toResult[TSource, Exception], which reduces the boilerplate required for type annotations.Use Eta-conversion (η-conversion) for Point-Free programming
mainEta-conversion involves adding or dropping an abstraction over a function. When used extensively to remove explicit arguments from function definitions, it is known as point-free programming. While point-free programming can simplify code, it can also lead to overly complex 'point-less' code.
# Eta-conversion: λx.(f x) is equivalent to f f = lambda x: x (lambda x: f(x)) == f # Example of point-free programming using reduce from functools import reduce # Standard style (with explicit lambda arguments) xs = reduce(lambda acc, x: max(acc, x), range(10)) # Point-free style (dropping the abstraction over arguments) xs = reduce(max, range(10))Distinguish between Option type and option module
mainExpression follows Python's PEP-8 naming conventions, which differs from F#.
Option(Capitalized): Refers to the actual type class.option(Lowercase): Refers to the module containing functional utilities (e.g.,option.map).
When using functional operations on an
Option, import theoptionmodule rather than trying to call methods directly on the type.>>> from expression import Option, option >>> Option <class'expression.core.option.Option'> >>> option <module 'expression.core.option' ...>Model errors using the Result type
mainInstead of raising exceptions, use the
Resulttype to model success and failure. This makes error handling explicit and prevents code bloat from try/except blocks.Resulthas two variants:Ok(value): Represents a successful operation containing the result value.Error(exn): Represents a failure containing the error/exception information.
This pattern is similar to the
Eithertype in other functional languages, whereRightis success andLeftis error.from expression import Ok, Error def fetch(url): try: if not "http://" in url: raise Exception("Error: unable to fetch from: '%s'" % url) value = url.replace("http://", "") return Ok(value) except Exception as exn: return Error(exn)