A lens library for python that enables immutable manipulation of deeply nested data structures
—
Comprehensive collection of lens constructors for accessing different data structures and implementing various access patterns. These methods are available on both UnboundLens and BoundLens classes, providing building blocks for complex data manipulation.
Methods for accessing elements within containers like lists, dictionaries, and objects.
def GetItem(self, key: Any) -> BaseUiLens:
"""Focus an item inside a container. Analogous to operator.itemgetter."""
def GetAttr(self, name: str) -> BaseUiLens:
"""Focus an attribute of an object. Analogous to getattr."""
def Get(self, key: Any, default: Optional[Y] = None) -> BaseUiLens:
"""Focus an item with default value for missing keys. Analogous to dict.get."""
def Contains(self, item: A) -> BaseUiLens[S, S, bool, bool]:
"""Focus a boolean indicating whether state contains item."""from lenses import lens
# GetItem (also available as [key] syntax)
data = [1, 2, 3]
lens.GetItem(0).get()(data) # 1
lens[0].get()(data) # Same as above
# Dictionary access
person = {"name": "Alice", "age": 30}
lens["name"].get()(person) # "Alice"
# GetAttr for object attributes
from collections import namedtuple
Person = namedtuple('Person', 'name age')
p = Person("Bob", 25)
lens.GetAttr('name').get()(p) # "Bob"
# Get with default values
data = {"a": 1}
lens.Get("b", 0).get()(data) # 0 (default)
lens.Get("b").set(2)(data) # {"a": 1, "b": 2}
# Contains
data = [1, 2, 3]
lens.Contains(2).get()(data) # True
lens.Contains(2).set(False)(data) # [1, 3] (removes item)Methods for working with multiple elements in collections.
def Each(self) -> BaseUiLens:
"""Traverse all items in an iterable. Analogous to iter."""
def Items(self) -> BaseUiLens:
"""Traverse key-value pairs in a dictionary. Analogous to dict.items."""
def Keys(self) -> BaseUiLens:
"""Traverse dictionary keys. Analogous to dict.keys."""
def Values(self) -> BaseUiLens:
"""Traverse dictionary values. Analogous to dict.values."""
def Iter(self) -> BaseUiLens:
"""Read-only fold over any iterable (cannot set values)."""from lenses import lens
# Each - traverse all items
data = [1, 2, 3, 4, 5]
lens.Each().collect()(data) # [1, 2, 3, 4, 5]
lens.Each().modify(lambda x: x * 2)(data) # [2, 4, 6, 8, 10]
# Dictionary traversals
data = {"a": 1, "b": 2, "c": 3}
lens.Items().collect()(data) # [("a", 1), ("b", 2), ("c", 3)]
lens.Keys().collect()(data) # ["a", "b", "c"]
lens.Values().collect()(data) # [1, 2, 3]
lens.Values().modify(lambda x: x + 10)(data) # {"a": 11, "b": 12, "c": 13}
# Each works with dictionaries by items
lens.Each().collect()(data) # [("a", 1), ("b", 2), ("c", 3)]
lens.Each()[1].modify(lambda x: x + 1)(data) # {"a": 2, "b": 3, "c": 4}Methods for conditionally selecting elements based on predicates or types.
def Filter(self, predicate: Callable[[A], bool]) -> BaseUiLens:
"""Focus values that satisfy the predicate."""
def Instance(self, type_: Type) -> BaseUiLens:
"""Focus values that are instances of the given type."""
def Just(self) -> BaseUiLens:
"""Focus the value inside a Just object (from Maybe type)."""from lenses import lens
# Filter by predicate
data = [1, -2, 3, -4, 5]
positive = lens.Each().Filter(lambda x: x > 0)
positive.collect()(data) # [1, 3, 5]
positive.modify(lambda x: x * 10)(data) # [10, -2, 30, -4, 50]
# Instance type filtering
mixed_data = [1, "hello", 2.5, "world", 42]
strings = lens.Each().Instance(str)
strings.collect()(mixed_data) # ["hello", "world"]
# Just for Maybe types
from lenses.maybe import Just, Nothing
data = [Just(1), Nothing(), Just(3)]
values = lens.Each().Just()
values.collect()(data) # [1, 3]Methods for converting between different data representations.
def Json(self) -> BaseUiLens:
"""Focus string as parsed JSON data. Analogous to json.loads."""
def Decode(self, encoding: str = "utf-8", errors: str = "strict") -> BaseUiLens:
"""Focus bytes as decoded string. Analogous to bytes.decode."""
def Iso(self, forwards: Callable[[A], X], backwards: Callable[[Y], B]) -> BaseUiLens:
"""Create an isomorphism from forward and backward functions."""
def Norm(self, setter: Callable[[A], X]) -> BaseUiLens:
"""Apply normalization function when setting values."""from lenses import lens
# JSON parsing
json_data = '{"users": [{"name": "Alice"}, {"name": "Bob"}]}'
users = lens.Json()["users"].Each()["name"].collect()(json_data) # ["Alice", "Bob"]
# Update JSON
updated = lens.Json()["users"][0]["name"].set("Charlie")(json_data)
# '{"users": [{"name": "Charlie"}, {"name": "Bob"}]}'
# Decode bytes
byte_data = b"hello world"
text = lens.Decode().get()(byte_data) # "hello world"
# Custom isomorphisms
chr_ord_iso = lens.Iso(chr, ord) # char <-> int conversion
lens.Iso(chr, ord).get()(65) # 'A'
lens.Iso(chr, ord).set('B')(65) # 66
# Normalization
normalize_int = lens[0].Norm(int)
normalize_int.set(4.7)([1, 2, 3]) # [4, 2, 3] (4.7 -> 4)Specialized lens constructors for complex access patterns.
def Item(self, key: Any) -> BaseUiLens:
"""Focus a dictionary item as (key, value) pair."""
def ItemByValue(self, value: Any) -> BaseUiLens:
"""Focus dictionary item by its value."""
def Recur(self, cls) -> BaseUiLens:
"""Recursively focus all objects of given type."""
def Regex(self, pattern: Union[str, Pattern], flags: int = 0) -> BaseUiLens:
"""Focus parts of string matching regex pattern."""
def Parts(self) -> BaseUiLens:
"""Convert fold/traversal into lens by focusing list of all parts."""from lenses import lens
# Item as key-value pairs
data = {"a": 1, "b": 2}
lens.Item("a").get()(data) # ("a", 1)
lens.Item("a").set(("a", 10))(data) # {"a": 10, "b": 2}
lens.Item("a").set(None)(data) # {"b": 2} (removes item)
# ItemByValue
data = {"x": 10, "y": 20, "z": 10}
lens.ItemByValue(10).get()(data) # ("x", 10) (first match)
# Recursive type traversal
data = [1, [2, [3, 4], 5], 6]
lens.Recur(int).collect()(data) # [1, 2, 3, 4, 5, 6]
# Regex patterns
text = "The quick brown fox"
words = lens.Regex(r'\w+')
words.collect()(text) # ["The", "quick", "brown", "fox"]
words.modify(lambda w: w.upper())(text) # "THE QUICK BROWN FOX"
# Parts - convert traversals to lenses
nested = [[1, 2], [3, 4], [5, 6]]
all_nums = lens.Each().Each().Parts()
all_nums.get()(nested) # [1, 2, 3, 4, 5, 6]
all_nums[0].set(99)(nested) # [[99, 2], [3, 4], [5, 6]]Methods for creating custom lenses from functions.
def Lens(self, getter: Callable[[A], X], setter: Callable[[A, Y], B]) -> BaseUiLens:
"""Create custom lens from getter and setter functions."""
def F(self, getter: Callable[[A], X]) -> BaseUiLens:
"""Create getter-only lens from function."""
def Prism(self, unpack: Callable[[A], Just[X]], pack: Callable[[Y], B],
ignore_none: bool = False, ignore_errors: Optional[tuple] = None) -> BaseUiLens:
"""Create prism from unpack/pack functions with optional error handling."""
def Traversal(self, folder: Callable[[A], Iterable[X]],
builder: Callable[[A, Iterable[Y]], B]) -> BaseUiLens:
"""Create custom traversal from folder and builder functions."""
def Fold(self, func: Callable[[A], Iterable[X]]) -> BaseUiLens:
"""Create read-only fold from function returning iterable."""from lenses import lens
# Custom lens for list average
def get_avg(lst):
return sum(lst) / len(lst)
def set_avg(old_lst, new_avg):
# Set average by adjusting last element
target_sum = new_avg * len(old_lst)
return old_lst[:-1] + [target_sum - sum(old_lst[:-1])]
avg_lens = lens.Lens(get_avg, set_avg)
avg_lens.get()([1, 2, 3]) # 2.0
avg_lens.set(5)([1, 2, 3]) # [1, 2, 12] (avg is now 5)
# Getter-only lens
abs_lens = lens.Each().F(abs)
abs_lens.collect()([-1, 2, -3]) # [1, 2, 3]
# Custom traversal for list endpoints
def ends_folder(lst):
yield lst[0]
yield lst[-1]
def ends_builder(old_lst, new_values):
new_vals = list(new_values)
result = list(old_lst)
result[0] = new_vals[0]
result[-1] = new_vals[1]
return result
ends_lens = lens.Traversal(ends_folder, ends_builder)
ends_lens.collect()([1, 2, 3, 4]) # [1, 4]
ends_lens.set(99)([1, 2, 3, 4]) # [99, 2, 3, 99]Methods for combining multiple lenses or operations.
def Fork(self, *lenses: BaseUiLens) -> BaseUiLens:
"""Parallel composition of multiple sub-lenses."""
def Tuple(self, *lenses: BaseUiLens) -> BaseUiLens:
"""Combine focuses of multiple lenses into a tuple."""from lenses import lens
# Fork - parallel operations
data = [1, 2, 3, 4, 5]
fork_lens = lens.Fork(lens[0], lens[2], lens[4])
fork_lens.set(99)(data) # [99, 2, 99, 4, 99]
# Tuple - combine multiple focuses
data = [10, 20, 30, 40]
tuple_lens = lens.Tuple(lens[0], lens[3])
tuple_lens.get()(data) # (10, 40)
tuple_lens.set((1, 9))(data) # [1, 20, 30, 9]
# Useful with Each for cross-product operations
state = ([1, 2], [3, 4])
cross = lens.Tuple(lens[0], lens[1]).Each().Each()
cross.collect()(state) # [1, 2, 3, 4]Utility lenses for debugging and special cases.
def Error(self, exception: Exception, message: Optional[str] = None) -> BaseUiLens:
"""Lens that raises exception when focused (for debugging)."""
def GetZoomAttr(self, name: str) -> BaseUiLens:
"""Focus attribute, zooming if it's a lens itself."""
def Zoom(self) -> BaseUiLens:
"""Follow state as if it were a BoundLens object."""
def ZoomAttr(self, name: str) -> BaseUiLens:
"""Focus attribute and follow it as BoundLens."""from lenses import lens, bind
# Error lens for debugging
debug_lens = lens.Error(ValueError("Debug breakpoint"))
# debug_lens.get()(data) # Raises ValueError
# Zoom lenses work with bound lenses as data
data = [bind([1, 2, 3])[1], 4, 5]
lens[0].Zoom().get()(data) # 2 (follows the bound lens)
lens[0].Zoom().set(99)(data) # [[1, 99, 3], 4, 5]Install with Tessl CLI
npx tessl i tessl/pypi-lenses