use std::cell::Cell;
use crate::ensure;
use crate::errors::QlResult;
use crate::handle::{AsObservable, Handle};
use crate::math::array::Array;
use crate::patterns::observable::{Observable, Observer, ResetThenNotify};
use crate::require;
use crate::shared::{Shared, SharedMut};
use crate::types::{Real, Size};
use super::{Quote, invalidator};
pub struct MultiCompositeQuote<F> {
elements: Vec<Handle<dyn Quote>>,
cache: Shared<Cell<Option<Real>>>,
observable: Shared<Observable>,
f: F,
_listener: SharedMut<ResetThenNotify>,
}
impl<F: Fn(&Array) -> Real> MultiCompositeQuote<F> {
pub fn new(elements: Vec<Handle<dyn Quote>>, f: F) -> Self {
let (cache, observable, listener) = invalidator();
let observer = listener.clone() as SharedMut<dyn Observer>;
for element in &elements {
element.register_observer(&observer);
}
MultiCompositeQuote {
elements,
cache,
observable,
f,
_listener: listener,
}
}
pub fn input_value(&self, i: Size) -> QlResult<Real> {
require!(
i < self.elements.len(),
"index {i} out of range for {} quotes",
self.elements.len()
);
self.elements[i].current_link()?.value()
}
}
impl<F> AsObservable for MultiCompositeQuote<F> {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl<F: Fn(&Array) -> Real> Quote for MultiCompositeQuote<F> {
fn value(&self) -> QlResult<Real> {
if let Some(cached) = self.cache.get() {
return Ok(cached);
}
ensure!(self.is_valid(), "invalid MultiCompositeQuote");
let args = self
.elements
.iter()
.map(|element| element.current_link()?.value())
.collect::<QlResult<Array>>()?;
let value = (self.f)(&args);
self.cache.set(Some(value));
Ok(value)
}
fn is_valid(&self) -> bool {
self.elements
.iter()
.all(|element| element.current_link().is_ok_and(|q| q.is_valid()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::quotes::SimpleQuote;
use crate::shared::{shared, shared_mut};
use crate::test_support::{Flag, as_observer};
#[test]
fn multi_composite_quote_tracks_function_of_sources() {
let funcs: [fn(&Array) -> Real; 3] =
[|a| a.iter().sum(), |a| a.iter().product(), |a| a.norm2()];
for f in funcs {
let mut mes: Vec<Shared<SimpleQuote>> = Vec::new();
let mut handles: Vec<Handle<dyn Quote>> = Vec::new();
for i in 0..3usize {
mes.push(shared(SimpleQuote::new((i + 1) as Real)));
handles.push(Handle::new(mes[i].clone()));
let composite = MultiCompositeQuote::new(handles.clone(), f);
for j in 0..=i {
mes[j].set_value((j * 10 + 1) as Real);
let args: Array = mes.iter().map(|me| me.value().unwrap()).collect();
let x = composite.value().unwrap();
let y = f(&args);
assert!(
(x - y).abs() <= 1.0e-10,
"composite quote yields {x}, function result is {y}"
);
for (k, me) in mes.iter().enumerate() {
assert_eq!(composite.input_value(k).unwrap(), me.value().unwrap());
}
}
}
}
}
#[test]
fn any_source_change_notifies_observers_and_recomputes() {
let me1 = shared(SimpleQuote::new(1.0));
let me2 = shared(SimpleQuote::new(2.0));
let me3 = shared(SimpleQuote::new(3.0));
let handles: Vec<Handle<dyn Quote>> =
vec![Handle::new(me1), Handle::new(me2), Handle::new(me3.clone())];
let composite = MultiCompositeQuote::new(handles, |a| a.iter().sum());
assert_eq!(composite.value().unwrap(), 6.0);
let flag = Flag::new();
composite
.observable()
.register_observer(&as_observer(&flag));
me3.set_value(30.0);
assert!(Flag::is_up(&flag), "any source change must notify");
assert_eq!(composite.value().unwrap(), 33.0);
}
type SumMultiQuote = MultiCompositeQuote<fn(&Array) -> Real>;
struct Recomputer {
composite: Shared<SumMultiQuote>,
seen: Option<Real>,
}
impl Observer for Recomputer {
fn update(&mut self) {
self.seen = self.composite.value().ok();
}
}
struct Rebalancer {
other: Shared<SimpleQuote>,
target: Real,
}
impl Observer for Rebalancer {
fn update(&mut self) {
self.other.set_value(self.target);
}
}
#[test]
fn cross_source_write_back_does_not_leave_the_cache_stale() {
let me1 = shared(SimpleQuote::new(1.0));
let me2 = shared(SimpleQuote::new(2.0));
let handles: Vec<Handle<dyn Quote>> =
vec![Handle::new(me1.clone()), Handle::new(me2.clone())];
let composite = shared(MultiCompositeQuote::new(
handles,
(|a: &Array| a.iter().sum()) as fn(&Array) -> Real,
));
let recomputer = shared_mut(Recomputer {
composite: composite.clone(),
seen: None,
});
composite
.observable()
.register_observer(&(recomputer.clone() as SharedMut<dyn Observer>));
let rebalancer = shared_mut(Rebalancer {
other: me2.clone(),
target: 20.0,
});
composite
.observable()
.register_observer(&(rebalancer.clone() as SharedMut<dyn Observer>));
me1.set_value(10.0);
assert_eq!(
composite.value().unwrap(),
30.0,
"cache must reflect the written-back source, not the stale one"
);
assert_eq!(
recomputer.borrow().seen,
Some(30.0),
"composite observers must converge on the final value"
);
}
#[test]
fn invalid_source_and_bad_index_error() {
let me1 = shared(SimpleQuote::new(1.0));
let handles: Vec<Handle<dyn Quote>> = vec![Handle::new(me1), Handle::empty()];
let composite = MultiCompositeQuote::new(handles, |a| a.iter().sum());
assert!(!composite.is_valid());
assert_eq!(
composite.value().unwrap_err().message(),
"invalid MultiCompositeQuote"
);
assert!(composite.input_value(2).is_err());
}
}