Expression

repository·main·Indexed 20 days ago

https://github.com/dbrattli/expression

A 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.

Tokens
15.5K
Snippets
54
Records
67
Agent score
72%

What's inside Expression

  1. Overview of Expression

    main
    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.
  2. Overview of Expression goals and design philosophy

    main

    Expression 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.
  3. Use Option as an effect with @effect.option

    main

    The expression library implements Option as an effect using decorated coroutines. By using the @effect.option[T]() decorator, you can use yield and yield from to consume or generate optional values.

    Short-circuiting behavior: This implements "railway oriented programming". If any part of the function yield froms a Nothing value, the entire function short-circuits. The remaining code in the function will never be executed, and the final result of the expression will be Nothing.

    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()
  4. Apply Alpha Conversion (α-conversion)

    main

    Alpha-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)
  5. Use Option types instead of None

    main

    In functional programming, the Option (or Maybe) type is used to represent values that might be missing. Unlike Python's None, which can lead to NameError or AttributeError when dereferenced, an Option explicitly wraps a value in a Some(value) container or represents its absence with Nothing.

    To use these types, import them from the expression package:

    from expression import Option, option, Some, Nothing
    from expression import Option, option, Some, Nothing
  6. Understand Pull vs Push collections

    main

    The tutorial distinguishes between two ways of handling data flow:

    Pull Collections (Spatial)

    These are standard collections like List, Iterable, Mapping, and str. You actively pull values out of them, typically by calling next() on an Iterator.

    Push Collections (Temporal)

    These are known as Observables. Instead of you pulling data, the collection pushes values to you. An Observable is the dual of an Iterable:

    • An Iterable provides a getter for an Iterator (__iter__).
    • An Observable provides a setter for an Observer (subscribe).
    • An Iterator provides a getter for the next value (__next__).
    • An Observer provides a setter for the next value (on_next or send).
  7. Create single-case tagged unions

    main

    A single-case tagged union is useful for creating a distinct type that wraps an underlying type (e.g., a SecurePassword wrapper for a str). 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 tag field; you only define the single case using case().

    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"
  8. Use the Try type for simplified error handling

    main
    If you do not need to specify a custom error type and are satisfied with using standard Python Exception objects, use the Try type. Try is a specialized version of Result pinned to Result[TSource, Exception], which reduces the boilerplate required for type annotations.
  9. Use Eta-conversion (η-conversion) for Point-Free programming

    main

    Eta-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))
  10. Distinguish between Option type and option module

    main

    Expression 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 the option module 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' ...>
  11. Model errors using the Result type

    main

    Instead of raising exceptions, use the Result type to model success and failure. This makes error handling explicit and prevents code bloat from try/except blocks.

    Result has 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 Either type in other functional languages, where Right is success and Left is 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)