///|
/// Subscriber ID
priv struct SubId(Int) derive(Eq, Hash)
///|
/// A reactive signal that can be subscribed to and updated.
struct Rsignal[T] {
id : String // unique ID for this signal
sub_id : () -> Int // for generating unique subscriber IDs
mut value : T // any changes to this value will trigger subscribers
subscribers : @hashmap.HashMap[SubId, Subscriber[T]] // subscribers to this signal
}
///|
struct Subscriber[T] {
id : SubId // unique ID for this subscriber, used to subscribe/unsubscribe
effect : (T) -> Unit // function to call when the signal changes
}
///|
fn[T] Subscriber::effect(self : Subscriber[T], value : T) -> Unit {
(self.effect)(value)
}
///|
fn id_generator() -> () -> Int {
let mut id : Int = -1
fn() -> Int {
id += 1
id
}
}
///|
/// Creates a new unique ID for signals
let signal_id : () -> Int = id_generator()
///|
/// Creates a new signal with the given initial value.
pub fn[T] new(value : T, label? : String = "signal") -> Rsignal[T] {
let sub_id = id_generator()
let id = "\{label}#\{signal_id()}"
Rsignal::{ id, value, sub_id, subscribers: @hashmap.new() }
}
///|
/// Returns the unique ID of the signal
///
/// Parameters:
/// - `self`: The signal to get the ID from.
///
/// Returns:
/// - The unique ID of the signal.
///
/// Example:
/// ```moonbit
/// let s = new(0)
/// assert_eq(s.id(), "signal#0")
/// let s2 = new(1, label="my_signal")
/// assert_eq(s2.id(), "my_signal#1")
/// ```
pub fn[T] Rsignal::id(self : Rsignal[T]) -> String {
self.id
}
///|
/// Returns the current value of the signal `s`
///
/// Parameters:
/// - `s`: The signal to get the value from.
///
/// Returns:
/// - The current value of the signal.
///
/// Example:
/// ```moonbit
/// let s = new(0)
/// assert_eq(s.val(), 0)
/// s.update(5)
/// assert_eq(s.val(), 5)
/// ```
pub fn[T] Rsignal::val(s : Rsignal[T]) -> T {
s.value
}
// ///|
// /// Sets the value of the signal `s` to `value` without notifying subscribers
// /// This is useful for cases where you want to set the value without triggering any side effects.
// ///
// /// Parameters:
// /// - `s`: The signal to set.
// /// - `value`: The new value to set.
// ///
// /// Example:
// /// ```moonbit
// /// let values = []
// /// let s = new(0)
// /// s.subscribe_permanent(fn(v) { values.push(v) })
// /// assert_eq(s.val(), 0)
// /// s.set(5) // This will set the signal to 5 without notifying subscribers.
// /// assert_eq(s.val(), 5)
// /// s.update(10) // This will notify subscribers with the new value.
// /// assert_eq(s.val(), 10)
// /// assert_eq(values, [0, 10])
// /// ```
// pub fn[T] Signal::set(s : Signal[T], value : T) -> Unit {
// s.value = value
// }
///|
/// Returns the unique subscriber ID for this signal
fn[T] Rsignal::sub_id(self : Rsignal[T]) -> SubId {
(self.sub_id)()
}
///|
/// Subscribes to a signal and immediately calls the effect function with the current value
pub fn[T] Rsignal::subscribe(
self : Rsignal[T],
effect : (T) -> Unit,
) -> Subscriber[T] {
let subscriber = Subscriber::{ id: self.sub_id(), effect }
self.subscribers[subscriber.id] = subscriber
subscriber.effect(self.value)
subscriber
}
///|
/// Subscribes to a signal without immediately calling the effect function
/// This is useful for cases where you want to set up a subscription but don't want to
/// trigger the effect function until the signal changes.
pub fn[T] Rsignal::subscribe_only(
self : Rsignal[T],
effect : (T) -> Unit,
) -> Subscriber[T] {
let subscriber = Subscriber::{ id: self.sub_id(), effect }
self.subscribers[subscriber.id] = subscriber
subscriber
}
///|
/// Subscribes to a signal permanently and immediately calls the effect function with the current value.
/// This is useful for cases where you want to set up a subscription that will not be removed
pub fn[T] Rsignal::subscribe_permanent(
self : Rsignal[T],
effect : (T) -> Unit,
) -> Unit {
self.subscribe(effect) |> ignore
}
///|
/// Subscribes to a signal permanently without immediately calling the effect function.
pub fn[T] Rsignal::subscribe_permanent_only(
self : Rsignal[T],
effect : (T) -> Unit,
) -> Unit {
self.subscribe_only(effect) |> ignore
}
///|
/// Updates the value of the signal. If `notify` is `true`, notifies all of its observers
/// of the change.
///
/// Parameters:
/// - `self`: The signal to update.
/// - `new_value`: The new value to set.
/// - `notify` : If `true` notifies all observers of this signal - default is `true`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// s.subscribe_permanent(fn(v) { values.push(v) })
/// assert_eq(s.val(), 0)
/// s.update(5) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 5)
/// assert_eq(values, [0, 5])
/// s.update(10) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 10)
/// assert_eq(values, [0, 5, 10])
/// ```
pub fn[T] Rsignal::update(
self : Rsignal[T],
new_value : T,
notify? : Bool = true,
) -> Unit {
self.value = new_value
if notify {
for _, subscriber in self.subscribers {
subscriber.effect(self.value)
}
}
}
///|
/// Unsubscribes a subscriber from a signal.
///
/// Parameters:
/// - `self`: The signal to unsubscribe from.
/// - `subscription`: The subscriber to unsubscribe.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// let sub = s.subscribe(fn(v) { values.push(v) })
/// assert_eq(s.val(), 0)
/// s.update(5) // This will notify subscribers with the new value.
/// assert_eq(s.val(), 5)
/// assert_eq(values, [0, 5])
/// s.unsubscribe(sub) // This will unsubscribe the subscriber.
/// assert_eq(s.val(), 5)
/// s.update(10) // This will not notify the unsubscribed subscriber.
/// assert_eq(s.val(), 10)
/// assert_eq(values, [0, 5])
/// ```
pub fn[T] Rsignal::unsubscribe(
self : Rsignal[T],
subscription : Subscriber[T],
) -> Unit {
self.subscribers.remove(subscription.id)
}
///|
/// Creates a new signal that maps the value of `self` using the function `f`.
///
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The mapping function that takes the value of `self` and returns a new value.
///
/// Returns:
/// - A new signal that emits the result of applying `f` to the value of `self`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s = new(0)
/// let s2 = s.map(fn(v) { v + 1 })
/// s2.subscribe_permanent(fn(v) { values.push(v) })
/// s.update(5) // This will trigger the subscriber with value 6.
/// s.update(10) // This will trigger the subscriber with value 11.
/// s.update(20) // This will trigger the subscriber with value 21.
/// inspect(values, content="[1, 6, 11, 21]")
/// ```
pub fn[T, U] Rsignal::map(self : Rsignal[T], f : (T) -> U) -> Rsignal[U] {
let s = new(f(self.value))
self.subscribe_permanent(v => s.update(f(v)))
s
}
///|
/// Creates a new signal that maps the values of two signals using the function `f`.
///
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
/// - `f`: The mapping function that takes the values of both signals and returns a new value.
///
/// Returns:
/// - A new signal that emits the result of applying `f` to the values of `s1` and `s2`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = map2(s1, s2, fn(a, b) { a + b })
/// s3.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(3) // This will trigger the subscriber with value 5.
/// s2.update(4) // This will trigger the subscriber with value 7.
/// s1.update(5) // This will trigger the subscriber with value 9.
/// inspect(values, content="[3, 5, 7, 9]")
/// ```
pub fn[T, U, V] map2(
s1 : Rsignal[T],
s2 : Rsignal[U],
f : (T, U) -> V,
) -> Rsignal[V] {
let s = new(f(s1.value, s2.value))
s1.subscribe_permanent(v => s.update(f(v, s2.value)))
s2.subscribe_permanent(v => s.update(f(s1.value, v)))
s
}
///|
/// Creates a new signal that receives values from `self` where `f(new_value)` is `true`.
/// If the initial value of `f(self.get()) == false`, the new signal will be
/// initialized with the given `seed` value.
///
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The filter function.
/// - `seed`: The initial value for the new signal if the filter function returns false during creation.
///
/// Returns:
/// - A new signal that emits only the values from `self` that pass the filter function `f`.
///
/// Example:
/// ```moonbit
/// let filtered_values = []
/// let s = new(1)
/// let s2 = s.filter(fn(v) { v > 5 }, 0)
/// s2.subscribe_permanent_only(fn(v) { filtered_values.push(v) })
/// s.update(10) // This will trigger the subscriber with value 10.
/// s.update(7) // This will trigger the subscriber with value 7.
/// s.update(3) // This will not trigger the subscriber.
/// inspect(filtered_values, content="[10, 7]")
/// ```
pub fn[T] Rsignal::filter(
self : Rsignal[T],
f : (T) -> Bool,
seed : T,
) -> Rsignal[T] {
let v = if f(self.value) { self.value } else { seed }
let s = new(v)
self.subscribe_permanent(v => if f(v) { s.update(v) })
s
}
///|
/// Creates a new signal that maps the values of `self` iff `f(self.get()) == Some(v)`.
/// If `f(self.get())` returns `None`, the new signal will be initialized with the given `seed` value
///
/// This combines the functionality of `map` and `filter` into a single operation.
///
/// Parameters:
/// - `self`: The original signal.
/// - `f`: The mapping function.
/// - `seed`: The initial value for the new signal if the mapping function returns `None` during creation.
///
/// Returns:
/// - A new signal that emits the values from `self` that pass the mapping function `f`.
///
/// Example:
/// ```moonbit
/// let mapped_values = []
/// let s = new(1)
/// let s2 = s.filter_map(fn(v) { if v > 5 { Some(v+2) } else { None } }, 0)
/// s2.subscribe_permanent_only(fn(v) { mapped_values.push(v) })
/// s.update(10) // This will trigger the subscriber with value 12.
/// s.update(7) // This will trigger the subscriber with value 9.
/// s.update(3) // This will not trigger the subscriber.
/// inspect(mapped_values, content="[12, 9]")
/// ```
pub fn[T, U] Rsignal::filter_map(
self : Rsignal[T],
f : (T) -> U?,
seed : U,
) -> Rsignal[U] {
let v = f(self.value).unwrap_or_else(() => seed)
let s = new(v)
self.subscribe_permanent(v => match f(v) {
Some(v) => s.update(v)
None => ()
})
s
}
// -- Combinators ---
///|
/// Creates a new signal which re-emits the latest signal from one of the
/// signals in `signals` array.
///
/// This is useful for combining multiple signals into a single signal.
///
/// The initial value of the new signal will be the default value of `T`.
///
/// Parameters:
/// - `signals`: An array of signals to combine.
///
/// Returns:
/// - A new signal that emits the values from all signals in the array.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let combined_signal = select_one([s1, s2, s3])
/// combined_signal.subscribe_permanent_only(fn(v) { values.push(v) })
/// s1.update(4) // This will trigger the subscriber with value 4.
/// s2.update(5) // This will trigger the subscriber with value 5.
/// s3.update(6) // This will trigger the subscriber with value 6.
/// inspect(values, content="[4, 5, 6]")
/// ```
pub fn[T : Default] select_one(signals : Array[Rsignal[T]]) -> Rsignal[T] {
let s = new(T::default())
for signal in signals {
signal.subscribe_permanent_only(v => s.update(v))
}
s
}
///|
/// Creates a new signal that emits a tuple of the latest values from both signals.
///
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
///
/// Returns:
/// - A new signal that emits a tuple of the latest values from `s1` and `s2`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = combine_pair(s1, s2)
/// s3.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(3) // This will trigger the subscriber with value (3, 2).
/// s2.update(4) // This will trigger the subscriber with value (3, 4).
/// s1.update(5) // This will trigger the subscriber with value (5, 4).
/// inspect(values, content="[(1, 2), (3, 2), (3, 4), (5, 4)]")
/// ```
pub fn[T, U] combine_pair(s1 : Rsignal[T], s2 : Rsignal[U]) -> Rsignal[(T, U)] {
let s = new((s1.value, s2.value))
s1.subscribe_permanent_only(v => s.update((v, s2.value)))
s2.subscribe_permanent_only(v => s.update((s1.value, v)))
s
}
///|
/// Creates a new signal that emits a tuple of the latest values from three signals.
///
/// Parameters:
/// - `s1`: The first signal.
/// - `s2`: The second signal.
/// - `s3`: The third signal.
///
/// Returns:
/// - A new signal that emits a tuple of the latest values from `s1`, `s2`, and `s3`.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let s4 = combine_triple(s1, s2, s3)
/// s4.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(4) // This will trigger the subscriber with value (4, 2, 3).
/// s2.update(5) // This will trigger the subscriber with value (4, 5, 3).
/// s3.update(6) // This will trigger the subscriber with value (4, 5, 6).
/// s1.update(7) // This will trigger the subscriber with value (7, 5, 6).
/// inspect(values, content="[(1, 2, 3), (4, 2, 3), (4, 5, 3), (4, 5, 6), (7, 5, 6)]")
/// ```
pub fn[T, U, V] combine_triple(
s1 : Rsignal[T],
s2 : Rsignal[U],
s3 : Rsignal[V],
) -> Rsignal[(T, U, V)] {
let s = new((s1.value, s2.value, s3.value))
s1.subscribe_permanent_only(v => s.update((v, s2.value, s3.value)))
s2.subscribe_permanent_only(v => s.update((s1.value, v, s3.value)))
s3.subscribe_permanent_only(v => s.update((s1.value, s2.value, v)))
s
}
///|
/// Combines all signals in `signals` into a single signal `s`. If any of the signals
/// change, `s` will emit an array of the latest values from all signals.
///
/// Parameters:
/// - `signals`: An array of signals to combine.
///
/// Returns:
/// - A new signal that emits an array of the latest values from all signals.
///
/// Example:
/// ```moonbit
/// let values = []
/// let s1 = new(1)
/// let s2 = new(2)
/// let s3 = new(3)
/// let s4 = new(4)
/// let combined_signal = combine_all([s1, s2, s3, s4])
/// combined_signal.subscribe_permanent(fn(v) { values.push(v) })
/// s1.update(5) // This will trigger the subscriber with value [5, 2, 3, 4].
/// s2.update(6) // This will trigger the subscriber with value [5, 6, 3, 4].
/// s3.update(7) // This will trigger the subscriber with value [5, 6, 7, 4].
/// s4.update(8) // This will trigger the subscriber with value [5, 6, 7, 8].
/// inspect(values, content="[[1, 2, 3, 4], [5, 2, 3, 4], [5, 6, 3, 4], [5, 6, 7, 4], [5, 6, 7, 8]]")
/// ```
pub fn[T] combine_all(signals : Array[Rsignal[T]]) -> Rsignal[Array[T]] {
let latest_values = []
for signal in signals {
latest_values.push(signal.value)
}
let s = new(latest_values)
for signal in signals {
signal.subscribe_permanent_only(new_val => {
let vals = []
for s in signals {
if s.id == signal.id {
vals.push(new_val)
} else {
vals.push(s.value)
}
}
s.update(vals)
})
}
s
}