Skip to content

Core Concepts

This guide explains the main ideas behind fptk without getting too theoretical. We'll focus on practical usage with real examples.

Functions as Building Blocks

fptk treats functions as reusable building blocks that you can combine in different ways.

pipe(): Linear Data Flow

pipe() threads data through functions in sequence:

from fptk.core.func import pipe

def process_user_data(raw_data):
    return pipe(
        raw_data,
        parse_json,      # Step 1: parse
        validate_user,   # Step 2: validate
        save_to_db,      # Step 3: save
        send_welcome     # Step 4: notify
    )

Benefits:

  • Easy to read (top to bottom)
  • Easy to add/remove steps
  • Easy to test individual steps

compose(): Function Building

compose() combines functions into new functions:

from fptk.core.func import compose

# Create a new function from existing ones
process_and_save = compose(save_to_db, validate_user, parse_json)

# Use it
result = process_and_save(raw_data)

Note: compose applies functions right-to-left: compose(f, g)(x) = f(g(x)).

curry(): Flexible Function Calls

curry() lets you call functions partially:

from fptk.core.func import curry

def send_email(to, subject, body):
    # Send email logic
    pass

# Create specialized functions
send_support_email = curry(send_email)("support@company.com")
notify_user = send_support_email("Welcome!")

# Use them
notify_user("Welcome to our platform!")

Handling Missing Data with Option

Python's None is error-prone. fptk's Option makes absence explicit.

Basic Option Usage

from fptk.adt.option import Some, NOTHING, from_nullable

# Convert potentially None values
name = from_nullable(user.get('name'))  # Some("Alice") or NOTHING

# Handle absence safely
display_name = name.map(lambda n: n.upper()).unwrap_or("Anonymous")

Chaining Optional Operations

def get_full_name(user):
    return (
        from_nullable(user.get('first_name'))
        .zip(from_nullable(user.get('last_name')))
        .map(lambda names: f"{names[0]} {names[1]}")
        .or_else(lambda: from_nullable(user.get('display_name')))
        .unwrap_or('Anonymous')
    )

get_full_name({'first_name': 'John', 'last_name': 'Doe'})  # "John Doe"
get_full_name({'display_name': 'Johnny'})                   # "Johnny"
get_full_name({})                                           # "Anonymous"

Key Operations:

Method Description
map(f) Transform the value if present
bind(f) Chain operations that return Option
zip(other) Combine two Options into tuple
or_else(f) Provide fallback Option
unwrap_or(default) Get value or default

Error Handling with Result

Exceptions are great for unexpected errors, but for expected failures (validation, parsing, etc.), Result is clearer.

Basic Result Usage

from fptk.adt.result import Ok, Err, Result

def divide(a: int, b: int) -> Result[int, str]:
    if b == 0:
        return Err("Division by zero")
    return Ok(a // b)

result = divide(10, 2)  # Ok(5)
error = divide(10, 0)   # Err("Division by zero")

Chaining Results

def process_payment(amount, card_number):
    return (
        validate_amount(amount)
        .bind(lambda amt: validate_card(card_number))
        .bind(lambda card: charge_card(amount, card))
    )

# Either Ok(success_data) or Err(error_message)
result = process_payment(100, "4111111111111111")

Key Operations:

Method Description
map(f) Transform success value
bind(f) Chain operations returning Result
map_err(f) Transform error
unwrap_or(default) Get value or default

Working with Collections

fptk provides lazy utilities for processing collections efficiently.

Lazy Processing

from fptk.core.func import pipe
from fptk.iter.lazy import map_iter, filter_iter

# Process large datasets without loading everything
def process_logs(logs):
    return pipe(
        logs,
        lambda ls: filter_iter(lambda log: log['level'] == 'ERROR', ls),
        lambda ls: map_iter(lambda log: log['message'], ls),
        list
    )

Grouping and Chunking

from fptk.iter.lazy import group_by_key, chunk

# Group data by category (input must be sorted by key)
grouped = dict(group_by_key(users, lambda u: u['department']))

# Process in batches
for user_batch in chunk(users, 10):
    process_batch(user_batch)

Async Operations

Handle concurrent operations with proper error handling.

Gathering Results

from fptk.async_tools import gather_results

async def fetch_user_data(user_ids):
    tasks = [fetch_user_api(uid) for uid in user_ids]
    # Returns Ok([user_data]) or Err(first_error)
    return await gather_results(tasks)

Async Pipelines

from fptk.core.func import async_pipe

async def process_request(request):
    return await async_pipe(
        request,
        parse_async,
        validate_async,
        save_async,
        notify_async
    )

Validation

Accumulate multiple validation errors instead of failing fast.

from fptk.adt.result import Ok, Err
from fptk.validate import validate_all

def validate_user(user):
    return validate_all([
        lambda u: Ok(u) if u.get('email') else Err("Email required"),
        lambda u: Ok(u) if '@' in u.get('email', '') else Err("Invalid email"),
        lambda u: Ok(u) if len(u.get('password', '')) >= 8 else Err("Password too short")
    ], user)

validate_user({'email': 'invalid', 'password': 'short'})
# Err(NonEmptyList("Invalid email", "Password too short"))

Putting It All Together

Here's a complete example combining multiple concepts:

from fptk.core.func import pipe
from fptk.adt.result import Ok, Err
from fptk.validate import validate_all

def process_registration(data):
    return pipe(
        data,
        validate_registration,
        lambda valid: valid.bind(save_user),
        lambda saved: saved.bind(send_welcome_email),
        lambda result: result.map(lambda user: {
            'user_id': user['id'],
            'message': 'Registration successful'
        })
    )

def validate_registration(data):
    return validate_all([
        lambda d: Ok(d) if d.get('email') else Err("Email required"),
        lambda d: Ok(d) if d.get('password') else Err("Password required"),
    ], data)

# Usage
result = process_registration({
    'email': 'user@example.com',
    'password': 'secure123'
})
# Ok({'user_id': 123, 'message': 'Registration successful'})

This example shows how fptk concepts work together to create robust, readable code.