# Fluent Chaining
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
> 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:
```python
# 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:
```python
# 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)
```bash
# 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):**
```bash
# 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:**
```bash
# 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`](docs/setup/setup-github-packages.md) for detailed setup instructions.
## ๐ Basic Usage
### Getting Started
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
# 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
```python
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
```python
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
```python
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
```python
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
```python
# 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
```python
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<T> - Functional Error Handling
No more exception handling - make errors explicit and composable:
```python
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<T> - Functional Null Handling
Handle null/missing values functionally with pattern matching:
```python
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:
```python
# 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](CONTRIBUTING.md) for details.
## ๐ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Project Saturday
This package is part of the [Project Saturday](https://github.com/Project-Saturday) organization, dedicated to creating tools that make programming more expressive and enjoyable.
---
*Made with โค๏ธ by the Project Saturday team*
Raw data
{
"_id": null,
"home_page": null,
"name": "fluent-chaining",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "functional, chaining, fluent, pure-functions, compose",
"author": null,
"author_email": "Project-Saturday <contact@project-saturday.org>",
"download_url": "https://files.pythonhosted.org/packages/d6/d1/6c10093bc6599b6ed32a3153823ffd48acd04dfe6be5e862e1abb41da572/fluent_chaining-0.1.0.tar.gz",
"platform": null,
"description": "# Fluent Chaining\n\n[](https://opensource.org/licenses/MIT)\n[](https://www.python.org/downloads/)\n\n> Pure functional programming with fluent chaining that reads like prose.\n\nFluent 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.\n\n## \u2728 Why Fluent Chaining?\n\nTraditional functional programming in Python can be verbose and hard to read:\n\n```python\n# Traditional approach - hard to read\nfrom functools import reduce\n\nnumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\nresult = reduce(\n lambda acc, x: acc + x,\n map(lambda x: x ** 2,\n filter(lambda x: x % 2 == 0, numbers)),\n 0\n)\n```\n\nWith Fluent Chaining, the same operation reads like prose:\n\n```python\n# Fluent Chaining - reads like natural language\nfrom fluent_chaining import take\n\nnumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\nresult = (take(numbers)\n .where(lambda x: x % 2 == 0)\n .transform(lambda x: x ** 2)\n .sum()\n .value())\n```\n\n## \ud83d\ude80 Installation (Project-Saturday Members Only)\n\n### Quick Setup (Recommended)\n```bash\n# 1. Set up your credentials (one-time)\ncp docs/setup/team-secrets.template team-secrets.env\n# Edit team-secrets.env with your GitHub PAT token\n\n# 2. Run automated setup\npython scripts/setup-team-environment.py\n\n# 3. Install the package\npip install fluent-chaining\n```\n\n### Alternative Installation Methods\n\n**GitHub Packages (manual setup):**\n```bash\n# Configure pip for GitHub Packages\npip config set global.extra-index-url https://pypi.pkg.github.com/Project-Saturday/simple/\npip config set global.trusted-host pypi.pkg.github.com\n\n# Install with your .pypirc configured\npip install fluent-chaining\n```\n\n**Direct Git Installation:**\n```bash\n# Install latest from git\npip install git+https://github.com/Project-Saturday/fluent-chaining.git\n\n# Install specific version\npip install git+https://github.com/Project-Saturday/fluent-chaining.git@v0.1.0\n```\n\n> **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`](docs/setup/setup-github-packages.md) for detailed setup instructions.\n\n## \ud83d\udcd6 Basic Usage\n\n### Getting Started\n\n```python\nfrom fluent_chaining import take\n\n# Start with data using take()\ndata = [1, 2, 3, 4, 5]\nresult = take(data).transform(lambda x: x * 2).value()\n# Result: [2, 4, 6, 8, 10]\n```\n\n### Core Operations\n\n#### Filtering with Natural Language\n\n```python\nnumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n\n# Using prose-like methods\neven_numbers = (take(numbers)\n .where(lambda x: x % 2 == 0)\n .value())\n\n# Even more natural\nlarge_numbers = (take(numbers)\n .that_are_greater_than(5)\n .value())\n\n# Chaining conditions\nresult = (take(numbers)\n .where(lambda x: x % 2 == 0)\n .that_are_greater_than(4)\n .value())\n# Result: [6, 8, 10]\n```\n\n#### Transformations\n\n```python\nnumbers = [1, 2, 3, 4, 5]\n\n# Mathematical operations with readable syntax\nresult = (take(numbers)\n .transform(lambda x: x * 2)\n .plus(1)\n .value())\n# Result: [3, 5, 7, 9, 11]\n\n# Multiple transformations\nresult = (take(numbers)\n .multiplied_by(3)\n .minus(1)\n .divided_by(2)\n .value())\n# Result: [1.0, 2.5, 4.0, 5.5, 7.0]\n```\n\n#### Aggregations\n\n```python\nnumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n\n# Simple aggregations\ntotal = take(numbers).sum().value()\ncount = take(numbers).count().value()\nmaximum = take(numbers).max().value()\nminimum = take(numbers).min().value()\n\n# Complex chained operations\naverage_of_squares = (take(numbers)\n .where(lambda x: x % 2 == 0)\n .transform(lambda x: x ** 2)\n .sum()\n .divided_by(take(numbers).where(lambda x: x % 2 == 0).count().value())\n .value())\n```\n\n## \ud83c\udfaf Real-World Examples\n\n### Data Processing Pipeline\n\n```python\nfrom fluent_chaining import take\n\n# Process a list of user records\nusers = [\n {\"name\": \"Alice\", \"age\": 25, \"city\": \"New York\", \"salary\": 50000},\n {\"name\": \"Bob\", \"age\": 30, \"city\": \"San Francisco\", \"salary\": 75000},\n {\"name\": \"Charlie\", \"age\": 35, \"city\": \"New York\", \"salary\": 60000},\n {\"name\": \"Diana\", \"age\": 28, \"city\": \"Boston\", \"salary\": 55000},\n]\n\n# Find the average salary of users over 25 in New York\naverage_salary = (take(users)\n .where(lambda user: user[\"age\"] > 25)\n .where(lambda user: user[\"city\"] == \"New York\")\n .transform(lambda user: user[\"salary\"])\n .sum()\n .divided_by(\n take(users)\n .where(lambda user: user[\"age\"] > 25)\n .where(lambda user: user[\"city\"] == \"New York\")\n .count()\n .value()\n )\n .value())\n```\n\n### Text Processing\n\n```python\ntext = [\"hello\", \"world\", \"this\", \"is\", \"a\", \"test\"]\n\n# Process text with readable operations\nresult = (take(text)\n .where(lambda word: len(word) > 3)\n .transform(str.upper)\n .transform(lambda word: f\"[{word}]\")\n .value())\n# Result: ['[HELLO]', '[WORLD]', '[THIS]', '[TEST]']\n```\n\n### Mathematical Computations\n\n```python\n# Calculate factorial using fluent chaining\ndef factorial(n):\n return (take(range(1, n + 1))\n .reduce(lambda acc, x: acc * x, 1)\n .value())\n\n# Find prime numbers with descriptive operations\ndef is_prime(n):\n if n < 2:\n return False\n return (take(range(2, int(n ** 0.5) + 1))\n .where(lambda x: n % x == 0)\n .count()\n .value()) == 0\n\nprimes = (take(range(2, 20))\n .where(is_prime)\n .value())\n```\n\n## \ud83d\udd27 Advanced Features\n\n### Working with Collections\n\n```python\nnested_data = [[1, 2], [3, 4], [5, 6]]\n\n# Flatten and process\nresult = (take(nested_data)\n .flatten()\n .where(lambda x: x % 2 == 0)\n .value())\n# Result: [2, 4, 6]\n\n# Remove duplicates\ndata_with_dupes = [1, 2, 2, 3, 3, 3, 4]\nunique_values = (take(data_with_dupes)\n .distinct()\n .value())\n# Result: [1, 2, 3, 4]\n```\n\n### Sorting and Ordering\n\n```python\npeople = [\n {\"name\": \"Alice\", \"age\": 25},\n {\"name\": \"Bob\", \"age\": 30},\n {\"name\": \"Charlie\", \"age\": 20},\n]\n\n# Sort by age\nsorted_people = (take(people)\n .sort(key=lambda person: person[\"age\"])\n .value())\n\n# Reverse order\nreversed_ages = (take(people)\n .transform(lambda person: person[\"age\"])\n .sort(reverse=True)\n .value())\n```\n\n### Grouping Operations\n\n```python\nstudents = [\n {\"name\": \"Alice\", \"grade\": \"A\"},\n {\"name\": \"Bob\", \"grade\": \"B\"},\n {\"name\": \"Charlie\", \"grade\": \"A\"},\n {\"name\": \"Diana\", \"grade\": \"B\"},\n]\n\n# Group students by grade\ngroups = (take(students)\n .group_by(lambda student: student[\"grade\"])\n .value())\n```\n\n## \ud83d\udee0\ufe0f Method Reference\n\n### Entry Points\n- `take(data)` - Start a fluent chain with data\n- `chain(data)` - Alias for `take()`\n\n### Filtering Methods\n- `.where(predicate)` - Filter elements that satisfy the predicate\n- `.filter(predicate)` - Alias for `where()`\n- `.that_are(predicate)` - Natural language filtering\n- `.that_are_greater_than(value)` - Filter elements > value\n- `.that_are_less_than(value)` - Filter elements < value\n- `.that_equal(value)` - Filter elements == value\n\n### Transformation Methods\n- `.transform(func)` - Transform each element\n- `.map(func)` - Alias for `transform()`\n- `.multiplied_by(factor)` - Multiply by factor\n- `.divided_by(divisor)` - Divide by divisor\n- `.plus(addend)` - Add value\n- `.minus(subtrahend)` - Subtract value\n\n### Aggregation Methods\n- `.sum()` - Sum all elements\n- `.count()` - Count elements\n- `.length()` - Alias for `count()`\n- `.max()` - Maximum element\n- `.min()` - Minimum element\n- `.first()` - First element\n- `.last()` - Last element\n\n### Collection Methods\n- `.take(n)` - Take first n elements\n- `.skip(n)` - Skip first n elements\n- `.distinct()` - Remove duplicates\n- `.unique()` - Alias for `distinct()`\n- `.reverse()` - Reverse order\n- `.sort(key=None, reverse=False)` - Sort elements\n- `.flatten()` - Flatten nested collections\n- `.group_by(key_func)` - Group by key function\n\n### Reduction Methods\n- `.reduce(func, initial=None)` - Reduce to single value\n- `.fold(func, initial)` - Fold with initial value\n\n### Output Methods\n- `.value()` - Get the final result\n- `.to_list()` - Convert to list\n\n## \ud83e\uddea Advanced Functional Programming\n\nThe package includes comprehensive utilities for advanced functional programming:\n\n### Function Composition and Utilities\n\n```python\nfrom fluent_chaining import compose, pipe, curry, partial, memoize\n\n# Function composition\nadd_one = lambda x: x + 1\nmultiply_by_two = lambda x: x * 2\ncomposed = compose(multiply_by_two, add_one)\n\nresult = take([1, 2, 3]).transform(composed).value()\n# Result: [4, 6, 8]\n\n# Function piping (left to right)\npiped = pipe(add_one, multiply_by_two)\nresult = take([1, 2, 3]).transform(piped).value()\n# Result: [4, 6, 8]\n\n# Currying\n@curry\ndef add_three_numbers(x, y, z):\n return x + y + z\n\nadd_five = add_three_numbers(2)(3)\nresult = take([1, 2, 3]).transform(add_five).value()\n# Result: [6, 7, 8]\n\n# Memoization for expensive computations\n@memoize\ndef fibonacci(n):\n if n < 2:\n return n\n return fibonacci(n-1) + fibonacci(n-2)\n\nfib_sequence = take(range(10)).transform(fibonacci).value()\n# Result: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]\n```\n\n### Advanced Chain Methods\n\n```python\n# Debugging and side effects\nresult = (take([1, 2, 3, 4, 5])\n .where(lambda x: x % 2 == 0)\n .debug(\"Even numbers\") # Prints: Debug: [2, 4]\n .transform(lambda x: x ** 2)\n .side_effect(lambda data: print(f\"Squared: {data}\"))\n .sum()\n .value())\n\n# Conditional processing\ndef double_if_small(chain):\n return chain.transform(lambda x: x * 2)\n\nresult = (take([1, 2, 3])\n .apply_if(True, double_if_small) # Apply transformation conditionally\n .apply_when(lambda data: sum(data) > 10, lambda c: c.plus(5)) # Apply when condition met\n .value())\n\n# Branching logic\nresult = (take([1, 2, 3, 4, 5])\n .branch(\n len([1, 2, 3, 4, 5]) > 3,\n lambda c: c.take(3), # If true: take first 3\n lambda c: c.reverse() # If false: reverse\n )\n .sum()\n .value())\n\n# Error handling and defaults\nresult = (take([])\n .or_else([1, 2, 3]) # Use default if empty\n .assert_that(lambda data: len(data) > 0, \"Must not be empty\")\n .sum()\n .value())\n```\n\n### Quantification and Higher-Order Functions\n\n```python\nfrom fluent_chaining import always, exists, identity, constant\n\n# Universal and existential quantification\nis_positive = lambda x: x > 0\nall_positive = always(is_positive)\nany_positive = exists(is_positive)\n\nresult1 = all_positive([1, 2, 3]) # True\nresult2 = any_positive([-1, 0, 1]) # True\n\n# Utility functions\nnumbers = [1, 2, 3, 4, 5]\nresult = (take(numbers)\n .transform(identity) # No-op transformation\n .where(constant(True)) # Always true predicate\n .value())\n```\n\n## \ud83d\ude80 Monadic Types (.NET-Style Functional Programming)\n\nInspired by .NET's functional programming patterns, we provide `Result<T>` and `Option<T>` types for robust error and null handling.\n\n### Result<T> - Functional Error Handling\n\nNo more exception handling - make errors explicit and composable:\n\n```python\nfrom fluent_chaining import Result, Ok, Err, safe, try_parse_int\n\n# Safe operations that return Result<T>\ndef safe_divide(x, y):\n if y == 0:\n return Err(\"Division by zero\")\n return Ok(x / y)\n\n# Chain operations with .then() - like .NET's Result.Then()\nresult = (safe_divide(10, 2)\n .then(lambda x: safe_divide(x, 2))\n .then(lambda x: Ok(x + 1))\n .match(\n ok_func=lambda value: f\"Success: {value}\",\n err_func=lambda error: f\"Error: {error}\"\n ))\n# Result: \"Success: 3.5\"\n\n# Convert exception-throwing functions with @safe decorator\n@safe\ndef risky_operation(value):\n if value < 0:\n raise ValueError(\"Negative value not allowed\")\n return value * 2\n\nresults = [risky_operation(x) for x in [1, -2, 3]]\nsuccessful_values = [r.unwrap() for r in results if r.is_ok()]\n# successful_values: [2, 6]\n\n# Parse and process data safely\ninput_data = [\"10\", \"20\", \"invalid\", \"30\"]\nparsed_results = [try_parse_int(s) for s in input_data]\n\ntotal = (take([r.unwrap() for r in parsed_results if r.is_ok()])\n .sum()\n .value())\n# total: 60 (10 + 20 + 30, \"invalid\" safely ignored)\n```\n\n### Option<T> - Functional Null Handling\n\nHandle null/missing values functionally with pattern matching:\n\n```python\nfrom fluent_chaining import Some, Nothing, from_nullable, from_dict\n\n# Create Options\nuser_name = Some(\"Alice\")\nmissing_data = Nothing()\n\n# Pattern matching - like .NET's Option.Match()\ndef process_user(name_option):\n return name_option.match(\n some_func=lambda name: f\"Processing user: {name}\",\n none_func=lambda: \"No user to process\"\n )\n\nprint(process_user(user_name)) # \"Processing user: Alice\"\nprint(process_user(missing_data)) # \"No user to process\"\n\n# Safe data access\nuser_data = {\"name\": \"Bob\", \"profile\": {\"age\": 30}}\n\nage_message = (from_dict(user_data, \"profile\")\n .then(lambda profile: from_dict(profile, \"age\"))\n .match(\n some_func=lambda age: f\"User is {age} years old\",\n none_func=lambda: \"Age not available\"\n ))\n# Result: \"User is 30 years old\"\n\n# Chain multiple optional operations\ndef get_user_email_domain(user):\n return (from_dict(user, \"email\")\n .map(lambda email: email.split(\"@\"))\n .filter(lambda parts: len(parts) == 2)\n .map(lambda parts: parts[1])\n .unwrap_or(\"unknown\"))\n\nusers = [\n {\"email\": \"alice@example.com\"},\n {\"email\": \"invalid-email\"},\n {\"name\": \"Bob\"} # No email\n]\n\ndomains = [get_user_email_domain(user) for user in users]\n# domains: [\"example.com\", \"unknown\", \"unknown\"]\n```\n\n### Integration with Fluent Chains\n\nConvert between chains and monadic types seamlessly:\n\n```python\n# Chain to Result/Option\nnumbers = [1, 2, 3, 4, 5]\nresult = (take(numbers)\n .where(lambda x: x > 10)\n .to_result(\"No large numbers found\"))\n\nresult.match(\n ok_func=lambda values: f\"Found: {values}\",\n err_func=lambda error: f\"Error: {error}\"\n)\n# Result: \"Error: No large numbers found\"\n\n# Option to Chain\noptional_data = Some([1, 2, 3])\ntotal = (from_option(optional_data, [])\n .sum()\n .value())\n# total: 6\n\n# Complex pipeline with error handling\ndef safe_process_scores(scores_text):\n return (try_parse_int(scores_text)\n .filter(lambda score: 0 <= score <= 100, \"Score out of range\")\n .map(lambda score: score * 1.1) # Apply 10% bonus\n .map(lambda score: min(score, 100))) # Cap at 100\n\ntest_scores = [\"85\", \"95\", \"invalid\", \"105\", \"-5\"]\nprocessed = [safe_process_scores(s) for s in test_scores]\n\n# Extract successful scores\nfinal_scores = [r.unwrap() for r in processed if r.is_ok()]\naverage = take(final_scores).sum().value() / len(final_scores) if final_scores else 0\n# average: ~96.5 (from 85*1.1=93.5 and 95*1.1=100 capped)\n```\n\n## \ud83e\udd1d Contributing\n\nWe welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.\n\n## \ud83d\udcc4 License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n## \ud83c\udf1f Project Saturday\n\nThis package is part of the [Project Saturday](https://github.com/Project-Saturday) organization, dedicated to creating tools that make programming more expressive and enjoyable.\n\n---\n\n*Made with \u2764\ufe0f by the Project Saturday team*\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A Python package for pure functional programming with fluent chaining that reads like prose",
"version": "0.1.0",
"project_urls": {
"Bug Reports": "https://github.com/Project-Saturday/fluent-chaining/issues",
"Homepage": "https://github.com/Project-Saturday/fluent-chaining",
"Source": "https://github.com/Project-Saturday/fluent-chaining"
},
"split_keywords": [
"functional",
" chaining",
" fluent",
" pure-functions",
" compose"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "3b2ad335bd889b5df97c25c55600fc599a6c08d63223a4a28c7e871c57623dcf",
"md5": "89e00b0fe505bac48578cb5e2bda616f",
"sha256": "31721932456fd308705fe036c05ba710121a5149267408f935ca023355b2fc13"
},
"downloads": -1,
"filename": "fluent_chaining-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "89e00b0fe505bac48578cb5e2bda616f",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 18091,
"upload_time": "2025-08-12T23:20:58",
"upload_time_iso_8601": "2025-08-12T23:20:58.818927Z",
"url": "https://files.pythonhosted.org/packages/3b/2a/d335bd889b5df97c25c55600fc599a6c08d63223a4a28c7e871c57623dcf/fluent_chaining-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "d6d16c10093bc6599b6ed32a3153823ffd48acd04dfe6be5e862e1abb41da572",
"md5": "a7055d8abf8821bf46c96bd7a2babe93",
"sha256": "66bbf824c44e91ec59ca33e1571f7c4664bca27f3630959f6ca08dc0cd9491f9"
},
"downloads": -1,
"filename": "fluent_chaining-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "a7055d8abf8821bf46c96bd7a2babe93",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 33444,
"upload_time": "2025-08-12T23:21:00",
"upload_time_iso_8601": "2025-08-12T23:21:00.739369Z",
"url": "https://files.pythonhosted.org/packages/d6/d1/6c10093bc6599b6ed32a3153823ffd48acd04dfe6be5e862e1abb41da572/fluent_chaining-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-08-12 23:21:00",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Project-Saturday",
"github_project": "fluent-chaining",
"github_not_found": true,
"lcname": "fluent-chaining"
}