Skip to main content

dataset_ml/
traits.rs

1//! The [`MlDataset`] trait shared by every loader in this crate.
2//!
3//! Each loader has its own inherent accessors, named for what it holds:
4//! `features()`/`labels()` for the tabular loaders, `targets()` for the regression
5//! loaders, `texts()` for the text corpora. Those names make each loader pleasant
6//! to use directly. Before this trait existed, though, no code could work with
7//! datasets *generically*.
8//!
9//! [`MlDataset`] is the common denominator: the container operations that are the
10//! same whatever the loader parses into. It adds three capabilities the inherent
11//! APIs never exposed:
12//!
13//! - [`invalidate`](MlDataset::invalidate) drops the in-memory cache and forces the
14//!   next access to re-read (and re-verify) the file on disk.
15//! - [`is_loaded`](MlDataset::is_loaded) and [`storage_dir`](MlDataset::storage_dir)
16//!   let you inspect a loader without touching the data.
17//! - [`n_samples`](MlDataset::n_samples) gives a uniform sample count that works
18//!   across the pair-shaped and triple-shaped datasets alike.
19//!
20//! This trait deliberately names its data accessors [`load`](MlDataset::load),
21//! [`peek`](MlDataset::peek), and [`unload`](MlDataset::unload) rather than reusing
22//! `data`, `get_data`, and `take_data`. This way, a trait method never silently
23//! shadows the inherent method of the same name, and the inherent method never
24//! shadows the trait method either. Both sets are always available and always
25//! agree. Use whichever reads better where you are.
26//!
27//! # Example
28//!
29//! ```no_run
30//! use dataset_ml::traits::MlDataset;
31//! use dataset_ml::{Iris, SmsSpam};
32//!
33//! // One function works for any loader, including the text corpora, whose data has an
34//! // entirely different shape from Iris's.
35//! fn describe<D: MlDataset>(dataset: &D) -> String {
36//!     format!("{} ({} samples)", D::NAME, dataset.n_samples().unwrap())
37//! }
38//!
39//! assert_eq!(describe(&Iris::new("./data")), "iris (150 samples)");
40//! assert_eq!(describe(&SmsSpam::new("./data")), "sms_spam (5574 samples)");
41//! ```
42
43use dataset_core::{Dataset, DatasetError};
44use ndarray::{Array, Axis, Dimension};
45
46/// A parsed dataset whose samples can be counted.
47///
48/// This crate implements it for the array pairs and triples every loader parses
49/// into. Examples are `(features, labels)`, `(features, targets)`,
50/// `(texts, labels)`, `(categorical, numeric, labels)`, and
51/// `(texts, sources, labels)`. In all of them, the first array's leading axis is
52/// the sample axis, so this counts that axis.
53///
54/// You only need this trait directly to call [`MlDataset::n_samples`] in a generic
55/// function. If you want the same from a loader of your own, implement it for your
56/// own data type.
57pub trait NumSamples {
58    /// The number of samples the parsed data holds.
59    fn num_samples(&self) -> usize;
60}
61
62impl<A, DA, B, DB> NumSamples for (Array<A, DA>, Array<B, DB>)
63where
64    DA: Dimension,
65    DB: Dimension,
66{
67    fn num_samples(&self) -> usize {
68        self.0.len_of(Axis(0))
69    }
70}
71
72impl<A, DA, B, DB, C, DC> NumSamples for (Array<A, DA>, Array<B, DB>, Array<C, DC>)
73where
74    DA: Dimension,
75    DB: Dimension,
76    DC: Dimension,
77{
78    fn num_samples(&self) -> usize {
79        self.0.len_of(Axis(0))
80    }
81}
82
83/// The lazy-loading behavior every dataset loader in this crate shares.
84///
85/// Implementors wrap a [`Dataset<Self::Data, DatasetError>`](dataset_core::Dataset)
86/// and only need to expose it through the three needed methods. The trait
87/// provides everything else.
88///
89/// # Implementing it for your own loader
90///
91/// ```rust
92/// use dataset_core::{Dataset, DatasetError};
93/// use dataset_ml::traits::MlDataset;
94/// use ndarray::{Array1, Array2};
95///
96/// type MyData = (Array2<f64>, Array1<u8>);
97///
98/// struct MyDataset {
99///     dataset: Dataset<MyData, DatasetError>,
100/// }
101///
102/// impl MlDataset for MyDataset {
103///     type Data = MyData;
104///     const NAME: &'static str = "my_dataset";
105///
106///     fn dataset(&self) -> &Dataset<Self::Data, DatasetError> {
107///         &self.dataset
108///     }
109///
110///     fn dataset_mut(&mut self) -> &mut Dataset<Self::Data, DatasetError> {
111///         &mut self.dataset
112///     }
113///
114///     fn into_dataset(self) -> Dataset<Self::Data, DatasetError> {
115///         self.dataset
116///     }
117/// }
118/// ```
119pub trait MlDataset: Sized {
120    /// What this loader parses into: the module's `…Data` type alias.
121    ///
122    /// It must implement [`NumSamples`], which the array pairs and triples every
123    /// loader here produces already satisfy. This trait needs that bound up
124    /// front, rather than placing it on [`n_samples`](Self::n_samples) itself.
125    /// That way, a generic `fn f<D: MlDataset>(d: &D)` can call `d.n_samples()`
126    /// without repeating the bound.
127    type Data: NumSamples;
128
129    /// The dataset's identifier, matching the one used in its error messages
130    /// (for example, `"iris"`, `"sms_spam"`).
131    const NAME: &'static str;
132
133    /// Borrow the underlying container.
134    fn dataset(&self) -> &Dataset<Self::Data, DatasetError>;
135
136    /// Borrow the underlying container mutably.
137    fn dataset_mut(&mut self) -> &mut Dataset<Self::Data, DatasetError>;
138
139    /// Consume the loader and return the underlying container.
140    fn into_dataset(self) -> Dataset<Self::Data, DatasetError>;
141
142    /// Load the dataset if needed and borrow the parsed data.
143    ///
144    /// The generic equivalent of each loader's inherent `data()`: it downloads and
145    /// parses on first call, then returns the cached value. Concurrent calls run
146    /// the loader once and share the result.
147    ///
148    /// # Errors
149    ///
150    /// Returns `DatasetError` if the download, file I/O, or parsing fails.
151    fn load(&self) -> Result<&Self::Data, DatasetError> {
152        self.dataset().load()
153    }
154
155    /// Load the dataset if needed and borrow the parsed data **mutably**.
156    ///
157    /// If you edit the data in place through the returned reference, the change
158    /// persists in the cache, so later accesses observe it. Unlike the inherent
159    /// `get_data_mut()`, this loads rather than returning `None` when nothing is
160    /// cached yet.
161    ///
162    /// # Errors
163    ///
164    /// Returns `DatasetError` if the download, file I/O, or parsing fails.
165    fn load_mut(&mut self) -> Result<&mut Self::Data, DatasetError> {
166        self.dataset_mut().load_mut()
167    }
168
169    /// Borrow the parsed data **without** triggering loading.
170    ///
171    /// The generic equivalent of each loader's inherent `get_data()`. Returns
172    /// `None`, rather than downloading, when the dataset is not loaded yet.
173    fn peek(&self) -> Option<&Self::Data> {
174        self.dataset().get()
175    }
176
177    /// Move the parsed data out, leaving the loader reusable and unloaded.
178    ///
179    /// The generic equivalent of each loader's inherent `take_data()`, except that
180    /// it never loads: it returns `None` if nothing is cached.
181    fn unload(&mut self) -> Option<Self::Data> {
182        self.dataset_mut().take()
183    }
184
185    /// Whether the cache currently holds the data.
186    ///
187    /// Never triggers loading, so this is the cheap way to ask whether an accessor
188    /// would return instantly or start a download.
189    fn is_loaded(&self) -> bool {
190        self.dataset().is_loaded()
191    }
192
193    /// The directory this loader stores its files in.
194    fn storage_dir(&self) -> &str {
195        self.dataset().storage_dir()
196    }
197
198    /// Drop the cached data, keeping the loader usable.
199    ///
200    /// The next access re-reads the file from `storage_dir`. This re-runs the
201    /// SHA-256 check and the parser, and re-downloads if the file is gone or no
202    /// longer matches. Use it to reclaim the memory a dataset occupies
203    /// (`covtype`, `kddcup99`), or to read a file that changed on disk.
204    ///
205    /// To retrieve the data rather than discard it, use [`unload`](Self::unload).
206    fn invalidate(&mut self) {
207        self.dataset_mut().invalidate();
208    }
209
210    /// The number of samples in the dataset, loading it if needed.
211    ///
212    /// Reads the leading axis of the data's first array: the row count for the
213    /// tabular loaders, the document count for the text ones.
214    ///
215    /// # Errors
216    ///
217    /// Returns `DatasetError` if the download, file I/O, or parsing fails.
218    fn n_samples(&self) -> Result<usize, DatasetError> {
219        Ok(self.load()?.num_samples())
220    }
221}
222
223/// Implement [`MlDataset`] for a loader that stores its container in a field named
224/// `dataset`.
225///
226/// Every loader in this crate has that exact shape, so the implementation is
227/// entirely mechanical. This macro writes the three needed methods and leaves
228/// the rest to the trait's defaults. It is crate-internal. Downstream loaders
229/// implement the trait directly (see [`MlDataset`]'s own example).
230macro_rules! impl_ml_dataset {
231    ($struct_name:ident, $data_type:ty, $name:literal) => {
232        impl $crate::traits::MlDataset for $struct_name {
233            type Data = $data_type;
234            const NAME: &'static str = $name;
235
236            fn dataset(
237                &self,
238            ) -> &::dataset_core::Dataset<Self::Data, ::dataset_core::DatasetError> {
239                &self.dataset
240            }
241
242            fn dataset_mut(
243                &mut self,
244            ) -> &mut ::dataset_core::Dataset<Self::Data, ::dataset_core::DatasetError> {
245                &mut self.dataset
246            }
247
248            fn into_dataset(
249                self,
250            ) -> ::dataset_core::Dataset<Self::Data, ::dataset_core::DatasetError> {
251                self.dataset
252            }
253        }
254    };
255}
256
257pub(crate) use impl_ml_dataset;