to-values 0.1.0

Flatten values into ordered f32 feature vectors.
//! Ordered `f32` feature vectors for machine-learning inputs.
//!
//! [`ToInputVector`] turns a value into a flat, ordered sequence of `f32`
//! values. It is designed for feature extraction at the boundary between domain
//! types and neural networks, reinforcement-learning environments, or other
//! numeric models.
//!
//! The crate implements the trait for numeric primitives, [`bool`], slices,
//! arrays, [`Vec`], [`Box`], references, the unit type, and tuples of up to
//! twelve elements. Collections and tuples are flattened in iteration and
//! declaration order respectively.
//!
//! # Deriving implementations
//!
//! The default `derive` feature provides an implementation for ordinary
//! structs. Every non-skipped field must implement [`ToInputVector`].
//!
//! ```
//! # #[cfg(feature = "derive")]
//! # fn derive_example() {
//! use to_values::ToInputVector;
//!
//! #[derive(ToInputVector)]
//! struct Entity {
//!     position: [f32; 2],
//!     #[input(skip)]
//!     id: u64,
//!     money: f32,
//! }
//!
//! let entity = Entity {
//!     position: [1.5, -2.0],
//!     id: 42,
//!     money: 10.0,
//! };
//!
//! assert_eq!(entity.to_input_vector(), vec![1.5, -2.0, 10.0]);
//! # }
//! # #[cfg(feature = "derive")]
//! # derive_example();
//! ```
//!
//! Disable default features and enable only the `std` feature as needed. The
//! core API supports `no_std` environments with an allocator (`alloc`).
//!
//! Integer and [`f64`] values use Rust's `as f32` conversion. That may lose
//! precision, so model code should normalize or otherwise transform those
//! values in a domain-specific implementation where precision matters.

#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]

extern crate alloc;

use alloc::{boxed::Box, vec::Vec};

/// Implementation detail used by the optional derive macro.
///
/// This alias is public only because code generated by a procedural macro must
/// name the output type from the consuming crate. It is not part of the
/// supported public API.
#[doc(hidden)]
pub type __InputVector = Vec<f32>;

#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
#[doc(inline)]
/// Derives [`ToInputVector`] for structs whose non-skipped fields implement it.
///
/// Apply `#[input(skip)]` to a field that should not become a model input.
pub use to_values_derive::ToInputVector;

/// Converts a value into one or more ordered `f32` model inputs.
///
/// Implementations must append inputs in a stable order. The only required
/// method is [`Self::append_input_vector`]. Overriding
/// [`Self::input_vector_len`] gives [`Self::to_input_vector`] an exact capacity
/// hint and avoids reallocation for values with a cheap, known output length.
pub trait ToInputVector {
    /// Returns a capacity hint for values appended by [`Self::append_input_vector`].
    ///
    /// Implementations with a cheap, known output length should return that
    /// exact length. The default does not inspect the value and returns `0`; it
    /// is therefore always safe, but may let [`Self::to_input_vector`] grow its
    /// allocation.
    #[must_use]
    #[inline]
    fn input_vector_len(&self) -> usize {
        0
    }

    /// Appends this value's model inputs to `output`.
    fn append_input_vector(&self, output: &mut Vec<f32>);

    /// Converts this value to a newly allocated, flat input vector.
    #[must_use]
    #[inline]
    fn to_input_vector(&self) -> Vec<f32> {
        let mut output = Vec::with_capacity(self.input_vector_len());
        self.append_input_vector(&mut output);
        output
    }

    /// Alias for [`Self::to_input_vector`].
    ///
    /// New code may prefer [`Self::to_input_vector`], which is shorter and
    /// applies equally to primitive values and composite domain types.
    #[must_use]
    #[inline]
    fn members_as_f32_vector(&self) -> Vec<f32> {
        self.to_input_vector()
    }
}

macro_rules! impl_numeric_input {
    ($($type:ty),+ $(,)?) => {
        $(
            impl ToInputVector for $type {
                #[inline]
                fn input_vector_len(&self) -> usize {
                    1
                }

                #[inline]
                fn append_input_vector(&self, output: &mut Vec<f32>) {
                    output.push(*self as f32);
                }
            }
        )+
    };
}

impl_numeric_input!(
    f32, f64, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize,
);

impl ToInputVector for bool {
    #[inline]
    fn input_vector_len(&self) -> usize {
        1
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        output.push(f32::from(*self));
    }
}

impl ToInputVector for () {
    #[inline]
    fn input_vector_len(&self) -> usize {
        0
    }

    #[inline]
    fn append_input_vector(&self, _output: &mut Vec<f32>) {}
}

impl<T> ToInputVector for [T]
where
    T: ToInputVector,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        self.iter().fold(0, |length, item| {
            length.saturating_add(item.input_vector_len())
        })
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        for item in self {
            item.append_input_vector(output);
        }
    }
}

impl<T, const N: usize> ToInputVector for [T; N]
where
    T: ToInputVector,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        self.as_slice().input_vector_len()
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        self.as_slice().append_input_vector(output);
    }
}

impl<T> ToInputVector for Vec<T>
where
    T: ToInputVector,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        self.as_slice().input_vector_len()
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        self.as_slice().append_input_vector(output);
    }
}

impl<T> ToInputVector for Box<T>
where
    T: ToInputVector + ?Sized,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        (**self).input_vector_len()
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        (**self).append_input_vector(output);
    }
}

impl<T> ToInputVector for &T
where
    T: ToInputVector + ?Sized,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        (*self).input_vector_len()
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        (*self).append_input_vector(output);
    }
}

impl<T> ToInputVector for &mut T
where
    T: ToInputVector + ?Sized,
{
    #[inline]
    fn input_vector_len(&self) -> usize {
        (**self).input_vector_len()
    }

    #[inline]
    fn append_input_vector(&self, output: &mut Vec<f32>) {
        (**self).append_input_vector(output);
    }
}

macro_rules! impl_tuple_input {
    ($($type:ident : $index:tt),+ $(,)?) => {
        impl<$($type),+> ToInputVector for ($($type,)+)
        where
            $($type: ToInputVector),+
        {
            #[inline]
            fn input_vector_len(&self) -> usize {
                let mut length = 0usize;
                $(length = length.saturating_add(self.$index.input_vector_len());)+
                length
            }

            #[inline]
            fn append_input_vector(&self, output: &mut Vec<f32>) {
                $(self.$index.append_input_vector(output);)+
            }
        }
    };
}

impl_tuple_input!(A: 0);
impl_tuple_input!(A: 0, B: 1);
impl_tuple_input!(A: 0, B: 1, C: 2);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9);
impl_tuple_input!(A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10);
impl_tuple_input!(
    A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, H: 7, I: 8, J: 9, K: 10, L: 11
);

#[cfg(test)]
mod tests {
    use alloc::{boxed::Box, vec, vec::Vec};

    use super::ToInputVector;

    #[test]
    fn numeric_primitives_convert_to_one_value() {
        assert_eq!(42_u16.to_input_vector(), vec![42.0]);
        assert_eq!((-3_i8).to_input_vector(), vec![-3.0]);
        assert_eq!(1.25_f64.to_input_vector(), vec![1.25]);
    }

    #[test]
    fn booleans_are_zero_or_one() {
        assert_eq!(false.to_input_vector(), vec![0.0]);
        assert_eq!(true.to_input_vector(), vec![1.0]);
    }

    #[test]
    fn collections_flatten_in_order() {
        let values = vec![[1_u8, 2], [3, 4]];

        assert_eq!(values.input_vector_len(), 4);
        assert_eq!(values.to_input_vector(), vec![1.0, 2.0, 3.0, 4.0]);
    }

    #[test]
    fn indirection_and_empty_values_are_supported() {
        let mut value = Box::new(((), 3_u8));
        let reference = &mut value;

        assert_eq!(reference.input_vector_len(), 1);
        assert_eq!(
            <&mut Box<((), u8)> as ToInputVector>::to_input_vector(&reference),
            vec![3.0]
        );
    }

    #[test]
    fn tuples_flatten_in_order() {
        let value = (1_u8, [2_i16, 3_i16], true);

        assert_eq!(value.input_vector_len(), 4);
        assert_eq!(value.to_input_vector(), vec![1.0, 2.0, 3.0, 1.0]);
    }

    #[test]
    fn custom_types_can_compose_fields_and_skip_fields() {
        struct Entity {
            position: [f32; 2],
            id: u64,
            money: f32,
        }

        impl ToInputVector for Entity {
            fn input_vector_len(&self) -> usize {
                self.position.input_vector_len() + self.money.input_vector_len()
            }

            fn append_input_vector(&self, output: &mut Vec<f32>) {
                self.position.append_input_vector(output);
                self.money.append_input_vector(output);
            }
        }

        let entity = Entity {
            position: [4.0, -1.0],
            id: 99,
            money: 12.5,
        };

        assert_eq!(entity.id, 99);
        assert_eq!(entity.members_as_f32_vector(), vec![4.0, -1.0, 12.5]);
    }

    #[test]
    fn custom_types_can_omit_the_capacity_hint() {
        struct Dynamic;

        impl ToInputVector for Dynamic {
            fn append_input_vector(&self, output: &mut Vec<f32>) {
                output.extend([3.0, 5.0]);
            }
        }

        assert_eq!(Dynamic.input_vector_len(), 0);
        assert_eq!(Dynamic.to_input_vector(), vec![3.0, 5.0]);
    }
}