πŸ”­ 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:

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:

  1. Value modifications are atomic
  2. Observers are notified consistently
  3. 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

  1. Avoid infinite loops: Don’t modify an Observable within its own observer
  2. Keep observers lightweight: Heavy operations should be asynchronous
  3. Use combinators: Prefer map and filter over manual transformations
  4. 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