Option¶
fptk.adt.option provides the Option type for handling values that might be absent. Instead of using None and checking for it everywhere, Option makes absence explicit and composable.
Concept: The Maybe/Option Monad¶
In functional programming, Option (also called Maybe in Haskell) represents a value that might not exist. It has two cases:
- Some(value): The value is present
- Nothing: The value is absent
This matters because:
- No null pointer exceptions: You can't accidentally call methods on
None - Explicit absence: The type signature tells you a value might be missing
- Composable transformations: Chain operations that gracefully handle missing values
The Problem with None¶
user = get_user(id)
name = user.get("profile").get("name").upper() # AttributeError if any is None!
# Defensive coding everywhere
if user and user.get("profile") and user.get("profile").get("name"):
name = user["profile"]["name"].upper()
else:
name = "Anonymous"
The Option Solution¶
from fptk.adt.option import from_nullable, Some, NOTHING
name = (
from_nullable(get_user(id))
.bind(lambda u: from_nullable(u.get("profile")))
.bind(lambda p: from_nullable(p.get("name")))
.map(str.upper)
.unwrap_or("Anonymous")
)
Each .bind() short-circuits to NOTHING if the previous step was absent. No exceptions, no nested conditionals.
API¶
Types¶
| Type | Description |
|---|---|
Option[T] |
Base type representing an optional value |
Some[T] |
Variant containing a present value |
Nothing |
Variant representing absence (singleton class) |
NOTHING |
The singleton instance of Nothing |
Constructors¶
from fptk.adt.option import Some, NOTHING, from_nullable
# Directly construct
present = Some(42)
absent = NOTHING
# From nullable value
from_nullable(some_value) # Some(x) if x is not None, else NOTHING
Methods¶
| Method | Signature | Description |
|---|---|---|
is_some() |
() -> bool |
Returns True if Some |
is_none() |
() -> bool |
Returns True if Nothing |
map(f) |
(T -> U) -> Option[U] |
Transform the value if present |
bind(f) |
(T -> Option[U]) -> Option[U] |
Chain Option-returning functions |
and_then(f) |
(T -> Option[U]) -> Option[U] |
Alias for bind (Rust naming) |
filter(p) |
(T -> bool) -> Option[T] |
Keep Some only if predicate holds |
flatten() |
Option[Option[T]] -> Option[T] |
Unwrap nested Option |
zip(other) |
(Option[U]) -> Option[tuple[T, U]] |
Combine two Options into tuple |
zip_with(other, f) |
(Option[U], (T, U) -> R) -> Option[R] |
Combine two Options with function |
ap(other) |
Option[T -> U].ap(Option[T]) -> Option[U] |
Apply wrapped function to wrapped value |
unwrap_or(default) |
(U) -> T | U |
Get value or default |
or_else(alt) |
(Option[T] | () -> Option[T]) -> Option[T] |
Alternative if absent |
to_result(err) |
(E) -> Result[T, E] |
Convert to Result |
match(some, none) |
(T -> U, () -> U) -> U |
Pattern match |
unwrap() |
() -> T |
Get value or raise ValueError |
expect(msg) |
(str) -> T |
Get value or raise with message |
Async Methods¶
| Method | Signature | Description |
|---|---|---|
map_async(f) |
async (T -> U) -> Option[U] |
Async transform |
bind_async(f) |
async (T -> Option[U]) -> Option[U] |
Async chain |
or_else: Eager vs Lazy¶
or_else accepts both a direct Option value and a callable returning Option:
from fptk.adt.option import Some, NOTHING
# Eager: value is always evaluated
result = NOTHING.or_else(Some(42)) # Some(42)
# Lazy: callable only invoked if needed
result = NOTHING.or_else(lambda: Some(expensive_computation()))
When to use which:
| Pattern | Syntax | Use when |
|---|---|---|
| Eager | .or_else(Some(x)) |
Default is cheap/already computed |
| Lazy | .or_else(lambda: ...) |
Default is expensive or has side effects |
# Fallback chain with lazy evaluation
config_value = (
from_nullable(os.getenv("MY_VAR"))
.or_else(lambda: from_nullable(config_file.get("my_var"))) # Only if env missing
.or_else(Some("default")) # Cheap, can be eager
)
How It Works¶
Data Structure¶
Option is implemented as a sealed type with two variants:
class Option[T]:
"""Base class - not instantiated directly."""
pass
@dataclass(frozen=True, slots=True)
class Some[T](Option[T]):
value: T
@dataclass(frozen=True, slots=True)
class Nothing(Option[None]):
pass
NOTHING = Nothing() # Singleton
The @dataclass(frozen=True, slots=True) makes instances immutable and memory-efficient.
The Functor: map¶
map applies a function to the value inside Some, or does nothing for Nothing:
This is the Functor operation: lifting a function A -> B to work on Option[A] -> Option[B].
The Monad: bind¶
bind (also called flatMap or >>=) chains operations that themselves return Option:
def bind(self, f):
if isinstance(self, Some):
return f(self.value) # f returns Option[U]
return NOTHING
This is the Monad operation. It prevents nested Option[Option[T]] by "flattening" the result.
Why bind vs map?¶
- Use
mapwhen your function returns a plain value:lambda x: x + 1 - Use
bindwhen your function returns anOption:lambda x: from_nullable(lookup(x))
# map: str -> str (plain value)
Some("hello").map(str.upper) # Some("HELLO")
# bind: str -> Option[int] (returns Option)
Some("42").bind(lambda s: from_nullable(safe_parse(s))) # Some(42) or NOTHING
Examples¶
Safe Dictionary Access¶
from fptk.adt.option import from_nullable
config = {"database": {"host": "localhost", "port": 5432}}
# Chain lookups safely
port = (
from_nullable(config.get("database"))
.bind(lambda db: from_nullable(db.get("port")))
.map(str)
.unwrap_or("5432")
)
Parsing User Input¶
def parse_int(s: str) -> Option[int]:
try:
return Some(int(s))
except ValueError:
return NOTHING
def parse_positive(s: str) -> Option[int]:
return parse_int(s).bind(
lambda n: Some(n) if n > 0 else NOTHING
)
parse_positive("42") # Some(42)
parse_positive("-1") # NOTHING
parse_positive("abc") # NOTHING
Filtering Values¶
Use filter to keep a Some only if it satisfies a predicate:
from fptk.adt.option import Some, NOTHING
# Keep only positive numbers
Some(5).filter(lambda x: x > 0) # Some(5)
Some(-3).filter(lambda x: x > 0) # NOTHING
NOTHING.filter(lambda x: x > 0) # NOTHING
# Practical example: validate user input
def get_valid_age(input: str) -> Option[int]:
return parse_int(input).filter(lambda age: 0 <= age <= 150)
get_valid_age("25") # Some(25)
get_valid_age("-5") # NOTHING (invalid age)
get_valid_age("200") # NOTHING (invalid age)
get_valid_age("abc") # NOTHING (parse failed)
Flattening Nested Options¶
Use flatten when you have an Option[Option[T]] and want Option[T]:
from fptk.adt.option import Some, NOTHING
# Direct usage
Some(Some(42)).flatten() # Some(42)
Some(NOTHING).flatten() # NOTHING
NOTHING.flatten() # NOTHING
# Common scenario: map with a function that returns Option
def get_user(id: int) -> Option[User]: ...
def get_manager(user: User) -> Option[User]: ...
# Without flatten: Option[Option[User]]
nested = get_user(1).map(get_manager)
# With flatten: Option[User]
manager = get_user(1).map(get_manager).flatten()
# Note: this is equivalent to using bind directly
manager = get_user(1).bind(get_manager)
Applicative Apply¶
Use ap to apply a wrapped function to a wrapped value:
from fptk.adt.option import Some, NOTHING
# Basic usage
Some(lambda x: x + 1).ap(Some(5)) # Some(6)
Some(lambda x: x + 1).ap(NOTHING) # NOTHING
NOTHING.ap(Some(5)) # NOTHING
# Curried functions for multiple arguments
def add(a: int):
return lambda b: a + b
Some(add).ap(Some(1)).ap(Some(2)) # Some(3)
# Practical example: combining optional values
def create_user(name: str):
return lambda email: {"name": name, "email": email}
user = Some(create_user).ap(from_nullable(name)).ap(from_nullable(email))
# Some({"name": ..., "email": ...}) if both present, else NOTHING
First-Available Value¶
from fptk.adt.option import from_nullable, NOTHING
def get_config_value(key: str) -> Option[str]:
"""Try environment, then file, then default."""
return (
from_nullable(os.getenv(key))
.or_else(lambda: from_nullable(config_file.get(key)))
.or_else(lambda: from_nullable(defaults.get(key)))
)
Pattern Matching¶
def describe(opt: Option[int]) -> str:
return opt.match(
some=lambda n: f"Got number: {n}",
none=lambda: "No value"
)
describe(Some(42)) # "Got number: 42"
describe(NOTHING) # "No value"
Converting to Result¶
from fptk.adt.option import from_nullable
def find_user(id: int) -> Option[User]:
return from_nullable(db.get(id))
# Convert to Result for error handling
result = find_user(42).to_result(f"User {id} not found")
# Ok(user) or Err("User 42 not found")
Iteration¶
from fptk.adt.option import Some, NOTHING
# Option implements __iter__ for zero-or-one elements
for value in Some(42):
print(value) # Prints 42
for value in NOTHING:
print(value) # Never executes
When to Use Option¶
Use Option when:
- A value might legitimately be absent (not an error condition)
- You want to chain transformations that might fail
- You're parsing or looking up values that might not exist
- You want to avoid
Nonechecks scattered through your code
Don't use Option when:
- Absence represents an error that should be reported → use
Result - You need to know why a value is missing → use
Resultwith error info - Performance is critical in tight loops → Option has some overhead
See Also¶
Result— When absence is an error with informationfrom_nullable— Bridge from Python'sNonetoOptiontraverse_option— Collect multiple Options into one