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;
;
The Tagged::new constructor returns a Result that can be unwrapped directly if the Tag's Refine implementation returns std::convert::Infallible. The InfallibleInto trait and its infallible() method provide a convenient way to handle this.
ViaString Struct
The ViaString<Rep, Tag> struct is a specialized newtype primarily intended for scenarios where the underlying representation (Rep) is closely tied to string manipulation or needs to be serialized/deserialized as a string. It differs from Tagged in its serialization and deserialization behavior:
- Serialization:
ViaStringserializes to its string representation (viaToStringonRep), whereasTaggedserializes theRepdirectly. - Deserialization:
ViaStringdeserializes from a string, then attempts to parse it intoRepusingFromStr.TaggeddeserializesRepdirectly.
It also implements FromStr and derives common traits, similar to Tagged, respecting the Refine trait for validation.
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 *;
;
Note that the Refine trait has a default implementation that simply returns the input Rep without modification. This allows you to create simple newtypes for type distinction without needing to implement the refine function, as demonstrated in the NonNegTag example.
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: Implemented for
Tagged<Rep, Tag>andViaString<Rep, Tag>. Returns aParseError<Rep, Tag>, which can be either aDecodeerror (fromRep::from_str) or aRefineerror (fromTag::refine). For nested types, these errors can be further nested. - serde::Deserialize (with
serdefeature) - diesel::Queryable (with
dieselfeature) - diesel::deserialize::FromSql (with
dieselfeature)
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.
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 *;
use Infallible;
pub type NonNeg = ;
let rep = 123;
let new = new.infallible;
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!;
© 2025 Functora. All rights reserved.