π Observables in GoloScript
Observables allow you to create values that automatically notify observers when they change. This is a reactive programming pattern particularly useful for managing state changes in a declarative and thread-safe manner.
Introduction
An Observable is a value that can be watched for changes. When the value changes, all registered observers are automatically notified in a thread-safe manner.
Typical use cases:
- Reactive state management in applications
- Automatic change propagation
- Coordination between multiple components
- Data transformation pipelines
Creating an Observable
To create an Observable, use the observable() function with an initial value:
# Create an observable with an initial value
let counter = observable(0)
let message = observable("Hello")
let data = observable([1, 2, 3])
Modifying the value
To read or modify an Observableβs value:
# Read the current value
let value = observableGet(counter)
# Modify the value (notifies all observers)
observableSet(counter, 42)
Adding observers
Observers are functions that will be called automatically on each value change:
# Create an observable
let temperature = observable(20)
# Add an observer
observableOnChange(temperature, |temp| {
println("Temperature updated: " + temp + "Β°C")
})
# Changing the value triggers the observer
observableSet(temperature, 25)
# Prints: Temperature updated: 25Β°C
Multiple observers
You can register multiple observers on the same Observable:
let price = observable(100)
# Observer 1: Display
observableOnChange(price, |p| {
println("Price: " + p + "β¬")
})
# Observer 2: Alert
observableOnChange(price, |p| {
if p > 150 {
println("β οΈ High price!")
}
})
observableSet(price, 200)
# Prints:
# Price: 200β¬
# β οΈ High price!
Method chaining
The observableOnChange() function returns the Observable, allowing chaining:
let obs = observable(42)
observableOnChange(
observableOnChange(obs, |v| { println("Observer 1: " + v) }),
|v| { println("Observer 2: " + v) }
)
Functional combinators
Observables support functional operations to create transformation pipelines.
Map - Transform values
observableMap() creates a new Observable that applies a transformation to values:
let source = observable(5)
# Create an observable that doubles the value
let doubled = observableMap(source, |value| {
return value * 2
})
observableOnChange(doubled, |v| {
println("Doubled value: " + v)
})
observableSet(source, 10)
# Prints: Doubled value: 20
Practical example - Formatting:
let price = observable(99.99)
let formatted = observableMap(price, |p| {
return p + "β¬"
})
observableOnChange(formatted, |f| {
println("Formatted price: " + f)
})
Filter - Filter values
observableFilter() creates a new Observable that only propagates values matching a predicate:
let numbers = observable(1)
# Only propagate even numbers
let evens = observableFilter(numbers, |n| {
return n % 2 == 0
})
observableOnChange(evens, |n| {
println("Even number received: " + n)
})
observableSet(numbers, 3) # No notification
observableSet(numbers, 4) # Prints: Even number received: 4
Practical example - Validation:
let userInput = observable("")
let validInput = observableFilter(userInput, |input| {
return len(input) >= 3
})
observableOnChange(validInput, |input| {
println("β Valid input: " + input)
})
observableSet(userInput, "ab") # No notification
observableSet(userInput, "abc") # β Valid input: abc
Chaining
Combine map and filter to create transformation pipelines:
let source = observable(1)
# Pipeline: multiply by 2, then filter > 5
let doubled = observableMap(source, |v| { return v * 2 })
let filtered = observableFilter(doubled, |v| { return v > 5 })
observableOnChange(filtered, |v| {
println("Result: " + v)
})
observableSet(source, 2) # 2*2=4, no notification (4 <= 5)
observableSet(source, 3) # 3*2=6, prints: Result: 6
observableSet(source, 5) # 5*2=10, prints: Result: 10
Thread-Safety
Observables are thread-safe by default. They use mutexes to ensure that:
- Value modifications are atomic
- Observers are notified consistently
- No conflicts occur during concurrent access
This makes them perfect for coordination between goroutines:
let sharedCounter = observable(0)
observableOnChange(sharedCounter, |count| {
println("Counter: " + count)
})
# Multiple goroutines can safely modify the counter
# Notification is guaranteed to be thread-safe
Complete API
Main functions
| Function | Description | Return |
|---|---|---|
observable(value) |
Creates a new Observable | Observable |
observableGet(obs) |
Reads the current value | Value |
observableSet(obs, value) |
Modifies the value and notifies | New value |
observableOnChange(obs, fn) |
Registers an observer | Observable (for chaining) |
observableMap(obs, fn) |
Creates a transformed Observable | Observable |
observableFilter(obs, predicate) |
Creates a filtered Observable | Observable |
Function signatures
# Creation
observable(initialValue) -> Observable
# Read/Write
observableGet(obs) -> Value
observableSet(obs, newValue) -> Value
# Observers
observableOnChange(obs, observer: |value| -> ...) -> Observable
# Transformation
observableMap(obs, mapper: |value| -> transformedValue) -> Observable
observableFilter(obs, predicate: |value| -> Boolean) -> Observable
Advanced examples
Data transformation pipeline
# Simulate a data stream
let rawData = observable(0)
# Step 1: Normalize (multiply by 10)
let normalized = observableMap(rawData, |v| { return v * 10 })
# Step 2: Validate (only >= 50)
let validated = observableFilter(normalized, |v| { return v >= 50 })
# Step 3: Format for display
let formatted = observableMap(validated, |v| { return "Value: " + v })
# Observe the final result
observableOnChange(formatted, |result| {
println(result)
})
# Test
observableSet(rawData, 3) # 30, no notification
observableSet(rawData, 5) # 50, prints: Value: 50
observableSet(rawData, 7) # 70, prints: Value: 70
Multiple reaction system
let state = observable("idle")
# Different components react to state changes
observableOnChange(state, |s| {
println("π State: " + s)
})
observableOnChange(state, |s| {
if s == "error" {
println("β Error detected!")
}
})
observableOnChange(state, |s| {
if s == "success" {
println("β
Success!")
}
})
observableSet(state, "loading") # π State: loading
observableSet(state, "success") # π State: success, β
Success!
observableSet(state, "error") # π State: error, β Error detected!
Aggregation and derived computation
let quantity = observable(1)
let unitPrice = observable(10)
# Calculate total price
let totalPrice = observableMap(quantity, |q| {
return q * observableGet(unitPrice)
})
observableOnChange(totalPrice, |total| {
println("Total price: " + total + "β¬")
})
observableSet(quantity, 3) # Total price: 30β¬
observableSet(unitPrice, 15) # Doesn't trigger totalPrice directly
observableSet(quantity, 3) # Total price: 45β¬
π Best practices
- Avoid infinite loops: Donβt modify an Observable within its own observer
- Keep observers lightweight: Heavy operations should be asynchronous
- Use combinators: Prefer
mapandfilterover manual transformations - Composition: Create declarative transformation pipelines
Note: This feature is inspired by the original Golo Observable module, adapted for GoloScript with a simplified and thread-safe API.
Β© 2026 GoloScript Project | Built with Gu10berg
Subscribe: π‘ RSS | βοΈ Atom