functora-tagged
Lightweight, macro-free newtypes with refinement and derived traits.
Motivation
Newtypes are a fundamental pattern in Rust for enhancing type safety and expressing semantic meaning. By wrapping an existing type (the representation) in a distinct new type, we prevent accidental misuse and clearly communicate intent. For example, distinguishing between a UserId and a ProductId, even if both are internally represented as u64, prevents bugs where one might be used in place of the other. This pattern makes code more self-documenting and less prone to logical errors.
While standard traits like Eq, PartialEq, Ord, PartialOrd, Clone, and Debug can often be derived automatically, many essential traits are not. These include parsing (FromStr), serialization (serde::Serialize/Deserialize), and database integration (diesel::Queryable, ToSql, FromSql, AsExpression). Implementing these traits for newtypes manually can lead to substantial boilerplate.
Rust has common macro-based solutions for deriving newtype traits. Macros are often employed as a last resort when language expressiveness is insufficient. However, they introduce significant drawbacks:
- Syntax: Macro-based code is not conventional Rust code. It acts as a complex "foreign" DSL that is hard to read, maintain, and extend.
- Boilerplate: Despite their intent, macros frequently require significant boilerplate, undermining their primary benefit.
- Complexity: Macros can obscure the underlying logic and make debugging difficult.
functora-tagged offers a superior, macro-free alternative. It provides a clean, idiomatic, and type-safe mechanism for creating newtypes. Through the Refine trait, you can define custom verification and transformation logic for your newtype. This logic is then automatically integrated into implementations for crucial, non-trivially derivable traits like FromStr, serde, and diesel, achieving zero boilerplate for these complex scenarios without the downsides of macros.
Tagged
The primary newtype building block is the Tagged<T, D, F> struct.
T: The underlying representation type (e.g.,String,i32).D: The "Dimension". A phantom type used at compile time to distinguish between different newtypes that share the sameTandF.F: The "Refinery". A phantom type that implements theRefine<T>trait to define refinement logic.
This separation of D and F allows you to have the same semantic type (e.g., Meter) with different refinements (e.g., NonNegative vs Positive) without changing the type's identity for dimension-checking purposes.
use PhantomData;
>);
The Tagged::new constructor returns a Result that you can unwrap directly if the Refine implementation for F returns Infallible. The InfallibleInto trait and its infallible() method provide a convenient way to handle this.
ViaString
The ViaString<T, D, F> struct is a specialized newtype for scenarios where the underlying representation (T) is closely tied to string manipulation or requires string-based serialization or deserialization. It differs from Tagged in its serialization behavior:
- Serialization:
ViaStringserializes to its string representation (via theToStringimplementation ofT), whereasTaggedserializes theTdirectly. - Deserialization:
ViaStringdeserializes from a string, then attempts to parse it intoTusingFromStr.TaggeddeserializesTdirectly.
It also implements FromStr and derives common traits, similar to Tagged, respecting the Refine trait.
Refinement
To enforce specific refinement rules for your newtypes, implement the Refine<T> trait for the F type (the Refinery). This trait allows you to define custom refinement logic.
Custom Refinery
Here is a complete example of defining a Dimension D, a Refinery F, and implementing Refine.
use *;
// 1. Define the Dimension (D)
// 2. Define the Refinery (F)
// 3. Define the Error Type
;
// 4. Implement Refine for the Refinery
// 5. Define the Newtype
pub type CurrencyCode = ;
// Usage
let usd = new;
assert!;
assert_eq!;
let eur = new; // Whitespace stripped, uppercased
assert!;
assert_eq!;
let err = new; // Too short
assert_eq!;
let err = new; // Not letters
assert_eq!;
Common Refineries
functora-tagged provides a set of common, ready-to-use refineries in the common module (src/common.rs). These allow you to quickly create refined newtypes with zero boilerplate.
FCrude: No-op refinery. Used when you only need a distinct type without refinement.RefineErrorisInfallible.FPositive: Ensures the value is strictly greater than zero (> 0).FNonNeg: Ensures the value is non-negative (>= 0).FNonEmpty: Ensures the value is not empty, i.e. the length is> 0.
Numeric Identities (zero() / one())
Common refineries provide associated functions to create refined values representing zero or one. These are available for any representation type T that implements the corresponding num_traits.
| Refinery | zero() |
one() |
Note |
|---|---|---|---|
FCrude |
✅ | ✅ | No restrictions |
FPositive |
❌ | ✅ | 0 is not positive |
FNonNeg |
✅ | ✅ | 0 and 1 are both non-neg |
use *;
pub type Weight = ;
// Create a positive weight of 1.0 safely
let w = one;
assert_eq!;
Non-Empty Collections (FNonEmpty)
When a Tagged type is refined with FNonEmpty, it provides a powerful set of methods that simplify your code by exploiting the non-emptiness guarantee.
1. Infallible Access
Standard library methods often return Option<T> for potentially empty collections. FNonEmpty methods return the values directly, eliminating the need for unwrap() or expect() at the call site.
| Tagged Method | Returns | Stdlib Analogue |
|---|---|---|
first() |
&T |
xs.first() -> Option<&T> |
last() |
&T |
xs.last() -> Option<&T> |
minimum() |
&T |
xs.iter().min() -> Option<&T> |
maximum() |
&T |
xs.iter().max() -> Option<&T> |
reduce(f) |
T |
xs.iter().reduce(f) -> Option<T> |
2. Invariant Preservation
These methods return a new Tagged<..., FNonEmpty> instance. Since they take ownership of self, they can leverage into_iter() to perform transformations without cloning individual elements. This makes them highly efficient for complex or large underlying data.
| Method | Behavior | Invariant |
|---|---|---|
map(f) |
Transform elements | Count remains same |
rev() |
Reverse elements | Count remains same |
sort() |
Sort elements | Count remains same |
dedup() |
Remove consecutive duplicates | At least one remains |
Example
use *;
use Error;
Derives
functora-tagged provides blanket implementations for several essential traits. These traits work seamlessly with your newtypes, respecting the behavior of the underlying representation and the refinement rules defined by the F type's implementation of Refine<T>.
Direct
EqPartialEqOrdPartialOrdCloneDebugserde::Serialize(withserdefeature)diesel::serialize::ToSql(withdieselfeature)diesel::expression::AsExpression(withdieselfeature)
Refined
FromStr: Implemented forTagged<T, D, F>andViaString<T, D, F>. Returns aParseError<T, D, F>, which can be an upstreamDecodeerror (fromT::from_str) or aRefineerror (fromF::refine).serde::Deserialize(withserdefeature)diesel::Queryable(withdieselfeature)diesel::deserialize::FromSql(withdieselfeature)
Integrations
functora-tagged provides optional integrations for common Rust ecosystems:
serde: For serialization and deserialization. Enable with theserdefeature.diesel: For database interactions. Enable with thedieselfeature.
These integrations respect the Refine rules defined for your types.
Recipes
You can promote rep values into newtypes using Tagged::new(rep). To demote a newtype back to its representation, use the .rep() method to get a reference, or .untag() to consume it. You can also use the Deref trait (via the * operator) to access the underlying value if it implements Copy. Standard serializers and deserializers available for Rep work directly with the newtype as well.
Simple Newtype
When you don't need refinement, use FCrude from the common module.
use *;
pub type UserId = ;
let id = new.infallible;
assert_eq!;
Refined Newtype
This example demonstrates ensuring numeric types are positive using FPositive.
use *;
// The Dimension
// Use common FPositive refinery
pub type PositiveCount = ;
let rep = 100;
let new = new.unwrap;
assert_eq!;
// FPositive returns PositiveError on failure
let err = new.unwrap_err;
assert_eq!;
Generic Newtype
This demonstrates a generic PositiveAmount<T> newtype that enforces positive values for any numeric type T that satisfies FPositive.
use *;
// Dimension
pub type PositiveAmount<T> = ;
// Works with i32
let rep = 100;
let new = new.unwrap;
assert_eq!;
// Works with f64
let rep = 10.5;
let new = new.unwrap;
assert_eq!;
// Refinement fails
let err = new.unwrap_err;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
Composite Newtype
This example demonstrates how to combine multiple refinement rules (e.g., NonNeg and Max 100) into a single, flat Tagged type using a composite refinery. This avoids the complexity of nesting Tagged types.
use *;
// Dimension
// Refinery
// Flat structure: one Tagged wrapper
pub type Score = ;
let val = 85;
let score = new.unwrap;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
Dimensional
functora-tagged includes a num module that enables type-safe dimensional analysis and arithmetic. It prevents accidental unit mixing (e.g., adding meters to seconds) and ensures that operations produce correctly typed results (e.g., dividing meters by seconds yields velocity).
The system is built on four core algebraic types that carry unit information in their PhantomData:
Identity<I, F>: Represents a neutral element or a base scalar (like a dimensionless number).Atomic<A, F>: Represents a fundamental unit (e.g., Meter, Second, Kg).Times<L, R, F>: Represents the product of two units (e.g., Meter * Meter = Area).Per<L, R, F>: Represents the quotient of two units (e.g., Meter / Second = Velocity).
All these types accept a refinement generic F (e.g., FPositive, FNonNeg etc.) to enforce constraints like non-negativity on the underlying values.
Physics
Large dimensional systems can become verbose if the refinery F is repeated for every type. To solve this, functora-tagged provides the Raffinate trait. Each dimensional type (Identity, Atomic, Times, Per) implements Raffinate, which carries its own refinery through a Refinery associated type.
This allows the dimensional types to automatically derive the correct refinery. By defining a simple Dim<D> type alias, you can eliminate the need to repeat refinery F in your definitions:
use *;
use Decimal;
type Dim<D> =
;
This example demonstrates how to define physical units and calculate Kinetic Energy (Ek = kg * (m/s)^2) safely and concisely.
use *;
use Decimal;
use dec;
//
// 1. Generic dimensional type alias
//
type Dim<D> =
;
//
// 2. Dimensionless unit (Identity)
//
type DNum = ;
type Num = ;
//
// 3. Fundamental units (Atomic)
//
type DMeter = ;
type DSecond = ;
type DKg = ;
type Meter = ;
type Second = ;
type Kg = ;
//
// 4. Composite units (Per, Times)
//
// Velocity = Meter / Second
type DVelocity = ;
type Velocity = ;
// Joule = Kg * Velocity^2
type DJoule = ;
type Joule = ;
//
// 5. Type-safe calculation
//
© 2025 Functora. All rights reserved.