hydro_lang/live_collections/optional.rs
1//! Definitions for the [`Optional`] live collection.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5use std::ops::Deref;
6use std::rc::Rc;
7
8use stageleft::{IntoQuotedMut, QuotedWithContext, q};
9use syn::parse_quote;
10
11use super::OperatorContext;
12use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
13use super::singleton::Singleton;
14use super::stream::{AtLeastOnce, ExactlyOnce, NoOrder, Stream, TotalOrder};
15use crate::compile::builder::{CycleId, FlowState};
16use crate::compile::ir::{CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, SharedNode};
17#[cfg(stageleft_runtime)]
18use crate::forward_handle::{CycleCollection, CycleCollectionWithInitial, ReceiverComplete};
19use crate::forward_handle::{ForwardRef, TickCycle};
20use crate::live_collections::singleton::SingletonBound;
21#[cfg(feature = "tokio")]
22use crate::location::TopLevel;
23#[cfg(stageleft_runtime)]
24use crate::location::dynamic::{DynLocation, LocationId};
25use crate::location::tick::{Atomic, DeferTick};
26use crate::location::{Location, Tick, check_matching_location};
27use crate::nondet::{NonDet, nondet};
28use crate::prelude::KeyedSingleton;
29use crate::properties::{StreamMapFuncAlgebra, ValidMutCommutativityFor, ValidMutIdempotenceFor};
30
31/// A *nullable* Rust value that can asynchronously change over time.
32///
33/// Optionals are the live collection equivalent of [`Option`]. If the optional is [`Bounded`],
34/// the value is frozen and will not change. But if it is [`Unbounded`], the value will
35/// asynchronously change over time, including becoming present of uninhabited.
36///
37/// Optionals are used in many of the same places as [`Singleton`], but when the value may be
38/// nullable. For example, the first element of a [`Stream`] is exposed as an [`Optional`].
39///
40/// Type Parameters:
41/// - `Type`: the type of the value in this optional (when it is not null)
42/// - `Loc`: the [`Location`] where the optional is materialized
43/// - `Bound`: tracks whether the value is [`Bounded`] (fixed) or [`Unbounded`] (changing asynchronously)
44pub struct Optional<Type, Loc, Bound: Boundedness> {
45 pub(crate) location: Loc,
46 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
47 pub(crate) flow_state: FlowState,
48
49 _phantom: PhantomData<(Type, Loc, Bound)>,
50}
51
52impl<T, L, B: Boundedness> Drop for Optional<T, L, B> {
53 fn drop(&mut self) {
54 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
55 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
56 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
57 input: Box::new(ir_node),
58 op_metadata: HydroIrOpMetadata::new(),
59 });
60 }
61 }
62}
63
64impl<'a, T, L> From<Optional<T, L, Bounded>> for Optional<T, L, Unbounded>
65where
66 T: Clone,
67 L: Location<'a>,
68{
69 fn from(value: Optional<T, L, Bounded>) -> Self {
70 let tick = value.location().tick();
71 value.clone_into_tick(&tick).latest()
72 }
73}
74
75impl<'a, T, L> DeferTick for Optional<T, Tick<L>, Bounded>
76where
77 L: Location<'a>,
78{
79 fn defer_tick(self) -> Self {
80 Optional::defer_tick(self)
81 }
82}
83
84impl<'a, T, L> CycleCollection<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
85where
86 L: Location<'a>,
87{
88 type Location = Tick<L>;
89
90 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
91 Optional::new(
92 location.clone(),
93 HydroNode::CycleSource {
94 cycle_id,
95 metadata: location.new_node_metadata(Self::collection_kind()),
96 },
97 )
98 }
99}
100
101impl<'a, T, L> CycleCollectionWithInitial<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
102where
103 L: Location<'a>,
104{
105 type Location = Tick<L>;
106
107 fn location(&self) -> &Self::Location {
108 self.location()
109 }
110
111 fn create_source_with_initial(cycle_id: CycleId, initial: Self, location: Tick<L>) -> Self {
112 let from_previous_tick: Optional<T, Tick<L>, Bounded> = Optional::new(
113 location.clone(),
114 HydroNode::DeferTick {
115 input: Box::new(HydroNode::CycleSource {
116 cycle_id,
117 metadata: location.new_node_metadata(Self::collection_kind()),
118 }),
119 metadata: location
120 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
121 },
122 );
123
124 from_previous_tick.or(initial.filter_if(location.optional_first_tick(q!(())).is_some()))
125 }
126}
127
128impl<'a, T, L> ReceiverComplete<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
129where
130 L: Location<'a>,
131{
132 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
133 assert_eq!(
134 Location::id(&self.location),
135 expected_location,
136 "locations do not match"
137 );
138 self.location
139 .flow_state()
140 .borrow_mut()
141 .push_root(HydroRoot::CycleSink {
142 cycle_id,
143 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
144 op_metadata: HydroIrOpMetadata::new(),
145 });
146 }
147}
148
149impl<'a, T, L, B: Boundedness> CycleCollection<'a, ForwardRef> for Optional<T, L, B>
150where
151 L: Location<'a>,
152{
153 type Location = L;
154
155 fn create_source(cycle_id: CycleId, location: L) -> Self {
156 Optional::new(
157 location.clone(),
158 HydroNode::CycleSource {
159 cycle_id,
160 metadata: location.new_node_metadata(Self::collection_kind()),
161 },
162 )
163 }
164}
165
166impl<'a, T, L, B: Boundedness> ReceiverComplete<'a, ForwardRef> for Optional<T, L, B>
167where
168 L: Location<'a>,
169{
170 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
171 assert_eq!(
172 Location::id(&self.location),
173 expected_location,
174 "locations do not match"
175 );
176 self.location
177 .flow_state()
178 .borrow_mut()
179 .push_root(HydroRoot::CycleSink {
180 cycle_id,
181 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
182 op_metadata: HydroIrOpMetadata::new(),
183 });
184 }
185}
186
187impl<'a, T, L, B: SingletonBound> From<Singleton<T, L, B>> for Optional<T, L, B::UnderlyingBound>
188where
189 L: Location<'a>,
190{
191 fn from(singleton: Singleton<T, L, B>) -> Self {
192 Optional::new(
193 singleton.location.clone(),
194 HydroNode::Cast {
195 inner: Box::new(singleton.ir_node.replace(HydroNode::Placeholder)),
196 metadata: singleton
197 .location
198 .new_node_metadata(Self::collection_kind()),
199 },
200 )
201 }
202}
203
204#[cfg(stageleft_runtime)]
205pub(super) fn zip_inside_tick<'a, T, O, L: Location<'a>, B: Boundedness>(
206 me: Optional<T, L, B>,
207 other: Optional<O, L, B>,
208) -> Optional<(T, O), L, B> {
209 check_matching_location(&me.location, &other.location);
210
211 Optional::new(
212 me.location.clone(),
213 HydroNode::CrossSingleton {
214 left: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
215 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
216 metadata: me
217 .location
218 .new_node_metadata(Optional::<(T, O), L, B>::collection_kind()),
219 },
220 )
221}
222
223#[cfg(stageleft_runtime)]
224fn or_inside_tick<'a, T, L: Location<'a>, B: Boundedness>(
225 me: Optional<T, L, B>,
226 other: Optional<T, L, B>,
227) -> Optional<T, L, B> {
228 check_matching_location(&me.location, &other.location);
229
230 Optional::new(
231 me.location.clone(),
232 HydroNode::ChainFirst {
233 first: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
234 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
235 metadata: me
236 .location
237 .new_node_metadata(Optional::<T, L, B>::collection_kind()),
238 },
239 )
240}
241
242impl<'a, T, L, B: Boundedness> Clone for Optional<T, L, B>
243where
244 T: Clone,
245 L: Location<'a>,
246{
247 fn clone(&self) -> Self {
248 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
249 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
250 *self.ir_node.borrow_mut() = HydroNode::Tee {
251 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
252 metadata: self.location.new_node_metadata(Self::collection_kind()),
253 };
254 }
255
256 if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
257 Optional {
258 location: self.location.clone(),
259 flow_state: self.flow_state.clone(),
260 ir_node: super::tracked_ir_node(
261 &self.flow_state,
262 HydroNode::Tee {
263 inner: SharedNode(inner.0.clone()),
264 metadata: metadata.clone(),
265 },
266 ),
267 _phantom: PhantomData,
268 }
269 } else {
270 unreachable!()
271 }
272 }
273}
274
275impl<'a, T, L, B: Boundedness> Optional<T, L, B>
276where
277 L: Location<'a>,
278{
279 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
280 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
281 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
282 let flow_state = location.flow_state().clone();
283 let ir_node = super::tracked_ir_node(&flow_state, ir_node);
284 Optional {
285 location,
286 flow_state,
287 ir_node,
288 _phantom: PhantomData,
289 }
290 }
291
292 pub(crate) fn collection_kind() -> CollectionKind {
293 CollectionKind::Optional {
294 bound: B::BOUND_KIND,
295 element_type: stageleft::quote_type::<T>().into(),
296 }
297 }
298
299 /// Returns the [`Location`] where this optional is being materialized.
300 pub fn location(&self) -> &L {
301 &self.location
302 }
303
304 /// Creates a shared reference handle to this optional that can be captured inside `q!()`
305 /// closures. The handle resolves to `&Option<T>` at runtime.
306 ///
307 /// The optional must be bounded, otherwise reading it would be non-deterministic.
308 /// The handle can only be captured in closures passed to operators on collections at
309 /// the same location with **matching boundedness**; capturing it in a closure over an
310 /// unbounded collection is rejected at compile time.
311 pub fn by_ref(&self) -> crate::handoff_ref::OptionalRef<'a, '_, T, L, B>
312 where
313 B: IsBounded,
314 {
315 crate::handoff_ref::OptionalRef::new(&self.ir_node)
316 }
317
318 /// Returns a mutable reference handle to this optional that can be captured inside `q!()`
319 /// closures. The handle resolves to `&mut Option<T>` at runtime.
320 pub fn by_mut(&self) -> crate::handoff_ref::OptionalMut<'a, '_, T, L, B>
321 where
322 B: IsBounded,
323 {
324 crate::handoff_ref::OptionalMut::new(&self.ir_node)
325 }
326
327 /// Weakens the consistency of this live collection to not guarantee any consistency across
328 /// cluster members (if this collection is on a cluster).
329 pub fn weaken_consistency(self) -> Optional<T, L::DropConsistency, B>
330 where
331 L: Location<'a>,
332 {
333 if L::consistency()
334 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
335 {
336 // already no consistency
337 Optional::new(
338 self.location.drop_consistency(),
339 self.ir_node.replace(HydroNode::Placeholder),
340 )
341 } else {
342 Optional::new(
343 self.location.drop_consistency(),
344 HydroNode::Cast {
345 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
346 metadata: self
347 .location
348 .clone()
349 .drop_consistency()
350 .new_node_metadata(Optional::<T, L::DropConsistency, B>::collection_kind()),
351 },
352 )
353 }
354 }
355
356 /// Casts this live collection to have the consistency guarantees specified in the given
357 /// location type parameter. The developer must ensure that the strengthened consistency
358 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
359 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
360 self,
361 _proof: impl crate::properties::ConsistencyProof,
362 ) -> Optional<T, L2, B>
363 where
364 L: Location<'a>,
365 {
366 if L::consistency() == L2::consistency() {
367 Optional::new(
368 self.location.with_consistency_of(),
369 self.ir_node.replace(HydroNode::Placeholder),
370 )
371 } else {
372 Optional::new(
373 self.location.with_consistency_of(),
374 HydroNode::AssertIsConsistent {
375 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
376 trusted: false,
377 metadata: self
378 .location
379 .clone()
380 .with_consistency_of::<L2>()
381 .new_node_metadata(Optional::<T, L2, B>::collection_kind()),
382 },
383 )
384 }
385 }
386
387 /// Transforms the optional value by applying a function `f` to it,
388 /// continuously as the input is updated.
389 ///
390 /// Whenever the optional is empty, the output optional is also empty.
391 ///
392 /// # Example
393 /// ```rust
394 /// # #[cfg(feature = "deploy")] {
395 /// # use hydro_lang::prelude::*;
396 /// # use futures::StreamExt;
397 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
398 /// let tick = process.tick();
399 /// let optional = tick.optional_first_tick(q!(1));
400 /// optional.map(q!(|v| v + 1)).all_ticks()
401 /// # }, |mut stream| async move {
402 /// // 2
403 /// # assert_eq!(stream.next().await.unwrap(), 2);
404 /// # }));
405 /// # }
406 /// ```
407 pub fn map<U, F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>) -> Optional<U, L, B>
408 where
409 F: Fn(T) -> U + 'a,
410 {
411 let f = f
412 .splice_fn1_ctx(&OperatorContext::<L, B>::new(&self.location))
413 .into();
414 Optional::new(
415 self.location.clone(),
416 HydroNode::Map {
417 f,
418 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
419 metadata: self
420 .location
421 .new_node_metadata(Optional::<U, L, B>::collection_kind()),
422 },
423 )
424 }
425
426 /// Transforms the optional value by applying a function `f` to it and then flattening
427 /// the result into a stream, preserving the order of elements.
428 ///
429 /// If the optional is empty, the output stream is also empty. If the optional contains
430 /// a value, `f` is applied to produce an iterator, and all items from that iterator
431 /// are emitted in the output stream in deterministic order.
432 ///
433 /// The implementation of [`Iterator`] for the output type `I` must produce items in a
434 /// **deterministic** order. For example, `I` could be a `Vec`, but not a `HashSet`.
435 /// If the order is not deterministic, use [`Optional::flat_map_unordered`] instead.
436 ///
437 /// # Example
438 /// ```rust
439 /// # #[cfg(feature = "deploy")] {
440 /// # use hydro_lang::prelude::*;
441 /// # use futures::StreamExt;
442 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
443 /// let tick = process.tick();
444 /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
445 /// optional.flat_map_ordered(q!(|v| v)).all_ticks()
446 /// # }, |mut stream| async move {
447 /// // 1, 2, 3
448 /// # for w in vec![1, 2, 3] {
449 /// # assert_eq!(stream.next().await.unwrap(), w);
450 /// # }
451 /// # }));
452 /// # }
453 /// ```
454 pub fn flat_map_ordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
455 self,
456 f: impl IntoQuotedMut<'a, F, OperatorContext<L, Bounded>, StreamMapFuncAlgebra<C, Idemp>>,
457 ) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
458 where
459 B: IsBounded,
460 I: IntoIterator<Item = U>,
461 F: FnMut(T) -> I + 'a,
462 C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
463 Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
464 {
465 self.into_stream().flat_map_ordered(f)
466 }
467
468 /// Like [`Optional::flat_map_ordered`], but allows the implementation of [`Iterator`]
469 /// for the output type `I` to produce items in any order.
470 ///
471 /// If the optional is empty, the output stream is also empty. If the optional contains
472 /// a value, `f` is applied to produce an iterator, and all items from that iterator
473 /// are emitted in the output stream in non-deterministic order.
474 ///
475 /// # Example
476 /// ```rust
477 /// # #[cfg(feature = "deploy")] {
478 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
479 /// # use futures::StreamExt;
480 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
481 /// let tick = process.tick();
482 /// let optional = tick.optional_first_tick(q!(
483 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
484 /// ));
485 /// optional.flat_map_unordered(q!(|v| v)).all_ticks()
486 /// # }, |mut stream| async move {
487 /// // 1, 2, 3, but in no particular order
488 /// # let mut results = Vec::new();
489 /// # for _ in 0..3 {
490 /// # results.push(stream.next().await.unwrap());
491 /// # }
492 /// # results.sort();
493 /// # assert_eq!(results, vec![1, 2, 3]);
494 /// # }));
495 /// # }
496 /// ```
497 pub fn flat_map_unordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
498 self,
499 f: impl IntoQuotedMut<'a, F, OperatorContext<L, Bounded>, StreamMapFuncAlgebra<C, Idemp>>,
500 ) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
501 where
502 B: IsBounded,
503 I: IntoIterator<Item = U>,
504 F: FnMut(T) -> I + 'a,
505 C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
506 Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
507 {
508 self.into_stream().flat_map_unordered(f)
509 }
510
511 /// Flattens the optional value into a stream, preserving the order of elements.
512 ///
513 /// If the optional is empty, the output stream is also empty. If the optional contains
514 /// a value that implements [`IntoIterator`], all items from that iterator are emitted
515 /// in the output stream in deterministic order.
516 ///
517 /// The implementation of [`Iterator`] for the element type `T` must produce items in a
518 /// **deterministic** order. For example, `T` could be a `Vec`, but not a `HashSet`.
519 /// If the order is not deterministic, use [`Optional::flatten_unordered`] instead.
520 ///
521 /// # Example
522 /// ```rust
523 /// # #[cfg(feature = "deploy")] {
524 /// # use hydro_lang::prelude::*;
525 /// # use futures::StreamExt;
526 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
527 /// let tick = process.tick();
528 /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
529 /// optional.flatten_ordered().all_ticks()
530 /// # }, |mut stream| async move {
531 /// // 1, 2, 3
532 /// # for w in vec![1, 2, 3] {
533 /// # assert_eq!(stream.next().await.unwrap(), w);
534 /// # }
535 /// # }));
536 /// # }
537 /// ```
538 pub fn flatten_ordered<U>(self) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
539 where
540 B: IsBounded,
541 T: IntoIterator<Item = U>,
542 {
543 self.flat_map_ordered(q!(|v| v))
544 }
545
546 /// Like [`Optional::flatten_ordered`], but allows the implementation of [`Iterator`]
547 /// for the element type `T` to produce items in any order.
548 ///
549 /// If the optional is empty, the output stream is also empty. If the optional contains
550 /// a value that implements [`IntoIterator`], all items from that iterator are emitted
551 /// in the output stream in non-deterministic order.
552 ///
553 /// # Example
554 /// ```rust
555 /// # #[cfg(feature = "deploy")] {
556 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
557 /// # use futures::StreamExt;
558 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
559 /// let tick = process.tick();
560 /// let optional = tick.optional_first_tick(q!(
561 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
562 /// ));
563 /// optional.flatten_unordered().all_ticks()
564 /// # }, |mut stream| async move {
565 /// // 1, 2, 3, but in no particular order
566 /// # let mut results = Vec::new();
567 /// # for _ in 0..3 {
568 /// # results.push(stream.next().await.unwrap());
569 /// # }
570 /// # results.sort();
571 /// # assert_eq!(results, vec![1, 2, 3]);
572 /// # }));
573 /// # }
574 /// ```
575 pub fn flatten_unordered<U>(self) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
576 where
577 B: IsBounded,
578 T: IntoIterator<Item = U>,
579 {
580 self.flat_map_unordered(q!(|v| v))
581 }
582
583 /// Creates an optional containing only the value if it satisfies a predicate `f`.
584 ///
585 /// If the optional is empty, the output optional is also empty. If the optional contains
586 /// a value and the predicate returns `true`, the output optional contains the same value.
587 /// If the predicate returns `false`, the output optional is empty.
588 ///
589 /// The closure `f` receives a reference `&T` rather than an owned value `T` because filtering does
590 /// not modify or take ownership of the value. If you need to modify the value while filtering
591 /// use [`Optional::filter_map`] instead.
592 ///
593 /// # Example
594 /// ```rust
595 /// # #[cfg(feature = "deploy")] {
596 /// # use hydro_lang::prelude::*;
597 /// # use futures::StreamExt;
598 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
599 /// let tick = process.tick();
600 /// let optional = tick.optional_first_tick(q!(5));
601 /// optional.filter(q!(|&x| x > 3)).all_ticks()
602 /// # }, |mut stream| async move {
603 /// // 5
604 /// # assert_eq!(stream.next().await.unwrap(), 5);
605 /// # }));
606 /// # }
607 /// ```
608 pub fn filter<F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>) -> Optional<T, L, B>
609 where
610 F: Fn(&T) -> bool + 'a,
611 {
612 let f = f
613 .splice_fn1_borrow_ctx(&OperatorContext::<L, B>::new(&self.location))
614 .into();
615 Optional::new(
616 self.location.clone(),
617 HydroNode::Filter {
618 f,
619 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
620 metadata: self.location.new_node_metadata(Self::collection_kind()),
621 },
622 )
623 }
624
625 /// An operator that both filters and maps. It yields only the value if the supplied
626 /// closure `f` returns `Some(value)`.
627 ///
628 /// If the optional is empty, the output optional is also empty. If the optional contains
629 /// a value and the closure returns `Some(new_value)`, the output optional contains `new_value`.
630 /// If the closure returns `None`, the output optional is empty.
631 ///
632 /// # Example
633 /// ```rust
634 /// # #[cfg(feature = "deploy")] {
635 /// # use hydro_lang::prelude::*;
636 /// # use futures::StreamExt;
637 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
638 /// let tick = process.tick();
639 /// let optional = tick.optional_first_tick(q!("42"));
640 /// optional
641 /// .filter_map(q!(|s| s.parse::<i32>().ok()))
642 /// .all_ticks()
643 /// # }, |mut stream| async move {
644 /// // 42
645 /// # assert_eq!(stream.next().await.unwrap(), 42);
646 /// # }));
647 /// # }
648 /// ```
649 pub fn filter_map<U, F>(
650 self,
651 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>,
652 ) -> Optional<U, L, B>
653 where
654 F: Fn(T) -> Option<U> + 'a,
655 {
656 let f = f
657 .splice_fn1_ctx(&OperatorContext::<L, B>::new(&self.location))
658 .into();
659 Optional::new(
660 self.location.clone(),
661 HydroNode::FilterMap {
662 f,
663 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
664 metadata: self
665 .location
666 .new_node_metadata(Optional::<U, L, B>::collection_kind()),
667 },
668 )
669 }
670
671 /// Combines this singleton with another [`Singleton`] or [`Optional`] by tupling their values.
672 ///
673 /// If the other value is a [`Optional`], the output will be non-null only if the argument is
674 /// non-null. This is useful for combining several pieces of state together.
675 ///
676 /// # Example
677 /// ```rust
678 /// # #[cfg(feature = "deploy")] {
679 /// # use hydro_lang::prelude::*;
680 /// # use futures::StreamExt;
681 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
682 /// let tick = process.tick();
683 /// let numbers = process
684 /// .source_iter(q!(vec![123, 456, 789]))
685 /// .batch(&tick, nondet!(/** test */));
686 /// let min = numbers.clone().min(); // Optional
687 /// let max = numbers.max(); // Optional
688 /// min.zip(max).all_ticks()
689 /// # }, |mut stream| async move {
690 /// // [(123, 789)]
691 /// # for w in vec![(123, 789)] {
692 /// # assert_eq!(stream.next().await.unwrap(), w);
693 /// # }
694 /// # }));
695 /// # }
696 /// ```
697 pub fn zip<O>(self, other: impl Into<Optional<O, L, B>>) -> Optional<(T, O), L, B>
698 where
699 B: IsBounded,
700 {
701 let other: Optional<O, L, B> = other.into();
702 check_matching_location(&self.location, &other.location);
703
704 if L::is_top_level()
705 && let Some(tick) = self.location.try_tick()
706 {
707 let self_location = self.location().clone();
708 let out = zip_inside_tick(
709 self.snapshot(&tick, nondet!(/** eventually stabilizes */)),
710 other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
711 )
712 .latest();
713
714 Optional::new(self_location, out.ir_node.replace(HydroNode::Placeholder))
715 } else {
716 zip_inside_tick(self, other)
717 }
718 }
719
720 /// Passes through `self` when it has a value, otherwise passes through `other`.
721 ///
722 /// Like [`Option::or`], this is helpful for defining a fallback for an [`Optional`], when the
723 /// fallback itself is an [`Optional`]. If the fallback is a [`Singleton`], you can use
724 /// [`Optional::unwrap_or`] to ensure that the output is always non-null.
725 ///
726 /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
727 /// of the inputs change (including to/from null states).
728 ///
729 /// # Example
730 /// ```rust
731 /// # #[cfg(feature = "deploy")] {
732 /// # use hydro_lang::prelude::*;
733 /// # use futures::StreamExt;
734 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
735 /// let tick = process.tick();
736 /// // ticks are lazy by default, forces the second tick to run
737 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
738 ///
739 /// let some_first_tick = tick.optional_first_tick(q!(123));
740 /// let some_second_tick = tick.optional_first_tick(q!(456)).defer_tick();
741 /// some_first_tick.or(some_second_tick).all_ticks()
742 /// # }, |mut stream| async move {
743 /// // [123 /* first tick */, 456 /* second tick */]
744 /// # for w in vec![123, 456] {
745 /// # assert_eq!(stream.next().await.unwrap(), w);
746 /// # }
747 /// # }));
748 /// # }
749 /// ```
750 pub fn or(self, other: Optional<T, L, B>) -> Optional<T, L, B> {
751 check_matching_location(&self.location, &other.location);
752
753 if L::is_top_level()
754 && !B::BOUNDED // only if unbounded we need to use a tick
755 && let Some(tick) = self.location.try_tick()
756 {
757 let self_location = self.location().clone();
758 let out = or_inside_tick(
759 self.snapshot(&tick, nondet!(/** eventually stabilizes */)),
760 other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
761 )
762 .latest();
763
764 Optional::new(self_location, out.ir_node.replace(HydroNode::Placeholder))
765 } else {
766 Optional::new(
767 self.location.clone(),
768 HydroNode::ChainFirst {
769 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
770 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
771 metadata: self.location.new_node_metadata(Self::collection_kind()),
772 },
773 )
774 }
775 }
776
777 /// Gets the contents of `self` when it has a value, otherwise passes through `other`.
778 ///
779 /// Like [`Option::unwrap_or`], this is helpful for defining a fallback for an [`Optional`].
780 /// If the fallback is not always defined (an [`Optional`]), you can use [`Optional::or`].
781 ///
782 /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
783 /// of the inputs change (including to/from null states).
784 ///
785 /// # Example
786 /// ```rust
787 /// # #[cfg(feature = "deploy")] {
788 /// # use hydro_lang::prelude::*;
789 /// # use futures::StreamExt;
790 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
791 /// let tick = process.tick();
792 /// // ticks are lazy by default, forces the later ticks to run
793 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
794 ///
795 /// let some_first_tick = tick.optional_first_tick(q!(123));
796 /// some_first_tick
797 /// .unwrap_or(tick.singleton(q!(456)))
798 /// .all_ticks()
799 /// # }, |mut stream| async move {
800 /// // [123 /* first tick */, 456 /* second tick */, 456 /* third tick */, 456, ...]
801 /// # for w in vec![123, 456, 456, 456] {
802 /// # assert_eq!(stream.next().await.unwrap(), w);
803 /// # }
804 /// # }));
805 /// # }
806 /// ```
807 pub fn unwrap_or(self, other: Singleton<T, L, B>) -> Singleton<T, L, B> {
808 let res_option = self.or(other.into());
809 Singleton::new(
810 res_option.location.clone(),
811 HydroNode::Cast {
812 inner: Box::new(res_option.ir_node.replace(HydroNode::Placeholder)),
813 metadata: res_option
814 .location
815 .new_node_metadata(Singleton::<T, L, B>::collection_kind()),
816 },
817 )
818 }
819
820 /// Gets the contents of `self` when it has a value, otherwise returns the default value of `T`.
821 ///
822 /// Like [`Option::unwrap_or_default`], this is helpful for defining a fallback for an
823 /// [`Optional`] when the default value of the type is a suitable fallback.
824 ///
825 /// # Example
826 /// ```rust
827 /// # #[cfg(feature = "deploy")] {
828 /// # use hydro_lang::prelude::*;
829 /// # use futures::StreamExt;
830 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
831 /// let tick = process.tick();
832 /// // ticks are lazy by default, forces the later ticks to run
833 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
834 ///
835 /// let some_first_tick = tick.optional_first_tick(q!(123i32));
836 /// some_first_tick.unwrap_or_default().all_ticks()
837 /// # }, |mut stream| async move {
838 /// // [123 /* first tick */, 0 /* second tick */, 0 /* third tick */, 0, ...]
839 /// # for w in vec![123, 0, 0, 0] {
840 /// # assert_eq!(stream.next().await.unwrap(), w);
841 /// # }
842 /// # }));
843 /// # }
844 /// ```
845 pub fn unwrap_or_default(self) -> Singleton<T, L, B>
846 where
847 T: Default + Clone,
848 {
849 self.into_singleton().map(q!(|v| v.unwrap_or_default()))
850 }
851
852 /// Converts this optional into a [`Singleton`] with a Rust [`Option`] as its contents.
853 ///
854 /// Useful for writing custom Rust code that needs to interact with both the null and non-null
855 /// states of the [`Optional`]. When possible, you should use the native APIs on [`Optional`]
856 /// so that Hydro can skip any computation on null values.
857 ///
858 /// # Example
859 /// ```rust
860 /// # #[cfg(feature = "deploy")] {
861 /// # use hydro_lang::prelude::*;
862 /// # use futures::StreamExt;
863 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
864 /// let tick = process.tick();
865 /// // ticks are lazy by default, forces the later ticks to run
866 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
867 ///
868 /// let some_first_tick = tick.optional_first_tick(q!(123));
869 /// some_first_tick.into_singleton().all_ticks()
870 /// # }, |mut stream| async move {
871 /// // [Some(123) /* first tick */, None /* second tick */, None /* third tick */, None, ...]
872 /// # for w in vec![Some(123), None, None, None] {
873 /// # assert_eq!(stream.next().await.unwrap(), w);
874 /// # }
875 /// # }));
876 /// # }
877 /// ```
878 pub fn into_singleton(self) -> Singleton<Option<T>, L, B>
879 where
880 T: Clone,
881 {
882 let none: syn::Expr = parse_quote!(::std::option::Option::None);
883
884 let none_singleton = Singleton::new(
885 self.location.clone(),
886 HydroNode::SingletonSource {
887 value: none.into(),
888 first_tick_only: false,
889 metadata: self
890 .location
891 .new_node_metadata(Singleton::<Option<T>, L, B>::collection_kind()),
892 },
893 );
894
895 self.map(q!(|v| Some(v))).unwrap_or(none_singleton)
896 }
897
898 /// Returns a [`Singleton`] containing `true` if this optional has a value, `false` otherwise.
899 ///
900 /// # Example
901 /// ```rust
902 /// # #[cfg(feature = "deploy")] {
903 /// # use hydro_lang::prelude::*;
904 /// # use futures::StreamExt;
905 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
906 /// let tick = process.tick();
907 /// // ticks are lazy by default, forces the second tick to run
908 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
909 ///
910 /// let some_first_tick = tick.optional_first_tick(q!(42));
911 /// some_first_tick.is_some().all_ticks()
912 /// # }, |mut stream| async move {
913 /// // [true /* first tick */, false /* second tick */, ...]
914 /// # for w in vec![true, false] {
915 /// # assert_eq!(stream.next().await.unwrap(), w);
916 /// # }
917 /// # }));
918 /// # }
919 /// ```
920 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
921 pub fn is_some(self) -> Singleton<bool, L, B> {
922 self.map(q!(|_| ()))
923 .into_singleton()
924 .map(q!(|o| o.is_some()))
925 }
926
927 /// Returns a [`Singleton`] containing `true` if this optional is null, `false` otherwise.
928 ///
929 /// # Example
930 /// ```rust
931 /// # #[cfg(feature = "deploy")] {
932 /// # use hydro_lang::prelude::*;
933 /// # use futures::StreamExt;
934 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
935 /// let tick = process.tick();
936 /// // ticks are lazy by default, forces the second tick to run
937 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
938 ///
939 /// let some_first_tick = tick.optional_first_tick(q!(42));
940 /// some_first_tick.is_none().all_ticks()
941 /// # }, |mut stream| async move {
942 /// // [false /* first tick */, true /* second tick */, ...]
943 /// # for w in vec![false, true] {
944 /// # assert_eq!(stream.next().await.unwrap(), w);
945 /// # }
946 /// # }));
947 /// # }
948 /// ```
949 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
950 pub fn is_none(self) -> Singleton<bool, L, B> {
951 self.map(q!(|_| ()))
952 .into_singleton()
953 .map(q!(|o| o.is_none()))
954 }
955
956 /// Returns a [`Singleton`] containing `true` if both optionals are non-null and their
957 /// values are equal, `false` otherwise (including when either is null).
958 ///
959 /// # Example
960 /// ```rust
961 /// # #[cfg(feature = "deploy")] {
962 /// # use hydro_lang::prelude::*;
963 /// # use futures::StreamExt;
964 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
965 /// let tick = process.tick();
966 /// // ticks are lazy by default, forces the second tick to run
967 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
968 ///
969 /// let a = tick.optional_first_tick(q!(5)); // Some(5), None
970 /// let b = tick.optional_first_tick(q!(5)); // Some(5), None
971 /// a.is_some_and_equals(b).all_ticks()
972 /// # }, |mut stream| async move {
973 /// // [true, false]
974 /// # for w in vec![true, false] {
975 /// # assert_eq!(stream.next().await.unwrap(), w);
976 /// # }
977 /// # }));
978 /// # }
979 /// ```
980 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
981 pub fn is_some_and_equals(self, other: Optional<T, L, B>) -> Singleton<bool, L, B>
982 where
983 T: PartialEq + Clone,
984 B: IsBounded,
985 {
986 self.into_singleton()
987 .zip(other.into_singleton())
988 .map(q!(|(a, b)| a.is_some() && a == b))
989 }
990
991 /// An operator which allows you to "name" a `HydroNode`.
992 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
993 pub fn ir_node_named(self, name: &str) -> Optional<T, L, B> {
994 {
995 let mut node = self.ir_node.borrow_mut();
996 let metadata = node.metadata_mut();
997 metadata.tag = Some(name.to_owned());
998 }
999 self
1000 }
1001
1002 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
1003 /// implies that `B == Bounded`.
1004 pub fn make_bounded(self) -> Optional<T, L, Bounded>
1005 where
1006 B: IsBounded,
1007 {
1008 Optional::new(
1009 self.location.clone(),
1010 self.ir_node.replace(HydroNode::Placeholder),
1011 )
1012 }
1013
1014 /// Clones this bounded optional into a tick, returning a optional that has the
1015 /// same value as the outer optional. Because the outer optional is bounded, this
1016 /// is deterministic because there is only a single immutable version.
1017 pub fn clone_into_tick(self, tick: &Tick<L>) -> Optional<T, Tick<L>, Bounded>
1018 where
1019 B: IsBounded,
1020 T: Clone,
1021 {
1022 // TODO(shadaj): avoid printing simulator logs for this snapshot
1023 let inner = self.snapshot(
1024 tick,
1025 nondet!(/** bounded top-level optional so deterministic */),
1026 );
1027 Optional::new(tick.clone(), inner.ir_node.replace(HydroNode::Placeholder))
1028 }
1029
1030 /// Converts this optional into a [`Stream`] containing a single element, the value, if it is
1031 /// non-null. Otherwise, the stream is empty.
1032 ///
1033 /// # Example
1034 /// ```rust
1035 /// # #[cfg(feature = "deploy")] {
1036 /// # use hydro_lang::prelude::*;
1037 /// # use futures::StreamExt;
1038 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1039 /// # let tick = process.tick();
1040 /// # // ticks are lazy by default, forces the second tick to run
1041 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1042 /// # let batch_first_tick = process
1043 /// # .source_iter(q!(vec![]))
1044 /// # .batch(&tick, nondet!(/** test */));
1045 /// # let batch_second_tick = process
1046 /// # .source_iter(q!(vec![123, 456]))
1047 /// # .batch(&tick, nondet!(/** test */))
1048 /// # .defer_tick(); // appears on the second tick
1049 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1050 /// input_batch // first tick: [], second tick: [123, 456]
1051 /// .clone()
1052 /// .max()
1053 /// .into_stream()
1054 /// .chain(input_batch)
1055 /// .all_ticks()
1056 /// # }, |mut stream| async move {
1057 /// // [456, 123, 456]
1058 /// # for w in vec![456, 123, 456] {
1059 /// # assert_eq!(stream.next().await.unwrap(), w);
1060 /// # }
1061 /// # }));
1062 /// # }
1063 /// ```
1064 pub fn into_stream(self) -> Stream<T, L, Bounded, TotalOrder, ExactlyOnce>
1065 where
1066 B: IsBounded,
1067 {
1068 Stream::new(
1069 self.location.clone(),
1070 HydroNode::Cast {
1071 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1072 metadata: self.location.new_node_metadata(Stream::<
1073 T,
1074 Tick<L>,
1075 Bounded,
1076 TotalOrder,
1077 ExactlyOnce,
1078 >::collection_kind()),
1079 },
1080 )
1081 }
1082
1083 /// Filters this optional, passing through the value if the boolean signal is `true`,
1084 /// otherwise the output is null.
1085 ///
1086 /// # Example
1087 /// ```rust
1088 /// # #[cfg(feature = "deploy")] {
1089 /// # use hydro_lang::prelude::*;
1090 /// # use futures::StreamExt;
1091 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1092 /// let tick = process.tick();
1093 /// // ticks are lazy by default, forces the second tick to run
1094 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1095 ///
1096 /// let some_first_tick = tick.optional_first_tick(q!(()));
1097 /// let signal = some_first_tick.is_some(); // true on first tick, false on second
1098 /// let batch_first_tick = process
1099 /// .source_iter(q!(vec![456]))
1100 /// .batch(&tick, nondet!(/** test */));
1101 /// let batch_second_tick = process
1102 /// .source_iter(q!(vec![789]))
1103 /// .batch(&tick, nondet!(/** test */))
1104 /// .defer_tick();
1105 /// batch_first_tick.chain(batch_second_tick).first()
1106 /// .filter_if(signal)
1107 /// .unwrap_or(tick.singleton(q!(0)))
1108 /// .all_ticks()
1109 /// # }, |mut stream| async move {
1110 /// // [456, 0]
1111 /// # for w in vec![456, 0] {
1112 /// # assert_eq!(stream.next().await.unwrap(), w);
1113 /// # }
1114 /// # }));
1115 /// # }
1116 /// ```
1117 pub fn filter_if(self, signal: Singleton<bool, L, B>) -> Optional<T, L, B>
1118 where
1119 B: IsBounded,
1120 {
1121 self.zip(signal.filter(q!(|b| *b))).map(q!(|(d, _)| d))
1122 }
1123
1124 /// Filters this optional, passing through the optional value if it is non-null **and** the
1125 /// argument (a [`Bounded`] [`Optional`]`) is non-null, otherwise the output is null.
1126 ///
1127 /// Useful for conditionally processing, such as only emitting an optional's value outside
1128 /// a tick if some other condition is satisfied.
1129 ///
1130 /// # Example
1131 /// ```rust
1132 /// # #[cfg(feature = "deploy")] {
1133 /// # use hydro_lang::prelude::*;
1134 /// # use futures::StreamExt;
1135 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1136 /// let tick = process.tick();
1137 /// // ticks are lazy by default, forces the second tick to run
1138 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1139 ///
1140 /// let batch_first_tick = process
1141 /// .source_iter(q!(vec![]))
1142 /// .batch(&tick, nondet!(/** test */));
1143 /// let batch_second_tick = process
1144 /// .source_iter(q!(vec![456]))
1145 /// .batch(&tick, nondet!(/** test */))
1146 /// .defer_tick(); // appears on the second tick
1147 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1148 /// batch_first_tick.chain(batch_second_tick).first()
1149 /// .filter_if_some(some_on_first_tick)
1150 /// .unwrap_or(tick.singleton(q!(789)))
1151 /// .all_ticks()
1152 /// # }, |mut stream| async move {
1153 /// // [789, 789]
1154 /// # for w in vec![789, 789] {
1155 /// # assert_eq!(stream.next().await.unwrap(), w);
1156 /// # }
1157 /// # }));
1158 /// # }
1159 /// ```
1160 #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1161 pub fn filter_if_some<U>(self, signal: Optional<U, L, B>) -> Optional<T, L, B>
1162 where
1163 B: IsBounded,
1164 {
1165 self.filter_if(signal.is_some())
1166 }
1167
1168 /// Filters this optional, passing through the optional value if it is non-null **and** the
1169 /// argument (a [`Bounded`] [`Optional`]`) is _null_, otherwise the output is null.
1170 ///
1171 /// Useful for conditionally processing, such as only emitting an optional's value outside
1172 /// a tick if some other condition is satisfied.
1173 ///
1174 /// # Example
1175 /// ```rust
1176 /// # #[cfg(feature = "deploy")] {
1177 /// # use hydro_lang::prelude::*;
1178 /// # use futures::StreamExt;
1179 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1180 /// let tick = process.tick();
1181 /// // ticks are lazy by default, forces the second tick to run
1182 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1183 ///
1184 /// let batch_first_tick = process
1185 /// .source_iter(q!(vec![]))
1186 /// .batch(&tick, nondet!(/** test */));
1187 /// let batch_second_tick = process
1188 /// .source_iter(q!(vec![456]))
1189 /// .batch(&tick, nondet!(/** test */))
1190 /// .defer_tick(); // appears on the second tick
1191 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1192 /// batch_first_tick.chain(batch_second_tick).first()
1193 /// .filter_if_none(some_on_first_tick)
1194 /// .unwrap_or(tick.singleton(q!(789)))
1195 /// .all_ticks()
1196 /// # }, |mut stream| async move {
1197 /// // [789, 789]
1198 /// # for w in vec![789, 456] {
1199 /// # assert_eq!(stream.next().await.unwrap(), w);
1200 /// # }
1201 /// # }));
1202 /// # }
1203 /// ```
1204 #[deprecated(note = "use `filter_if` with `!Optional::is_some()` instead")]
1205 pub fn filter_if_none<U>(self, other: Optional<U, L, B>) -> Optional<T, L, B>
1206 where
1207 B: IsBounded,
1208 {
1209 self.filter_if(other.is_none())
1210 }
1211
1212 /// If `self` is null, emits a null optional, but if it non-null, emits `value`.
1213 ///
1214 /// Useful for gating the release of a [`Singleton`] on a condition of the [`Optional`]
1215 /// having a value, such as only releasing a piece of state if the node is the leader.
1216 ///
1217 /// # Example
1218 /// ```rust
1219 /// # #[cfg(feature = "deploy")] {
1220 /// # use hydro_lang::prelude::*;
1221 /// # use futures::StreamExt;
1222 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1223 /// let tick = process.tick();
1224 /// // ticks are lazy by default, forces the second tick to run
1225 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1226 ///
1227 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1228 /// some_on_first_tick
1229 /// .if_some_then(tick.singleton(q!(456)))
1230 /// .unwrap_or(tick.singleton(q!(123)))
1231 /// # .all_ticks()
1232 /// # }, |mut stream| async move {
1233 /// // 456 (first tick) ~> 123 (second tick onwards)
1234 /// # for w in vec![456, 123, 123] {
1235 /// # assert_eq!(stream.next().await.unwrap(), w);
1236 /// # }
1237 /// # }));
1238 /// # }
1239 /// ```
1240 #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1241 pub fn if_some_then<U>(self, value: Singleton<U, L, B>) -> Optional<U, L, B>
1242 where
1243 B: IsBounded,
1244 {
1245 value.filter_if(self.is_some())
1246 }
1247}
1248
1249impl<'a, K, V, L, B: Boundedness> Optional<(K, V), L, B>
1250where
1251 L: Location<'a>,
1252{
1253 /// Converts this optional into a [`KeyedSingleton`] containing a single entry with the
1254 /// key-value pair of this [`Optional`].
1255 ///
1256 /// If this [`Optional`] is [`Bounded`], the [`KeyedSingleton`] will be [`Bounded`] as well
1257 /// if it is [`Unbounded`], the [`KeyedSingleton`] will be [`Unbounded`], which means that
1258 /// the entry will be updated and appear / disappear according to the state of the
1259 /// [`Optional`].
1260 pub fn into_keyed_singleton(self) -> KeyedSingleton<K, V, L, B> {
1261 KeyedSingleton::new(
1262 self.location.clone(),
1263 HydroNode::Cast {
1264 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1265 metadata: self
1266 .location
1267 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1268 },
1269 )
1270 }
1271}
1272
1273impl<'a, T, L, B: Boundedness> Optional<T, Atomic<L>, B>
1274where
1275 L: Location<'a>,
1276{
1277 /// Returns an optional value corresponding to the latest snapshot of the optional
1278 /// being atomically processed. The snapshot at tick `t + 1` is guaranteed to include
1279 /// at least all relevant data that contributed to the snapshot at tick `t`. Furthermore,
1280 /// all snapshots of this optional into the atomic-associated tick will observe the
1281 /// same value each tick.
1282 ///
1283 /// # Non-Determinism
1284 /// Because this picks a snapshot of a optional whose value is continuously changing,
1285 /// the output optional has a non-deterministic value since the snapshot can be at an
1286 /// arbitrary point in time.
1287 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1288 self,
1289 tick: &Tick<L2>,
1290 _nondet: NonDet,
1291 ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1292 Optional::new(
1293 tick.drop_consistency(),
1294 HydroNode::Batch {
1295 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1296 metadata: tick
1297 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1298 },
1299 )
1300 }
1301}
1302
1303impl<'a, T, L, B: Boundedness> Optional<T, L, B>
1304where
1305 L: Location<'a>,
1306{
1307 /// Given a tick, returns a optional value corresponding to a snapshot of the optional
1308 /// as of that tick. The snapshot at tick `t + 1` is guaranteed to include at least all
1309 /// relevant data that contributed to the snapshot at tick `t`.
1310 ///
1311 /// # Non-Determinism
1312 /// Because this picks a snapshot of a optional whose value is continuously changing,
1313 /// the output optional has a non-deterministic value since the snapshot can be at an
1314 /// arbitrary point in time.
1315 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1316 self,
1317 tick: &Tick<L2>,
1318 _nondet: NonDet,
1319 ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1320 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1321 Optional::new(
1322 tick.drop_consistency(),
1323 HydroNode::Batch {
1324 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1325 metadata: tick
1326 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1327 },
1328 )
1329 }
1330
1331 /// Eagerly samples the optional as fast as possible, returning a stream of snapshots
1332 /// with order corresponding to increasing prefixes of data contributing to the optional.
1333 ///
1334 /// # Non-Determinism
1335 /// At runtime, the optional will be arbitrarily sampled as fast as possible, but due
1336 /// to non-deterministic batching and arrival of inputs, the output stream is
1337 /// non-deterministic.
1338 pub fn sample_eager(
1339 self,
1340 nondet: NonDet,
1341 ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce> {
1342 let tick = self.location.tick();
1343 self.snapshot(&tick, nondet).all_ticks().weaken_retries()
1344 }
1345
1346 /// Given a time interval, returns a stream corresponding to snapshots of the optional
1347 /// value taken at various points in time. Because the input optional may be
1348 /// [`Unbounded`], there are no guarantees on what these snapshots are other than they
1349 /// represent the value of the optional given some prefix of the streams leading up to
1350 /// it.
1351 ///
1352 /// # Non-Determinism
1353 /// The output stream is non-deterministic in which elements are sampled, since this
1354 /// is controlled by a clock.
1355 #[cfg(feature = "tokio")]
1356 pub fn sample_every(
1357 self,
1358 interval: impl QuotedWithContext<'a, std::time::Duration, L> + Copy + 'a,
1359 nondet: NonDet,
1360 ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce>
1361 where
1362 L: TopLevel<'a>,
1363 {
1364 let samples = self.location.source_interval(interval);
1365 let tick = self.location.tick();
1366
1367 self.snapshot(&tick, nondet)
1368 .filter_if(samples.batch(&tick, nondet).first().is_some())
1369 .all_ticks()
1370 .weaken_retries()
1371 }
1372}
1373
1374impl<'a, T, L> Optional<T, Tick<L>, Bounded>
1375where
1376 L: Location<'a>,
1377{
1378 /// Asynchronously yields the value of this singleton outside the tick as an unbounded stream,
1379 /// which will stream the value computed in _each_ tick as a separate stream element (skipping
1380 /// null values).
1381 ///
1382 /// Unlike [`Optional::latest`], the value computed in each tick is emitted separately,
1383 /// producing one element in the output for each (non-null) tick. This is useful for batched
1384 /// computations, where the results from each tick must be combined together.
1385 ///
1386 /// # Example
1387 /// ```rust
1388 /// # #[cfg(feature = "deploy")] {
1389 /// # use hydro_lang::prelude::*;
1390 /// # use futures::StreamExt;
1391 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1392 /// # let tick = process.tick();
1393 /// # // ticks are lazy by default, forces the second tick to run
1394 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1395 /// # let batch_first_tick = process
1396 /// # .source_iter(q!(vec![]))
1397 /// # .batch(&tick, nondet!(/** test */));
1398 /// # let batch_second_tick = process
1399 /// # .source_iter(q!(vec![1, 2, 3]))
1400 /// # .batch(&tick, nondet!(/** test */))
1401 /// # .defer_tick(); // appears on the second tick
1402 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1403 /// input_batch // first tick: [], second tick: [1, 2, 3]
1404 /// .max()
1405 /// .all_ticks()
1406 /// # }, |mut stream| async move {
1407 /// // [3]
1408 /// # for w in vec![3] {
1409 /// # assert_eq!(stream.next().await.unwrap(), w);
1410 /// # }
1411 /// # }));
1412 /// # }
1413 /// ```
1414 pub fn all_ticks(self) -> Stream<T, L, Unbounded, TotalOrder, ExactlyOnce> {
1415 self.into_stream().all_ticks()
1416 }
1417
1418 /// Synchronously yields the value of this optional outside the tick as an unbounded stream,
1419 /// which will stream the value computed in _each_ tick as a separate stream element.
1420 ///
1421 /// Unlike [`Optional::all_ticks`], this preserves synchronous execution, as the output stream
1422 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1423 /// optional's [`Tick`] context.
1424 pub fn all_ticks_atomic(self) -> Stream<T, Atomic<L>, Unbounded, TotalOrder, ExactlyOnce> {
1425 self.into_stream().all_ticks_atomic()
1426 }
1427
1428 /// Asynchronously yields this optional outside the tick as an unbounded optional, which will
1429 /// be asynchronously updated with the latest value of the optional inside the tick, including
1430 /// whether the optional is null or not.
1431 ///
1432 /// This converts a bounded value _inside_ a tick into an asynchronous value outside the
1433 /// tick that tracks the inner value. This is useful for getting the value as of the
1434 /// "most recent" tick, but note that updates are propagated asynchronously outside the tick.
1435 ///
1436 /// # Example
1437 /// ```rust
1438 /// # #[cfg(feature = "deploy")] {
1439 /// # use hydro_lang::prelude::*;
1440 /// # use futures::StreamExt;
1441 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1442 /// # let tick = process.tick();
1443 /// # // ticks are lazy by default, forces the second tick to run
1444 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1445 /// # let batch_first_tick = process
1446 /// # .source_iter(q!(vec![]))
1447 /// # .batch(&tick, nondet!(/** test */));
1448 /// # let batch_second_tick = process
1449 /// # .source_iter(q!(vec![1, 2, 3]))
1450 /// # .batch(&tick, nondet!(/** test */))
1451 /// # .defer_tick(); // appears on the second tick
1452 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1453 /// input_batch // first tick: [], second tick: [1, 2, 3]
1454 /// .max()
1455 /// .latest()
1456 /// # .into_singleton()
1457 /// # .sample_eager(nondet!(/** test */))
1458 /// # }, |mut stream| async move {
1459 /// // asynchronously changes from None ~> 3
1460 /// # for w in vec![None, Some(3)] {
1461 /// # assert_eq!(stream.next().await.unwrap(), w);
1462 /// # }
1463 /// # }));
1464 /// # }
1465 /// ```
1466 pub fn latest(self) -> Optional<T, L, Unbounded> {
1467 Optional::new(
1468 self.location.outer().clone(),
1469 HydroNode::YieldConcat {
1470 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1471 metadata: self
1472 .location
1473 .outer()
1474 .new_node_metadata(Optional::<T, L, Unbounded>::collection_kind()),
1475 },
1476 )
1477 }
1478
1479 /// Synchronously yields this optional outside the tick as an unbounded optional, which will
1480 /// be updated with the latest value of the optional inside the tick.
1481 ///
1482 /// Unlike [`Optional::latest`], this preserves synchronous execution, as the output optional
1483 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1484 /// optional's [`Tick`] context.
1485 pub fn latest_atomic(self) -> Optional<T, Atomic<L>, Unbounded> {
1486 let out_location = Atomic {
1487 tick: self.location.clone(),
1488 };
1489
1490 Optional::new(
1491 out_location.clone(),
1492 HydroNode::YieldConcat {
1493 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1494 metadata: out_location
1495 .new_node_metadata(Optional::<T, Atomic<L>, Unbounded>::collection_kind()),
1496 },
1497 )
1498 }
1499
1500 /// Shifts the state in `self` to the **next tick**, so that the returned optional at tick `T`
1501 /// always has the state of `self` at tick `T - 1`.
1502 ///
1503 /// At tick `0`, the output optional is null, since there is no previous tick.
1504 ///
1505 /// This operator enables stateful iterative processing with ticks, by sending data from one
1506 /// tick to the next. For example, you can use it to compare state across consecutive batches.
1507 ///
1508 /// # Example
1509 /// ```rust
1510 /// # #[cfg(feature = "deploy")] {
1511 /// # use hydro_lang::prelude::*;
1512 /// # use futures::StreamExt;
1513 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1514 /// let tick = process.tick();
1515 /// // ticks are lazy by default, forces the second tick to run
1516 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1517 ///
1518 /// let batch_first_tick = process
1519 /// .source_iter(q!(vec![1, 2]))
1520 /// .batch(&tick, nondet!(/** test */));
1521 /// let batch_second_tick = process
1522 /// .source_iter(q!(vec![3, 4]))
1523 /// .batch(&tick, nondet!(/** test */))
1524 /// .defer_tick(); // appears on the second tick
1525 /// let current_tick_sum = batch_first_tick.chain(batch_second_tick)
1526 /// .reduce(q!(|state, v| *state += v));
1527 ///
1528 /// current_tick_sum.clone().into_singleton().zip(
1529 /// current_tick_sum.defer_tick().into_singleton() // state from previous tick
1530 /// ).all_ticks()
1531 /// # }, |mut stream| async move {
1532 /// // [(Some(3), None) /* first tick */, (Some(7), Some(3)) /* second tick */]
1533 /// # for w in vec![(Some(3), None), (Some(7), Some(3))] {
1534 /// # assert_eq!(stream.next().await.unwrap(), w);
1535 /// # }
1536 /// # }));
1537 /// # }
1538 /// ```
1539 pub fn defer_tick(self) -> Optional<T, Tick<L>, Bounded> {
1540 Optional::new(
1541 self.location.clone(),
1542 HydroNode::DeferTick {
1543 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1544 metadata: self.location.new_node_metadata(Self::collection_kind()),
1545 },
1546 )
1547 }
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552 #[cfg(feature = "deploy")]
1553 use futures::StreamExt;
1554 #[cfg(feature = "deploy")]
1555 use hydro_deploy::Deployment;
1556 #[cfg(any(feature = "deploy", feature = "sim"))]
1557 use stageleft::q;
1558
1559 #[cfg(feature = "deploy")]
1560 use super::Optional;
1561 #[cfg(any(feature = "deploy", feature = "sim"))]
1562 use crate::compile::builder::FlowBuilder;
1563 #[cfg(any(feature = "deploy", feature = "sim"))]
1564 use crate::location::Location;
1565 #[cfg(feature = "deploy")]
1566 use crate::nondet::nondet;
1567
1568 #[cfg(feature = "deploy")]
1569 #[tokio::test]
1570 async fn optional_or_cardinality() {
1571 let mut deployment = Deployment::new();
1572
1573 let mut flow = FlowBuilder::new();
1574 let node = flow.process::<()>();
1575 let external = flow.external::<()>();
1576
1577 let node_tick = node.tick();
1578 let tick_singleton = node_tick.singleton(q!(123));
1579 let tick_optional_inhabited: Optional<_, _, _> = tick_singleton.into();
1580 let counts = tick_optional_inhabited
1581 .clone()
1582 .or(tick_optional_inhabited)
1583 .into_stream()
1584 .count()
1585 .all_ticks()
1586 .send_bincode_external(&external);
1587
1588 let nodes = flow
1589 .with_process(&node, deployment.Localhost())
1590 .with_external(&external, deployment.Localhost())
1591 .deploy(&mut deployment);
1592
1593 deployment.deploy().await.unwrap();
1594
1595 let mut external_out = nodes.connect(counts).await;
1596
1597 deployment.start().await.unwrap();
1598
1599 assert_eq!(external_out.next().await.unwrap(), 1);
1600 }
1601
1602 #[cfg(feature = "deploy")]
1603 #[tokio::test]
1604 async fn into_singleton_top_level_none_cardinality() {
1605 let mut deployment = Deployment::new();
1606
1607 let mut flow = FlowBuilder::new();
1608 let node = flow.process::<()>();
1609 let external = flow.external::<()>();
1610
1611 let node_tick = node.tick();
1612 let top_level_none = node.singleton(q!(123)).filter(q!(|_| false));
1613 let into_singleton = top_level_none.into_singleton();
1614
1615 let tick_driver = node.spin();
1616
1617 let counts = into_singleton
1618 .snapshot(&node_tick, nondet!(/** test */))
1619 .into_stream()
1620 .count()
1621 .zip(tick_driver.batch(&node_tick, nondet!(/** test */)).count())
1622 .map(q!(|(c, _)| c))
1623 .all_ticks()
1624 .send_bincode_external(&external);
1625
1626 let nodes = flow
1627 .with_process(&node, deployment.Localhost())
1628 .with_external(&external, deployment.Localhost())
1629 .deploy(&mut deployment);
1630
1631 deployment.deploy().await.unwrap();
1632
1633 let mut external_out = nodes.connect(counts).await;
1634
1635 deployment.start().await.unwrap();
1636
1637 assert_eq!(external_out.next().await.unwrap(), 1);
1638 assert_eq!(external_out.next().await.unwrap(), 1);
1639 assert_eq!(external_out.next().await.unwrap(), 1);
1640 }
1641
1642 #[cfg(feature = "deploy")]
1643 #[tokio::test]
1644 async fn into_singleton_unbounded_top_level_none_cardinality() {
1645 let mut deployment = Deployment::new();
1646
1647 let mut flow = FlowBuilder::new();
1648 let node = flow.process::<()>();
1649 let external = flow.external::<()>();
1650
1651 let top_level_none = node
1652 .tick()
1653 .singleton(q!(123))
1654 .latest()
1655 .filter(q!(|_| false));
1656 let into_singleton = top_level_none.into_singleton();
1657
1658 let tick_driver = node.spin();
1659
1660 let tick_later = node.tick();
1661 let counts = into_singleton
1662 .snapshot(&tick_later, nondet!(/** test */))
1663 .into_stream()
1664 .count()
1665 .zip(tick_driver.batch(&tick_later, nondet!(/** test */)).count())
1666 .map(q!(|(c, _)| c))
1667 .all_ticks()
1668 .send_bincode_external(&external);
1669
1670 let nodes = flow
1671 .with_process(&node, deployment.Localhost())
1672 .with_external(&external, deployment.Localhost())
1673 .deploy(&mut deployment);
1674
1675 deployment.deploy().await.unwrap();
1676
1677 let mut external_out = nodes.connect(counts).await;
1678
1679 deployment.start().await.unwrap();
1680
1681 assert_eq!(external_out.next().await.unwrap(), 1);
1682 assert_eq!(external_out.next().await.unwrap(), 1);
1683 assert_eq!(external_out.next().await.unwrap(), 1);
1684 }
1685
1686 #[cfg(feature = "sim")]
1687 #[test]
1688 fn top_level_optional_some_into_stream_no_replay() {
1689 let mut flow = FlowBuilder::new();
1690 let node = flow.process::<()>();
1691
1692 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1693 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1694 let filtered_some = folded.filter(q!(|_| true));
1695
1696 let out_recv = filtered_some.into_stream().sim_output();
1697
1698 flow.sim().exhaustive(async || {
1699 out_recv.assert_yields_only([10]).await;
1700 });
1701 }
1702
1703 #[cfg(feature = "sim")]
1704 #[test]
1705 fn top_level_optional_none_into_stream_no_replay() {
1706 let mut flow = FlowBuilder::new();
1707 let node = flow.process::<()>();
1708
1709 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1710 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1711 let filtered_none = folded.filter(q!(|_| false));
1712
1713 let out_recv = filtered_none.into_stream().sim_output();
1714
1715 flow.sim().exhaustive(async || {
1716 out_recv.assert_yields_only([] as [i32; 0]).await;
1717 });
1718 }
1719
1720 #[cfg(feature = "deploy")]
1721 #[tokio::test]
1722 async fn test_optional_ref() {
1723 let mut deployment = Deployment::new();
1724
1725 let mut flow = FlowBuilder::new();
1726 let external = flow.external::<()>();
1727 let p1 = flow.process::<()>();
1728
1729 // Create an optional: reduce 0..5 => Some(10) (sum via reduce)
1730 let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
1731
1732 let opt_ref = my_opt.by_ref();
1733
1734 // Use the optional ref in a map: unwrap_or(0) + x
1735 let out_port = p1
1736 .source_iter(q!(1..=3i32))
1737 .map(q!(|x| x + opt_ref.unwrap_or(0)))
1738 .send_bincode_external(&external);
1739
1740 let nodes = flow
1741 .with_default_optimize()
1742 .with_process(&p1, deployment.Localhost())
1743 .with_external(&external, deployment.Localhost())
1744 .deploy(&mut deployment);
1745
1746 deployment.deploy().await.unwrap();
1747
1748 let mut out_recv = nodes.connect(out_port).await;
1749
1750 deployment.start().await.unwrap();
1751
1752 let mut results = Vec::new();
1753 for _ in 0..3 {
1754 results.push(out_recv.next().await.unwrap());
1755 }
1756 results.sort();
1757 // reduce(0..5) = 10, so results should be 11, 12, 13
1758 assert_eq!(results, vec![11, 12, 13]);
1759 }
1760
1761 #[cfg(feature = "deploy")]
1762 #[tokio::test]
1763 async fn test_optional_ref_none() {
1764 let mut deployment = Deployment::new();
1765
1766 let mut flow = FlowBuilder::new();
1767 let external = flow.external::<()>();
1768 let p1 = flow.process::<()>();
1769
1770 // Create an optional from an empty source => None
1771 let my_opt = p1
1772 .source_iter(q!(std::iter::empty::<i32>()))
1773 .reduce(q!(|a, b| *a += b));
1774
1775 let opt_ref = my_opt.by_ref();
1776
1777 // Use the optional ref: should be None, so unwrap_or(99)
1778 let out_port = p1
1779 .source_iter(q!(1..=2i32))
1780 .map(q!(|x| x + opt_ref.unwrap_or(99)))
1781 .send_bincode_external(&external);
1782
1783 let nodes = flow
1784 .with_default_optimize()
1785 .with_process(&p1, deployment.Localhost())
1786 .with_external(&external, deployment.Localhost())
1787 .deploy(&mut deployment);
1788
1789 deployment.deploy().await.unwrap();
1790
1791 let mut out_recv = nodes.connect(out_port).await;
1792
1793 deployment.start().await.unwrap();
1794
1795 let mut results = Vec::new();
1796 for _ in 0..2 {
1797 results.push(out_recv.next().await.unwrap());
1798 }
1799 results.sort();
1800 // optional is None, so unwrap_or(99) => 100, 101
1801 assert_eq!(results, vec![100, 101]);
1802 }
1803
1804 #[cfg(feature = "deploy")]
1805 #[tokio::test]
1806 async fn test_optional_ref_and_consume() {
1807 let mut deployment = Deployment::new();
1808
1809 let mut flow = FlowBuilder::new();
1810 let external = flow.external::<()>();
1811 let p1 = flow.process::<()>();
1812
1813 // Use reduce to produce an Optional
1814 let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
1815
1816 let opt_ref = my_opt.by_ref();
1817
1818 // Reference path
1819 let out_port_ref = p1
1820 .source_iter(q!(1..=2i32))
1821 .map(q!(|x| x + opt_ref.unwrap_or(0)))
1822 .send_bincode_external(&external);
1823
1824 let nodes = flow
1825 .with_default_optimize()
1826 .with_process(&p1, deployment.Localhost())
1827 .with_external(&external, deployment.Localhost())
1828 .deploy(&mut deployment);
1829
1830 deployment.deploy().await.unwrap();
1831
1832 let mut out_recv_ref = nodes.connect(out_port_ref).await;
1833
1834 deployment.start().await.unwrap();
1835
1836 let mut ref_results = Vec::new();
1837 for _ in 0..2 {
1838 ref_results.push(out_recv_ref.next().await.unwrap());
1839 }
1840 ref_results.sort();
1841 // reduce(0..5) = 10, so 1+10=11, 2+10=12
1842 assert_eq!(ref_results, vec![11, 12]);
1843 }
1844}