functora-tagged 0.2.5

Lightweight, macro-free newtypes with refinement and derived traits.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# 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 same `T` and `F`.
- `F`: The "Refinery". A phantom type that implements the `Refine<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.

```rust
use std::marker::PhantomData;

pub struct Tagged<T, D, F>(T, PhantomData<(D, F)>);
```

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**: `ViaString` serializes to its string representation (via the `ToString` implementation of `T`), whereas `Tagged` serializes the `T` directly.
- **Deserialization**: `ViaString` deserializes from a string, then attempts to parse it into `T` using `FromStr`. `Tagged` deserializes `T` directly.

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`.

```rust
use functora_tagged::*;

// 1. Define the Dimension (D)
#[derive(Debug)]
pub enum DCurrencyCode {}

// 2. Define the Refinery (F)
#[derive(Debug)]
pub enum FCurrencyCode {}

// 3. Define the Error Type
#[derive(Debug, PartialEq)]
pub struct CurrencyCodeError;

// 4. Implement Refine for the Refinery
impl Refine<String> for FCurrencyCode {
    type RefineError = CurrencyCodeError;

    fn refine(rep: String) -> Result<String, Self::RefineError> {
        let trimmed = rep.trim();
        if trimmed.len() == 3 && trimmed.chars().all(|c| c.is_ascii_alphabetic()) {
             Ok(trimmed.to_uppercase())
        } else {
            Err(CurrencyCodeError)
        }
    }
}

// 5. Define the Newtype
pub type CurrencyCode = Tagged<String, DCurrencyCode, FCurrencyCode>;

// Usage
let usd = CurrencyCode::new("USD".to_string());
assert!(usd.is_ok());
assert_eq!(*usd.unwrap(), "USD");

let eur = CurrencyCode::new("  eur  ".to_string()); // Whitespace stripped, uppercased
assert!(eur.is_ok());
assert_eq!(*eur.unwrap(), "EUR");

let err = CurrencyCode::new("us".to_string()); // Too short
assert_eq!(err.unwrap_err(), CurrencyCodeError);

let err = CurrencyCode::new("123".to_string()); // Not letters
assert_eq!(err.unwrap_err(), CurrencyCodeError);
```

## 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. `RefineError` is `Infallible`.
- **`FPositive`**: Ensures the value is strictly greater than zero (`> 0`).
- **`FNonNeg`**: Ensures the value is non-negative (`>= 0`).
- **`FZeroExclToOneExcl`**: Ensures the value is in the open interval `(0, 1)` — both endpoints excluded.
- **`FZeroInclToOneExcl`**: Ensures the value is in the half-open interval `[0, 1)` — zero included, one excluded.
- **`FZeroExclToOneIncl`**: Ensures the value is in the half-open interval `(0, 1]` — zero excluded, one included.
- **`FZeroInclToOneIncl`**: Ensures the value is in the closed interval `[0, 1]` — both endpoints included.
- **`FNonEmpty`**: Ensures the value is not empty, i.e. the length is `> 0`.

```rust
use functora_tagged::*;

#[derive(Debug)]
pub enum DProb {}
pub type Probability = Tagged<f64, DProb, FZeroInclToOneIncl>;

let p = Probability::new(0.0);
assert!(p.is_ok());

let p = Probability::new(1.0);
assert!(p.is_ok());

let err = Probability::new(-0.1);
assert!(err.is_err());

let err = Probability::new(1.1);
assert!(err.is_err());
```

### 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       |
| `FZeroExclToOneExcl` ||| Neither `0` nor `1` are in `(0,1)` |
| `FZeroInclToOneExcl` ||| `0``[0,1)`, `1``[0,1)`       |
| `FZeroExclToOneIncl` ||| `0``(0,1]`, `1``(0,1]`       |
| `FZeroInclToOneIncl` ||| Both `0` and `1` are in `[0,1]`    |

```rust
use functora_tagged::*;

pub enum DWeight {}
pub type Weight = Tagged<f64, DWeight, FPositive>;

// Create a positive weight of 1.0 safely
let w = Weight::one();
assert_eq!(*w, 1.0);
```

### 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>` value. 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

```rust
use functora_tagged::*;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
  pub enum DNames {}
  pub type Names = Tagged<Vec<String>, DNames, FNonEmpty>;

  let names = Names::new(vec!["Alice".to_string(), "Bob".to_string()])?;

  // 1. Invariant Preservation: map returns a refined collection
  // It consumes 'names' to avoid cloning the Strings
  let upper = names.map::<_, _, Vec<_>>(|n| n.to_uppercase());

  // 2. fluent chaining works efficiently
  let sorted_upper: Names = upper.sort();

  // 3. Infallible access: no Options, no unwraps!
  assert_eq!(sorted_upper.first(), "ALICE");
  assert_eq!(sorted_upper.last(), "BOB");

  Ok(())
}
```

## 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

- `Eq`
- `PartialEq`
- `Ord`
- `PartialOrd`
- `Clone`
- `Debug`
- `serde::Serialize` (with `serde` feature)
- `diesel::serialize::ToSql` (with `diesel` feature)
- `diesel::expression::AsExpression` (with `diesel` feature)

### Refined

- `FromStr`: Implemented for `Tagged<T, D, F>` and `ViaString<T, D, F>`. Returns a `ParseError<T, D, F>`, which can be an upstream `Decode` error (from `T::from_str`) or a `Refine` error (from `F::refine`).
- `serde::Deserialize` (with `serde` feature)
- `diesel::Queryable` (with `diesel` feature)
- `diesel::deserialize::FromSql` (with `diesel` feature)

## Integrations

`functora-tagged` provides optional integrations for common Rust ecosystems:

- **`serde`**: For serialization and deserialization. Enable with the `serde` feature.
- **`diesel`**: For database interactions. Enable with the `diesel` feature.

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.

```rust
use functora_tagged::*;

pub enum DUserId {}

pub type UserId = Tagged<u64, DUserId, FCrude>;

let id = UserId::new(12345).infallible();
assert_eq!(*id, 12345);
```

### Refined Newtype

This example demonstrates ensuring numeric types are positive using `FPositive`.

```rust
use functora_tagged::*;

// The Dimension
#[derive(PartialEq, Debug)]
pub enum DCount {}

// Use common FPositive refinery
pub type PositiveCount = Tagged<usize, DCount, FPositive>;

let rep = 100;
let new = PositiveCount::new(rep).unwrap();
assert_eq!(*new, rep);

// FPositive returns PositiveError on failure
let err = PositiveCount::new(0).unwrap_err();
assert_eq!(err, PositiveError(0));
```

### Generic Newtype

This demonstrates a generic `PositiveAmount<T>` newtype that enforces positive values for any numeric type `T` that satisfies `FPositive`.

```rust
use functora_tagged::*;

#[derive(Debug)]
pub enum DAmount {} // Dimension

pub type PositiveAmount<T> = Tagged<T, DAmount, FPositive>;

// Works with i32
let rep = 100;
let new = PositiveAmount::<i32>::new(rep).unwrap();
assert_eq!(*new, rep);

// Works with f64
let rep = 10.5;
let new = PositiveAmount::<f64>::new(rep).unwrap();
assert_eq!(*new, rep);

// Refinement fails
let err = PositiveAmount::<i32>::new(-5).unwrap_err();
assert_eq!(err, PositiveError(-5));

let err = PositiveAmount::<f64>::new(0.0).unwrap_err();
assert_eq!(err, PositiveError(0.0));
```

### 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.

```rust
use functora_tagged::*;

#[derive(Debug)]
pub enum DScore {} // Dimension
#[derive(Debug)]
pub enum FScore {} // Refinery

// Flat structure: one Tagged wrapper
pub type Score = Tagged<i32, DScore, FScore>;

#[derive(Debug, PartialEq)]
pub enum ScoreError {
    NonNeg(NonNegError<i32>),
    TooHigh(i32),
}

impl Refine<i32> for FScore {
    type RefineError = ScoreError;

    fn refine(rep: i32) -> Result<i32, Self::RefineError> {
        // Reuse FNonNeg logic first (DRY)
        let val = FNonNeg::refine(rep).map_err(ScoreError::NonNeg)?;

        // Then apply custom max check
        if val <= 100 {
            Ok(val)
        } else {
            Err(ScoreError::TooHigh(val))
        }
    }
}

let val = 85;
let score = Score::new(val).unwrap();
assert_eq!(*score, val);

let err = Score::new(-10).unwrap_err();
assert_eq!(err, ScoreError::NonNeg(NonNegError(-10)));

let err = Score::new(101).unwrap_err();
assert_eq!(err, ScoreError::TooHigh(101));
```

## 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:

```rust
use functora_tagged::*;
use rust_decimal::Decimal;

type Dim<D> =
    Tagged<Decimal, D, <D as Raffinate>::Refinery>;
```

This example demonstrates how to define physical units and calculate Kinetic Energy (**Ek = kg \* (m/s)^2**) safely and concisely.

```rust
use functora_tagged::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;

//
// 1. Generic dimensional type alias
//

type Dim<D> =
    Tagged<Decimal, D, <D as Raffinate>::Refinery>;

//
// 2. Dimensionless unit (Identity)
//

#[derive(Debug)]
pub enum INum {}
type DNum = Identity<INum, FCrude>;
type Num = Dim<DNum>;

//
// 3. Fundamental units (Atomic)
//

#[derive(Debug)]
pub enum AMeter {}
#[derive(Debug)]
pub enum ASecond {}
#[derive(Debug)]
pub enum AKg {}

type DMeter = Atomic<AMeter, FNonNeg>;
type DSecond = Atomic<ASecond, FNonNeg>;
type DKg = Atomic<AKg, FNonNeg>;

type Meter = Dim<DMeter>;
type Second = Dim<DSecond>;
type Kg = Dim<DKg>;

//
// 4. Composite units (Per, Times)
//

// Velocity = Meter / Second
type DVelocity = Per<DMeter, DSecond, FNonNeg>;
type Velocity = Dim<DVelocity>;

// Joule = Kg * Velocity^2
type DJoule = Times<
    DKg,
    Times<DVelocity, DVelocity, FNonNeg>,
    FNonNeg,
>;
type Joule = Dim<DJoule>;

//
// 5. Type-safe calculation
//

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let distance = Meter::new(dec!(50))?; // 50 meters
    let time = Second::new(dec!(5))?; // 5 seconds
    let mass = Kg::new(dec!(100))?; // 100 kg

    // Calculate velocity: 50m / 5s = 10 m/s
    let velocity: Velocity = distance.tdiv(&time)?;
    assert_eq!(*velocity, dec!(10));

    // Calculate Energy: 100kg * (10m/s)^2 = 10000J
    let energy: Joule =
        mass.tmul(&velocity.tmul(&velocity)?)?;
    assert_eq!(*energy, dec!(10000));

    // Scaling by a dimensionless 0.5 doesn't change the units
    let half = Num::new(dec!(0.5))?;
    let half_energy: Joule = energy.tmul(&half)?;
    assert_eq!(*half_energy, dec!(5000));

    Ok(())
}
```

<hr>

© 2025 [Functora](https://functora.github.io/). All rights reserved.