Skip to content

Usage Guide

Signified is a reactive programming library that helps you create and manage values that automatically update when their dependencies change. This guide will walk you through common usage patterns and features.

Basic Concepts

Signals

A Signal is a container for a mutable reactive value. When you change a signal's value, any computations that depend on it will automatically update.

from signified import Signal

name = Signal("Alice")
greeting = "Hello, " + name

print(greeting)  # "Hello, Alice"
name.value = "Bob"
print(greeting)  # "Hello, Bob"

Computed Values

Computed values are derived from other reactive values. They can be constructed implicitly using overloaded Python operators or explicitly using the computed decorator.

from signified import Signal

a = Signal(3)
b = Signal(4)

# c is a Computed object that will automatically update when a or b are updated
c = (a ** 2 + b ** 2) ** 0.5

print(c)  # 5

a.value = 5
b.value = 12

print(c)  # 13
from signified import Signal, computed

numbers = Signal([1, 2, 3, 4, 5])

@computed
def stats(nums):
    return {
        'sum': sum(nums),
        'mean': sum(nums) / len(nums),
        'min': min(nums),
        'max': max(nums)
    }

result = stats(numbers)
print(result)  # {'sum': 15, 'mean': 3.0, 'min': 1, 'max': 5}

numbers.value = [2, 4, 6, 8, 10]
print(result)  # {'sum': 30, 'mean': 6.0, 'min': 2, 'max': 10}

Working with Data

Collections (Lists, Dicts, etc.)

Signified handles collections like lists and dictionaries somewhat well, but there are currently some rough edges.

from signified import Signal, computed

# Working with lists
numbers = Signal([1, 2, 3, 4, 5])
doubled = computed(lambda x: [n * 2 for n in x])(numbers)

print(doubled)  # [2, 4, 6, 8, 10]
numbers.value = [5, 6, 7]
print(doubled)  # [10, 12, 14]

# Modifying lists
numbers[0] = 10  # Notifies observers
print(numbers)  # [10, 6, 7]
from signified import Signal, computed

# Working with dictionaries
config = Signal({"theme": "dark", "fontSize": 14})
theme = config["theme"]
font_size = config["fontSize"]

print(theme)      # "dark"
print(font_size)  # 14

config.value = {"theme": "light", "fontSize": 16}
print(theme)      # "light"
print(font_size)  # 16

NumPy

Signified integrates well with NumPy arrays:

from signified import Signal
import numpy as np

matrix = Signal(np.array([[1, 2], [3, 4]]))
vector = Signal(np.array([1, 1]))

result = matrix @ vector  # Matrix multiplication

print(result)  # array([3, 7])

matrix.value = np.array([[2, 2], [4, 4]])
print(result)  # array([4, 8])

Other Topics

Conditional Logic

Use the where() method for conditional computations:

from signified import Signal

username = Signal(None)
is_logged_in = username.is_not(None)

message = is_logged_in.where(f"Welcome back, {username}!", "Please log in")

print(message)  # "Please log in"
username.value = "admin"
print(message)  # "Welcome back, admin!"

Reactive Attribute Access and Method Calls

Signified supports reactively accessing attributes, properties, or methods on the underlying value.

Here, we construct a simple class and show that we can create Computed objects that reactively track the value of attributes stored within he underlying object.

from dataclasses import dataclass
from signified import Signal

@dataclass
class Person:
    name: str
    age: int

    def greet(self):
        return f"Hello, I'm {self.name} and I'm {self.age} years old!"

person = Signal(Person("Alice", 30))

# Access attributes reactively
name_display = person.name
age_display = person.age
greeting = person.greet()

print(name_display)  # "Alice"
print(age_display)  # 30
print(greeting)     # "Hello, I'm Alice and I'm 30 years old!"

# Update through the signal
person.name = "Bob"
person.age = 35

print(name_display)  # "Bob"
print(age_display)  # 35
print(greeting)     # "Hello, I'm Bob and I'm 35 years old!"

Therefore, if the underlying object supports method chaining, we can easily create reactive values that apply several methods in sequence.

from signified import Signal

text = Signal("  Hello, World!  ")
processed = text.strip().lower().replace(",", "")

print(processed.value)  # "hello world!"
text.value = "  Goodbye, World!  "
print(processed.value)  # "goodbye world!"

The reactive_method decorator

Use the @reactive_method decorator to turn a non-reactive method into a reactive one.

from dataclasses import dataclass
from typing import List

from signified import Signal, reactive_method

@dataclass
class Item:
    name: str
    price: float

class Cart:
    def __init__(self, items: List[Item]):
        self.items = Signal(items)
        self.tax_rate = Signal(0.125)

    # Providing the names of the reactive values this method depends on tells
    # signified to monitor them for updates
    @reactive_method('items', 'tax_rate')
    def total(self):
        subtotal = sum(item.price for item in self.items.value)
        return subtotal * (1 + self.tax_rate)

items = [Item(name="Book", price=20), Item(name="Pen", price=4)]
cart = Cart(items)

total_price = cart.total()
print(total_price)  # 27 (24 * 1.125)
cart.tax_rate.value = 0.25
print(total_price)  # 30 (24 * 1.25)
cart.items[0] = Item(name="Rare book?", price=400)
print(total_price)  # 505 (404 * 1.25)

Understanding unref

The unref function is particularly useful when working with values that might be either reactive or non-reactive. This is common when writing functions that should handle both types transparently.

from signified import HasValue, Signal, unref

def process_data(value: HasValue[float]):
    # unref handles both reactive and non-reactive values
    actual_value = unref(value)
    return actual_value * 2

# Works with regular values
regular_value = 4
print(process_data(regular_value))  # 8

# Works with reactive values
reactive_value = Signal(5)
print(process_data(reactive_value))  # 10

# Works with nested reactive values
nested_value = Signal(Signal(Signal(6)))
print(process_data(nested_value))   # 12