better-py

Mappable Protocol

Apply functions over values in a context

The Mappable protocol defines the ability to apply a function to values in a context, similar to fmap in Haskell or map in other functional languages. It's the foundation of functor-like operations in better-py.

A Mappable is a container or context that can have a function applied to its contents, producing a new Mappable with the result.

Understanding Mappable

The Mappable protocol represents types that support map operations. The key idea is that you can transform the contents of a container without changing its structure.

from better_py import Maybe, Some

# Transform the value inside a Maybe
result = Some(5).map(lambda x: x * 2)
# Some(10)

Use Mappable when you need to apply transformations to values wrapped in contexts like Maybe, Result, or PersistentList.

Implementing Mappable

To implement Mappable for your own types, define a map method that applies a function to the contained value.

from better_py.protocols import Mappable

class Box(Mappable):
    def __init__(self, value):
        self.value = value

    def map(self, f):
        return Box(f(self.value))

The map method must:

  • Accept a function from the inner type to a new type
  • Return a new container with the function applied
  • Preserve the container structure

Laws

Mappable implementations should satisfy these laws:

Identity Law

Mapping the identity function returns the original container.

container.map(lambda x: x) == container

Composition Law

Mapping two functions is the same as mapping their composition.

container.map(f).map(g) == container.map(lambda x: g(f(x)))

Protocol Variants

Mappable1

Mappable1 is a simpler variant of the Mappable protocol that only requires the map method without enforcing return type constraints as strictly. This is useful for:

  • Legacy code that doesn't use strict typing
  • Situations where you need more flexibility
  • Duck-typing scenarios
from better_py.protocols import Mappable1

class SimpleContainer(Mappable1):
    def __init__(self, value):
        self.value = value

    def map(self, f):  # No type annotations required
        return SimpleContainer(f(self.value))

Use Mappable1 when you need a more permissive protocol or when working with untyped code.

Built-in Implementations

Maybe

from better_py import Some, Nothing

Some(5).map(lambda x: x * 2)      # Some(10)
Nothing.map(lambda x: x * 2)      # Nothing

Result

from better_py import Ok, Error

Ok(5).map(lambda x: x * 2)        # Ok(10)
Error("bad").map(lambda x: x * 2)  # Error("bad")

PersistentList

from better_py import PersistentList

PersistentList.of(1, 2, 3).map(lambda x: x * 2)
# PersistentList(2, 4, 6)

PersistentMap

from better_py import PersistentMap

PersistentMap.of({"a": 1}).map(lambda k, v: v * 2)
# PersistentMap({'a': 2})

Real-World Pattern: Chaining Transformations

from better_py import Some, Maybe

def get_user_id() -> Maybe[int]:
    return Some(42)

def fetch_user(id: int) -> dict:
    return {"id": id, "name": "Alice"}

# Transform through the Maybe context
user_name = (get_user_id()
    .map(fetch_user)
    .map(lambda user: user["name"]))
# Some("Alice")

This pattern shows Mappable's power: transformations chain through the context, and if any step returns Nothing, the entire chain short-circuits.

When to Use

Use Mappable when:

  • You need to transform values in a context
  • You want to chain transformations
  • You're working with monadic types
  • You want composable data transformations

Don't use Mappable when:

  • You have simple values (apply functions directly)
  • You need side effects during transformation (use monadic bind instead)
  • Your type doesn't represent a context

See Also

  • Reducible - Protocol for reducing values
  • Combinable - Protocol for combining values
  • Maybe - Example Mappable implementation

On this page