Skip to main content

dataloader_rs/
transform.rs

1//! Per-item CPU work the loader applies to a dataset's items: [`Transform`].
2
3use crate::error::Result;
4
5/// A synchronous, CPU-side step the loader applies to every item, such as decoding bytes into
6/// an image. Set with [`DataLoaderBuilder::transform`](crate::DataLoaderBuilder::transform).
7///
8/// A dataset only fetches; the transform runs on the loader's threads, with the index of the
9/// item: right after `get` on the worker for a [`Dataset`](crate::Dataset), on the loader's
10/// rayon pool once the item is fetched for an `AsyncDataset` (feature `async`).
11///
12/// Closures `Fn(usize, In) -> Result<Out>` are transforms; [`then`](Self::then) chains two. A
13/// closure passed where a transform is expected needs its types written out, as Rust does not
14/// infer them through this trait:
15///
16/// ```rust
17/// use dataloader_rs::{Transform, error::Result};
18///
19/// let double = |_: usize, x: u64| -> Result<u64> { Ok(x * 2) };
20/// let label = |index: usize, x: u64| -> Result<String> { Ok(format!("{index}:{x}")) };
21/// assert_eq!(double.then(label).apply(7, 21).unwrap(), "7:42");
22/// ```
23pub trait Transform<In>: Send + Sync + 'static {
24    /// The transformed item.
25    type Output: Send + 'static;
26
27    /// Transform `item`, the item at `index`. Called concurrently from several threads.
28    fn apply(&self, index: usize, item: In) -> Result<Self::Output>;
29
30    /// Whether [`apply`](Self::apply) returns its input as is: the async path then applies
31    /// it where the item is fetched instead of handing it to the pool. Default: `false`.
32    fn is_identity(&self) -> bool {
33        false
34    }
35
36    /// Apply `self`, then `next` to its output.
37    fn then<T>(self, next: T) -> Then<Self, T>
38    where
39        Self: Sized,
40        T: Transform<Self::Output>,
41    {
42        Then {
43            first: self,
44            second: next,
45        }
46    }
47
48    /// Transform the items of a batch, `items[i]` being the item at `indices[i]`; both have
49    /// the same length. Default: [`apply`](Self::apply) to each item in order, up to the first
50    /// error. [`Identity`] returns the items as they are, without a pass over them.
51    fn apply_batch(&self, indices: &[usize], items: Vec<In>) -> Result<Vec<Self::Output>> {
52        items
53            .into_iter()
54            .zip(indices)
55            .map(|(item, &index)| self.apply(index, item))
56            .collect()
57    }
58}
59
60impl<In, Out, F> Transform<In> for F
61where
62    F: Fn(usize, In) -> Result<Out> + Send + Sync + 'static,
63    Out: Send + 'static,
64{
65    type Output = Out;
66
67    fn apply(&self, index: usize, item: In) -> Result<Out> {
68        self(index, item)
69    }
70}
71
72/// The loader's default transform: items are left as they are.
73#[derive(Clone, Copy, Debug, Default)]
74pub struct Identity;
75
76impl<In: Send + 'static> Transform<In> for Identity {
77    type Output = In;
78
79    fn apply_batch(&self, _indices: &[usize], items: Vec<In>) -> Result<Vec<In>> {
80        Ok(items)
81    }
82
83    fn apply(&self, _index: usize, item: In) -> Result<In> {
84        Ok(item)
85    }
86
87    fn is_identity(&self) -> bool {
88        true
89    }
90}
91
92/// Two transforms applied one after the other; see [`Transform::then`].
93#[derive(Clone, Debug)]
94pub struct Then<A, B> {
95    first: A,
96    second: B,
97}
98
99impl<In, A, B> Transform<In> for Then<A, B>
100where
101    A: Transform<In>,
102    B: Transform<A::Output>,
103{
104    type Output = B::Output;
105
106    fn apply(&self, index: usize, item: In) -> Result<Self::Output> {
107        self.second.apply(index, self.first.apply(index, item)?)
108    }
109}
110
111/// Apply `transform` to the items a `get_batch` returned for `indices`, in order.
112pub(crate) fn apply_all<In, T: Transform<In>>(
113    transform: &T,
114    indices: &[usize],
115    items: Vec<In>,
116) -> Result<Vec<T::Output>> {
117    // A short or long batch would silently pair items with the wrong indices.
118    if items.len() != indices.len() {
119        return Err(format!(
120            "get_batch returned {} items for {} indices",
121            items.len(),
122            indices.len()
123        )
124        .into());
125    }
126    transform.apply_batch(indices, items)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::{Identity, Transform, apply_all};
132    use crate::error::Result;
133
134    struct AddOne;
135
136    impl Transform<u32> for AddOne {
137        type Output = u32;
138
139        fn apply(&self, _index: usize, item: u32) -> Result<u32> {
140            item.checked_add(1).ok_or_else(|| "overflow".into())
141        }
142    }
143
144    #[test]
145    fn struct_and_closure_chain() {
146        let t =
147            AddOne.then(|index: usize, x: u32| -> Result<String> { Ok(format!("{index}<{x}>")) });
148        assert_eq!(t.apply(3, 1).unwrap(), "3<2>");
149        assert!(!t.is_identity());
150        assert!(Transform::<u32>::is_identity(&Identity));
151    }
152
153    #[test]
154    fn first_error_stops_the_chain() {
155        let t = AddOne.then(|_: usize, _: u32| -> Result<u32> { panic!("not reached") });
156        assert_eq!(t.apply(0, u32::MAX).unwrap_err().to_string(), "overflow");
157    }
158
159    #[test]
160    fn apply_all_pairs_items_with_indices_and_checks_the_count() {
161        let tag = |index: usize, x: u32| -> Result<(usize, u32)> { Ok((index, x)) };
162        assert_eq!(
163            apply_all(&tag, &[5, 2], vec![10, 20]).unwrap(),
164            vec![(5, 10), (2, 20)]
165        );
166        let err = apply_all(&tag, &[5, 2], vec![10]).unwrap_err();
167        assert_eq!(err.to_string(), "get_batch returned 1 items for 2 indices");
168    }
169
170    #[test]
171    fn identity_hands_the_batch_back_as_is() {
172        let items = vec![10u32, 20];
173        let buffer = items.as_ptr();
174        let out = apply_all(&Identity, &[5, 2], items).unwrap();
175        assert_eq!(out, vec![10, 20]);
176        assert_eq!(out.as_ptr(), buffer);
177        assert!(apply_all(&Identity, &[5, 2], vec![10u32]).is_err());
178    }
179}