dataset_ml/traits.rs
1//! The [`MlDataset`] trait shared by every loader in this crate.
2//!
3//! Every loader parses into a [`Table`], so the trait needs no type parameter and
4//! no per-loader accessor. It covers the container operations that stay the same
5//! whatever the table holds:
6//!
7//! - [`invalidate`](MlDataset::invalidate) drops the in-memory cache and forces the
8//! next access to re-read (and re-verify) the file on disk.
9//! - [`is_loaded`](MlDataset::is_loaded) and [`storage_dir`](MlDataset::storage_dir)
10//! let you inspect a loader without touching the data.
11//! - [`n_samples`](MlDataset::n_samples) gives the sample count.
12//!
13//! This trait names its data accessors [`load`](MlDataset::load),
14//! [`peek`](MlDataset::peek), and [`unload`](MlDataset::unload) rather than reusing
15//! `data`, `get_data`, and `take_data`. This way, a trait method never silently
16//! shadows the inherent method of the same name, and the inherent method never
17//! shadows the trait method either. Both sets are always available and always
18//! agree. Use whichever reads better where you are.
19//!
20//! This module is always available. The `dataset` feature only decides whether the
21//! built-in loaders that implement the trait compile with it.
22//!
23//! # Example
24//!
25//! This example needs the `dataset` feature, because it uses two built-in loaders.
26//!
27//! ```no_run
28//! use dataset_ml::traits::MlDataset;
29//! use dataset_ml::{Iris, SmsSpam};
30//!
31//! // One function works for any loader, including the text corpora, whose table
32//! // holds entirely different columns from Iris's.
33//! fn describe<D: MlDataset>(dataset: &D) -> String {
34//! format!("{} ({} samples)", D::NAME, dataset.n_samples().unwrap())
35//! }
36//!
37//! assert_eq!(describe(&Iris::new("./data")), "iris (150 samples)");
38//! assert_eq!(describe(&SmsSpam::new("./data")), "sms_spam (5574 samples)");
39//! ```
40
41use crate::table::Table;
42use dataset_core::{Dataset, DatasetError};
43
44/// The lazy-loading behavior every dataset loader in this crate shares.
45///
46/// Implementors wrap a [`Dataset<Table, DatasetError>`](dataset_core::Dataset) and
47/// only need to expose it through the three needed methods. The trait provides
48/// everything else.
49///
50/// # Implementing it for your own loader
51///
52/// ```rust
53/// use dataset_core::{Dataset, DatasetError};
54/// use dataset_ml::table::Table;
55/// use dataset_ml::traits::MlDataset;
56///
57/// struct MyDataset {
58/// dataset: Dataset<Table, DatasetError>,
59/// }
60///
61/// impl MlDataset for MyDataset {
62/// const NAME: &'static str = "my_dataset";
63///
64/// fn dataset(&self) -> &Dataset<Table, DatasetError> {
65/// &self.dataset
66/// }
67///
68/// fn dataset_mut(&mut self) -> &mut Dataset<Table, DatasetError> {
69/// &mut self.dataset
70/// }
71///
72/// fn into_dataset(self) -> Dataset<Table, DatasetError> {
73/// self.dataset
74/// }
75/// }
76/// ```
77pub trait MlDataset: Sized {
78 /// The dataset's identifier, matching the one used in its error messages
79 /// (for example, `"iris"`, `"sms_spam"`).
80 const NAME: &'static str;
81
82 /// Borrow the underlying container.
83 fn dataset(&self) -> &Dataset<Table, DatasetError>;
84
85 /// Borrow the underlying container mutably.
86 fn dataset_mut(&mut self) -> &mut Dataset<Table, DatasetError>;
87
88 /// Consume the loader and return the underlying container.
89 fn into_dataset(self) -> Dataset<Table, DatasetError>;
90
91 /// Load the dataset if needed and borrow the table.
92 ///
93 /// The generic equivalent of each loader's inherent `data()`: it downloads and
94 /// parses on first call, then returns the cached value. Concurrent calls run
95 /// the loader once and share the result.
96 ///
97 /// # Errors
98 ///
99 /// Returns `DatasetError` if the download, file I/O, or parsing fails.
100 fn load(&self) -> Result<&Table, DatasetError> {
101 self.dataset().load()
102 }
103
104 /// Load the dataset if needed and borrow the table **mutably**.
105 ///
106 /// If you edit the table in place through the returned reference, the change
107 /// persists in the cache, so later accesses observe it. Unlike the inherent
108 /// `get_data_mut()`, this loads rather than returning `None` when nothing is
109 /// cached yet.
110 ///
111 /// # Errors
112 ///
113 /// Returns `DatasetError` if the download, file I/O, or parsing fails.
114 fn load_mut(&mut self) -> Result<&mut Table, DatasetError> {
115 self.dataset_mut().load_mut()
116 }
117
118 /// Borrow the table **without** triggering loading.
119 ///
120 /// The generic equivalent of each loader's inherent `get_data()`. Returns
121 /// `None`, rather than downloading, when the dataset has not loaded yet.
122 fn peek(&self) -> Option<&Table> {
123 self.dataset().get()
124 }
125
126 /// Move the table out, leaving the loader reusable and unloaded.
127 ///
128 /// The generic equivalent of each loader's inherent `take_data()`, except that
129 /// it never loads: it returns `None` if nothing is cached.
130 fn unload(&mut self) -> Option<Table> {
131 self.dataset_mut().take()
132 }
133
134 /// Whether the cache currently holds the table.
135 ///
136 /// Never triggers loading, so this is the cheap way to ask whether an accessor
137 /// already has the value or must start a download.
138 fn is_loaded(&self) -> bool {
139 self.dataset().is_loaded()
140 }
141
142 /// The directory this loader stores its files in.
143 fn storage_dir(&self) -> &str {
144 self.dataset().storage_dir()
145 }
146
147 /// Drop the cached table, keeping the loader usable.
148 ///
149 /// The next access re-reads the file from `storage_dir`. This re-runs the
150 /// SHA-256 check and the parser, and re-downloads if the file is gone or no
151 /// longer matches. Use it to reclaim the memory a dataset occupies
152 /// (`covtype`, `kddcup99`), or to read a file that changed on disk.
153 ///
154 /// To retrieve the table rather than discard it, use [`unload`](Self::unload).
155 fn invalidate(&mut self) {
156 self.dataset_mut().invalidate();
157 }
158
159 /// The number of samples in the dataset, loading it if needed.
160 ///
161 /// Every column of the table holds this many values.
162 ///
163 /// # Errors
164 ///
165 /// Returns `DatasetError` if the download, file I/O, or parsing fails.
166 fn n_samples(&self) -> Result<usize, DatasetError> {
167 Ok(self.load()?.n_samples())
168 }
169}
170
171/// Implement [`MlDataset`] for a loader that stores its container in a field named
172/// `dataset`.
173///
174/// Every loader in this crate has that exact shape, so the implementation is
175/// entirely mechanical. This macro writes the three needed methods and leaves the
176/// rest to the trait's defaults. It is crate-internal. Downstream loaders
177/// implement the trait directly (see [`MlDataset`]'s own example).
178#[cfg(feature = "dataset")]
179macro_rules! impl_ml_dataset {
180 ($struct_name:ident, $name:literal) => {
181 impl $crate::traits::MlDataset for $struct_name {
182 const NAME: &'static str = $name;
183
184 fn dataset(
185 &self,
186 ) -> &::dataset_core::Dataset<$crate::table::Table, ::dataset_core::DatasetError> {
187 &self.dataset
188 }
189
190 fn dataset_mut(
191 &mut self,
192 ) -> &mut ::dataset_core::Dataset<$crate::table::Table, ::dataset_core::DatasetError>
193 {
194 &mut self.dataset
195 }
196
197 fn into_dataset(
198 self,
199 ) -> ::dataset_core::Dataset<$crate::table::Table, ::dataset_core::DatasetError> {
200 self.dataset
201 }
202 }
203 };
204}
205
206#[cfg(feature = "dataset")]
207pub(crate) use impl_ml_dataset;