Skip to main content

A Python package for pure functional programming with fluent chaining that reads like prose

Project description

Fluent Chaining

License: MIT Python 3.8+

Pure functional programming with fluent chaining that reads like prose.

Fluent Chaining is a Python package that enables functional programming with method chaining that reads like natural language, making your code more expressive, readable, and maintainable.

✨ Why Fluent Chaining?

Traditional functional programming in Python can be verbose and hard to read:

# Traditional approach - hard to read
from functools import reduce

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = reduce(
    lambda acc, x: acc + x,
    map(lambda x: x ** 2,
        filter(lambda x: x % 2 == 0, numbers)),
    0
)

With Fluent Chaining, the same operation reads like prose:

# Fluent Chaining - reads like natural language
from fluent_chaining import take

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = (take(numbers)
    .where(lambda x: x % 2 == 0)
    .transform(lambda x: x ** 2)
    .sum()
    .value())

🚀 Installation (Project-Saturday Members Only)

Quick Setup (Recommended)

# 1. Set up your credentials (one-time)
cp docs/setup/team-secrets.template team-secrets.env
# Edit team-secrets.env with your GitHub PAT token

# 2. Run automated setup
python scripts/setup-team-environment.py

# 3. Install the package
pip install fluent-chaining

Alternative Installation Methods

GitHub Packages (manual setup):

# Configure pip for GitHub Packages
pip config set global.extra-index-url https://pypi.pkg.github.com/Project-Saturday/simple/
pip config set global.trusted-host pypi.pkg.github.com

# Install with your .pypirc configured
pip install fluent-chaining

Direct Git Installation:

# Install latest from git
pip install git+https://github.com/Project-Saturday/fluent-chaining.git

# Install specific version
pip install git+https://github.com/Project-Saturday/fluent-chaining.git@v0.1.0

Note: This is a private Project-Saturday library. You'll need a GitHub Personal Access Token with packages:read permission. See docs/setup/setup-github-packages.md for detailed setup instructions.

📖 Basic Usage

Getting Started

from fluent_chaining import take

# Start with data using take()
data = [1, 2, 3, 4, 5]
result = take(data).transform(lambda x: x * 2).value()
# Result: [2, 4, 6, 8, 10]

Core Operations

Filtering with Natural Language

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Using prose-like methods
even_numbers = (take(numbers)
    .where(lambda x: x % 2 == 0)
    .value())

# Even more natural
large_numbers = (take(numbers)
    .that_are_greater_than(5)
    .value())

# Chaining conditions
result = (take(numbers)
    .where(lambda x: x % 2 == 0)
    .that_are_greater_than(4)
    .value())
# Result: [6, 8, 10]

Transformations

numbers = [1, 2, 3, 4, 5]

# Mathematical operations with readable syntax
result = (take(numbers)
    .transform(lambda x: x * 2)
    .plus(1)
    .value())
# Result: [3, 5, 7, 9, 11]

# Multiple transformations
result = (take(numbers)
    .multiplied_by(3)
    .minus(1)
    .divided_by(2)
    .value())
# Result: [1.0, 2.5, 4.0, 5.5, 7.0]

Aggregations

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Simple aggregations
total = take(numbers).sum().value()
count = take(numbers).count().value()
maximum = take(numbers).max().value()
minimum = take(numbers).min().value()

# Complex chained operations
average_of_squares = (take(numbers)
    .where(lambda x: x % 2 == 0)
    .transform(lambda x: x ** 2)
    .sum()
    .divided_by(take(numbers).where(lambda x: x % 2 == 0).count().value())
    .value())

🎯 Real-World Examples

Data Processing Pipeline

from fluent_chaining import take

# Process a list of user records
users = [
    {"name": "Alice", "age": 25, "city": "New York", "salary": 50000},
    {"name": "Bob", "age": 30, "city": "San Francisco", "salary": 75000},
    {"name": "Charlie", "age": 35, "city": "New York", "salary": 60000},
    {"name": "Diana", "age": 28, "city": "Boston", "salary": 55000},
]

# Find the average salary of users over 25 in New York
average_salary = (take(users)
    .where(lambda user: user["age"] > 25)
    .where(lambda user: user["city"] == "New York")
    .transform(lambda user: user["salary"])
    .sum()
    .divided_by(
        take(users)
        .where(lambda user: user["age"] > 25)
        .where(lambda user: user["city"] == "New York")
        .count()
        .value()
    )
    .value())

Text Processing

text = ["hello", "world", "this", "is", "a", "test"]

# Process text with readable operations
result = (take(text)
    .where(lambda word: len(word) > 3)
    .transform(str.upper)
    .transform(lambda word: f"[{word}]")
    .value())
# Result: ['[HELLO]', '[WORLD]', '[THIS]', '[TEST]']

Mathematical Computations

# Calculate factorial using fluent chaining
def factorial(n):
    return (take(range(1, n + 1))
        .reduce(lambda acc, x: acc * x, 1)
        .value())

# Find prime numbers with descriptive operations
def is_prime(n):
    if n < 2:
        return False
    return (take(range(2, int(n ** 0.5) + 1))
        .where(lambda x: n % x == 0)
        .count()
        .value()) == 0

primes = (take(range(2, 20))
    .where(is_prime)
    .value())

🔧 Advanced Features

Working with Collections

nested_data = [[1, 2], [3, 4], [5, 6]]

# Flatten and process
result = (take(nested_data)
    .flatten()
    .where(lambda x: x % 2 == 0)
    .value())
# Result: [2, 4, 6]

# Remove duplicates
data_with_dupes = [1, 2, 2, 3, 3, 3, 4]
unique_values = (take(data_with_dupes)
    .distinct()
    .value())
# Result: [1, 2, 3, 4]

Sorting and Ordering

people = [
    {"name": "Alice", "age": 25},
    {"name": "Bob", "age": 30},
    {"name": "Charlie", "age": 20},
]

# Sort by age
sorted_people = (take(people)
    .sort(key=lambda person: person["age"])
    .value())

# Reverse order
reversed_ages = (take(people)
    .transform(lambda person: person["age"])
    .sort(reverse=True)
    .value())

Grouping Operations

students = [
    {"name": "Alice", "grade": "A"},
    {"name": "Bob", "grade": "B"},
    {"name": "Charlie", "grade": "A"},
    {"name": "Diana", "grade": "B"},
]

# Group students by grade
groups = (take(students)
    .group_by(lambda student: student["grade"])
    .value())

🛠️ Method Reference

Entry Points

  • take(data) - Start a fluent chain with data
  • chain(data) - Alias for take()

Filtering Methods

  • .where(predicate) - Filter elements that satisfy the predicate
  • .filter(predicate) - Alias for where()
  • .that_are(predicate) - Natural language filtering
  • .that_are_greater_than(value) - Filter elements > value
  • .that_are_less_than(value) - Filter elements < value
  • .that_equal(value) - Filter elements == value

Transformation Methods

  • .transform(func) - Transform each element
  • .map(func) - Alias for transform()
  • .multiplied_by(factor) - Multiply by factor
  • .divided_by(divisor) - Divide by divisor
  • .plus(addend) - Add value
  • .minus(subtrahend) - Subtract value

Aggregation Methods

  • .sum() - Sum all elements
  • .count() - Count elements
  • .length() - Alias for count()
  • .max() - Maximum element
  • .min() - Minimum element
  • .first() - First element
  • .last() - Last element

Collection Methods

  • .take(n) - Take first n elements
  • .skip(n) - Skip first n elements
  • .distinct() - Remove duplicates
  • .unique() - Alias for distinct()
  • .reverse() - Reverse order
  • .sort(key=None, reverse=False) - Sort elements
  • .flatten() - Flatten nested collections
  • .group_by(key_func) - Group by key function

Reduction Methods

  • .reduce(func, initial=None) - Reduce to single value
  • .fold(func, initial) - Fold with initial value

Output Methods

  • .value() - Get the final result
  • .to_list() - Convert to list

🧪 Advanced Functional Programming

The package includes comprehensive utilities for advanced functional programming:

Function Composition and Utilities

from fluent_chaining import compose, pipe, curry, partial, memoize

# Function composition
add_one = lambda x: x + 1
multiply_by_two = lambda x: x * 2
composed = compose(multiply_by_two, add_one)

result = take([1, 2, 3]).transform(composed).value()
# Result: [4, 6, 8]

# Function piping (left to right)
piped = pipe(add_one, multiply_by_two)
result = take([1, 2, 3]).transform(piped).value()
# Result: [4, 6, 8]

# Currying
@curry
def add_three_numbers(x, y, z):
    return x + y + z

add_five = add_three_numbers(2)(3)
result = take([1, 2, 3]).transform(add_five).value()
# Result: [6, 7, 8]

# Memoization for expensive computations
@memoize
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

fib_sequence = take(range(10)).transform(fibonacci).value()
# Result: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Advanced Chain Methods

# Debugging and side effects
result = (take([1, 2, 3, 4, 5])
    .where(lambda x: x % 2 == 0)
    .debug("Even numbers")  # Prints: Debug: [2, 4]
    .transform(lambda x: x ** 2)
    .side_effect(lambda data: print(f"Squared: {data}"))
    .sum()
    .value())

# Conditional processing
def double_if_small(chain):
    return chain.transform(lambda x: x * 2)

result = (take([1, 2, 3])
    .apply_if(True, double_if_small)  # Apply transformation conditionally
    .apply_when(lambda data: sum(data) > 10, lambda c: c.plus(5))  # Apply when condition met
    .value())

# Branching logic
result = (take([1, 2, 3, 4, 5])
    .branch(
        len([1, 2, 3, 4, 5]) > 3,
        lambda c: c.take(3),  # If true: take first 3
        lambda c: c.reverse()  # If false: reverse
    )
    .sum()
    .value())

# Error handling and defaults
result = (take([])
    .or_else([1, 2, 3])  # Use default if empty
    .assert_that(lambda data: len(data) > 0, "Must not be empty")
    .sum()
    .value())

Quantification and Higher-Order Functions

from fluent_chaining import always, exists, identity, constant

# Universal and existential quantification
is_positive = lambda x: x > 0
all_positive = always(is_positive)
any_positive = exists(is_positive)

result1 = all_positive([1, 2, 3])  # True
result2 = any_positive([-1, 0, 1])  # True

# Utility functions
numbers = [1, 2, 3, 4, 5]
result = (take(numbers)
    .transform(identity)  # No-op transformation
    .where(constant(True))  # Always true predicate
    .value())

🚀 Monadic Types (.NET-Style Functional Programming)

Inspired by .NET's functional programming patterns, we provide Result<T> and Option<T> types for robust error and null handling.

Result - Functional Error Handling

No more exception handling - make errors explicit and composable:

from fluent_chaining import Result, Ok, Err, safe, try_parse_int

# Safe operations that return Result<T>
def safe_divide(x, y):
    if y == 0:
        return Err("Division by zero")
    return Ok(x / y)

# Chain operations with .then() - like .NET's Result.Then()
result = (safe_divide(10, 2)
    .then(lambda x: safe_divide(x, 2))
    .then(lambda x: Ok(x + 1))
    .match(
        ok_func=lambda value: f"Success: {value}",
        err_func=lambda error: f"Error: {error}"
    ))
# Result: "Success: 3.5"

# Convert exception-throwing functions with @safe decorator
@safe
def risky_operation(value):
    if value < 0:
        raise ValueError("Negative value not allowed")
    return value * 2

results = [risky_operation(x) for x in [1, -2, 3]]
successful_values = [r.unwrap() for r in results if r.is_ok()]
# successful_values: [2, 6]

# Parse and process data safely
input_data = ["10", "20", "invalid", "30"]
parsed_results = [try_parse_int(s) for s in input_data]

total = (take([r.unwrap() for r in parsed_results if r.is_ok()])
    .sum()
    .value())
# total: 60 (10 + 20 + 30, "invalid" safely ignored)

Option - Functional Null Handling

Handle null/missing values functionally with pattern matching:

from fluent_chaining import Some, Nothing, from_nullable, from_dict

# Create Options
user_name = Some("Alice")
missing_data = Nothing()

# Pattern matching - like .NET's Option.Match()
def process_user(name_option):
    return name_option.match(
        some_func=lambda name: f"Processing user: {name}",
        none_func=lambda: "No user to process"
    )

print(process_user(user_name))    # "Processing user: Alice"
print(process_user(missing_data)) # "No user to process"

# Safe data access
user_data = {"name": "Bob", "profile": {"age": 30}}

age_message = (from_dict(user_data, "profile")
    .then(lambda profile: from_dict(profile, "age"))
    .match(
        some_func=lambda age: f"User is {age} years old",
        none_func=lambda: "Age not available"
    ))
# Result: "User is 30 years old"

# Chain multiple optional operations
def get_user_email_domain(user):
    return (from_dict(user, "email")
        .map(lambda email: email.split("@"))
        .filter(lambda parts: len(parts) == 2)
        .map(lambda parts: parts[1])
        .unwrap_or("unknown"))

users = [
    {"email": "alice@example.com"},
    {"email": "invalid-email"},
    {"name": "Bob"}  # No email
]

domains = [get_user_email_domain(user) for user in users]
# domains: ["example.com", "unknown", "unknown"]

Integration with Fluent Chains

Convert between chains and monadic types seamlessly:

# Chain to Result/Option
numbers = [1, 2, 3, 4, 5]
result = (take(numbers)
    .where(lambda x: x > 10)
    .to_result("No large numbers found"))

result.match(
    ok_func=lambda values: f"Found: {values}",
    err_func=lambda error: f"Error: {error}"
)
# Result: "Error: No large numbers found"

# Option to Chain
optional_data = Some([1, 2, 3])
total = (from_option(optional_data, [])
    .sum()
    .value())
# total: 6

# Complex pipeline with error handling
def safe_process_scores(scores_text):
    return (try_parse_int(scores_text)
        .filter(lambda score: 0 <= score <= 100, "Score out of range")
        .map(lambda score: score * 1.1)  # Apply 10% bonus
        .map(lambda score: min(score, 100)))  # Cap at 100

test_scores = ["85", "95", "invalid", "105", "-5"]
processed = [safe_process_scores(s) for s in test_scores]

# Extract successful scores
final_scores = [r.unwrap() for r in processed if r.is_ok()]
average = take(final_scores).sum().value() / len(final_scores) if final_scores else 0
# average: ~96.5 (from 85*1.1=93.5 and 95*1.1=100 capped)

🤝 Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🌟 Project Saturday

This package is part of the Project Saturday organization, dedicated to creating tools that make programming more expressive and enjoyable.


Made with ❤️ by the Project Saturday team

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

fluent_chaining-0.1.0.tar.gz (33.4 kB view details)

Uploaded Source

Built Distribution

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

fluent_chaining-0.1.0-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for fluent_chaining-0.1.0.tar.gz
Algorithm Hash digest
SHA256 66bbf824c44e91ec59ca33e1571f7c4664bca27f3630959f6ca08dc0cd9491f9
MD5 a7055d8abf8821bf46c96bd7a2babe93
BLAKE2b-256 d6d16c10093bc6599b6ed32a3153823ffd48acd04dfe6be5e862e1abb41da572

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for fluent_chaining-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 31721932456fd308705fe036c05ba710121a5149267408f935ca023355b2fc13
MD5 89e00b0fe505bac48578cb5e2bda616f
BLAKE2b-256 3b2ad335bd889b5df97c25c55600fc599a6c08d63223a4a28c7e871c57623dcf

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