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 new, distinct 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 style 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 in Rust, 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 the newtype traits derivation problem. Macros are often employed as a last resort when language expressiveness is insufficient. However, they introduce significant drawbacks:
- Syntax: Macro-based code is not just 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 validation 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 true zero boilerplate for these complex scenarios without the downsides of macros.
Tagged Struct
The primary newtype building block is the Tagged<Rep, Tag> struct.
Rep: The underlying representation type (e.g.,String,i32).Tag: A phantom type used at compile time to distinguish between different newtypes that share the sameRep. TheTagtype itself implements theRefine<Rep>trait to define refinement logic.
This structure allows you to create distinct types that behave identically to their Rep type for many traits, unless explicitly customized.
use PhantomData;
;
Refine Trait
To enforce specific refinement rules for your newtypes, you implement the Refine<Rep> trait for the Tag type. This trait allows you to define custom logic for refining the newtype representation.
use *;
;
Derived Traits
functora-tagged provides blanket implementations for several important traits. These traits work seamlessly with your newtypes, respecting the underlying representation behavior and customizable refinement rules defined by the Tag type's implementation of Refine<Rep>.
Direct Derive:
- Eq
- PartialEq
- Ord
- PartialOrd
- Clone
- Debug
- serde::Serialize (with
serdefeature) - diesel::serialize::ToSql (with
dieselfeature) - diesel::expression::AsExpression (with
dieselfeature)
Refined Derive:
- FromStr
- serde::Deserialize (with
serdefeature) - diesel::Queryable (with
dieselfeature) - diesel::deserialize::FromSql (with
dieselfeature)
Examples
You can promote Rep values into newtype values using Tagged::new(rep) applied directly to a Rep value. To demote a newtype value back to a Rep value, you can use the .rep() method. You can also use any serializer or deserializer for the newtype that is available for Rep.
Default Newtype
When a Tag type has a default Refine implementation that doesn't add new constraints or transformations, Tagged can be used for simple type distinction.
use *;
pub type NonNeg = ;
let rep = 123;
let new = new.unwrap;
assert_eq!;
Refined Newtype
This example demonstrates a simple refinement for numeric types to ensure they are positive, using PositiveTag.
use *;
pub type Positive = ;
;
let rep = 100;
let new = new.unwrap;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
Generic Newtype
This demonstrates a generic Positive<Rep> newtype that enforces positive values for any numeric type Rep that implements Refine<PositiveTag>.
use *;
use Zero;
pub type Positive<Rep> = ;
;
let rep = 100;
let new = new.unwrap;
assert_eq!;
let rep = 10.5;
let new = new.unwrap;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
let err = new.unwrap_err;
assert_eq!;
Nested Newtype
This example demonstrates nesting newtypes: UserId<Rep> generic newtype is built on top of the other NonEmpty<Rep> generic newtype and adds its own refinement logic.
use *;
pub type NonEmpty<Rep> = ;
;
pub type UserId<Rep> =
;
;
let rep = "user_123";
let new = rep..unwrap;
assert_eq!;
let err = ""..unwrap_err;
assert_eq!;
let err = "post_123"
.
.unwrap_err;
assert_eq!;
let rep: isize = 123;
let new = rep
.to_string
.
.unwrap;
assert_eq!;
let err = "0"..unwrap_err;
assert_eq!;
let err = "-1"..unwrap_err;
assert_eq!;
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.
© 2025 Functora. All rights reserved.