| |

Rust 20 🦀 Tuple Structs and Unit-Like Structs

Rust has three kinds of structs, and two of them are frequently overlooked. The named-field struct is the common case: fields with names, accessed by name, used for domain modeling. The tuple struct and the unit-like struct are the specialized forms. A tuple struct has fields without names, accessed by position, and it is the tool for the newtype pattern: a wrapper around an inner type that creates a distinct type checked by the compiler. A unit-like struct has no fields at all, and it is the tool for marker types and trait implementations that carry no data. Both are small, and both are essential in idiomatic Rust.

The newtype pattern is the most common use of the tuple struct. Wrapping a f64 in a Meters struct creates a type that is distinct from every other f64. The compiler rejects passing a Feet where a Meters is expected, even though both wrap the same inner type. This costs nothing at runtime: the wrapper is the same size as the inner value, and the access is direct. The pattern is used throughout Rust for units of measure, identifiers, validated strings, and any case where the type itself is the documentation.

The unit-like struct is the companion. It has no fields and no data, but it is a real type with a real value. It is used for marker types (a type whose only purpose is to exist), for trait implementations that do not need state, and for representing a value that has no information beyond its type.

This chapter covers the tuple struct definition and instantiation, the field access, the newtype pattern, the unit-like struct, its uses, the methods that operate on both, and the patterns that make them idiomatic.

Key point: A tuple struct has fields without names, accessed by position (.0, .1). It is the tool for the newtype pattern: wrapping an inner type to create a distinct type with no runtime cost. A unit-like struct has no fields and is used for marker types and stateless trait implementations. Both are real types and can have methods in an impl block.


Why tuple structs and unit-like structs exist

The type-safety problem. A function that takes a f64 accepts any f64, whether it represents meters, feet, seconds, or dollars. A newtype wraps the f64 in a distinct type, and the compiler rejects passing the wrong unit. The type itself documents the meaning and enforces it.

The field-naming problem. Sometimes the fields have no natural names. A Color(i32, i32, i32) has three components whose meaning is obvious from the type and the order. Adding names like red, green, blue is repetitive. The tuple struct omits the names and keeps the type safety.

The trait-impl problem. A trait sometimes needs a type that implements it, and the type carries no data. A unit-like struct is that type. It is zero-sized, and it can implement any trait whose methods do not need state.

The marker problem. Some generic code uses a type parameter only to distinguish two versions of a generic type. The type parameter has no data; it is a marker. A unit-like struct is the marker type.

The wrapper problem. A type that wraps another type to add behavior, to restrict the interface, or to implement a foreign trait needs a struct that holds the inner value. The tuple struct is the wrapper. The field is accessed by position because the wrapper’s purpose is the type, not the field names.


a. The tuple struct definition

A tuple struct is declared with the struct keyword, a name, and a list of field types in parentheses, terminated by a semicolon.

struct Color(i32, i32, i32);
struct Point(f64, f64);
struct Meters(f64);

The definition has no field names, only types. The fields are positional: .0, .1, .2.

The struct name is the type. A value of type Color is created with the struct name followed by the field values in parentheses:

let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
let distance = Meters(3.5);

The order of the values must match the order of the types in the definition. The compiler checks the types.

The tuple struct is distinct from the tuple type. A Color(0, 0, 0) is a Color, not a (i32, i32, i32). The compiler treats them as different types, even though they have the same layout.

let c = Color(0, 0, 0);
let t = (0, 0, 0);
// let c: Color = t;  // Error: expected Color, found (i32, i32, i32)

The distinction is the point: the struct name gives the tuple a type identity.


b. Field access

The fields of a tuple struct are accessed by position with the dot syntax.

let black = Color(0, 0, 0);
println!("{}", black.0);  // 0
println!("{}", black.1);  // 0
println!("{}", black.2);  // 0

The index is a literal integer, not a variable. black.0 is valid; black.i where i is a variable is not. The reason is that each field has a distinct type, and the compiler needs to know the type at compile time.

A single-field tuple struct is accessed as .0:

let m = Meters(3.5);
println!("{}", m.0);  // 3.5

The .0 is the only field. There is no .1 for a single-field struct.

A field can be modified if the binding is mutable:

let mut p = Point(1.0, 2.0);
p.0 = 3.0;
p.1 = 4.0;
println!("{} {}", p.0, p.1);  // 3, 4

The mutability is on the binding, not the field, as with named-field structs.


c. The newtype pattern

The newtype pattern wraps an inner type in a single-field tuple struct. The wrapper is a distinct type, and the compiler enforces the distinction.

struct Meters(f64);
struct Feet(f64);

fn distance_in_meters(m: Meters) {
    println!("{} meters", m.0);
}

fn main() {
    distance_in_meters(Meters(3.0));  // OK
    // distance_in_meters(Feet(3.0));  // Error: expected Meters, found Feet
    // distance_in_meters(3.0);        // Error: expected Meters, found f64
}

The function accepts only Meters. Passing a Feet or a bare f64 is a compile error. The type is the check.

The newtype is zero-cost. The wrapper has the same size and layout as the inner type, and the field access compiles to a direct read.

println!("{}", std::mem::size_of::<Meters>());  // 8
println!("{}", std::mem::size_of::<f64>());     // 8

The wrapper can add methods and trait implementations that the inner type does not have:

struct Meters(f64);

impl Meters {
    fn to_feet(&self) -> f64 {
        self.0 * 3.28084
    }
}

let m = Meters(1.0);
println!("{}", m.to_feet());  // 3.28084

The method is on Meters, not on f64. It does not pollute the f64 namespace, and it can be called only on a Meters value.

The newtype is also the way to implement a foreign trait for a foreign type. The orphan rule prevents implementing a foreign trait for a foreign type directly, but the newtype wrapper is a local type, so the implementation is allowed.

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

The Display trait is foreign, and Vec<String> is foreign, but Wrapper is local, so the implementation is permitted.


d. The unit-like struct

A unit-like struct has no fields. It is declared with the struct keyword, the name, and a semicolon.

struct AlwaysEqual;
struct Marker;

The unit-like struct is a type with a single value, written with the same name:

let x = AlwaysEqual;
let m = Marker;

The value is zero-sized. std::mem::size_of::<Marker>() is 0. The compiler optimizes it away entirely; the type exists only in the type system.

A unit-like struct can have methods:

struct Logger;

impl Logger {
    fn log(&self, message: &str) {
        println!("[LOG] {}", message);
    }
}

let logger = Logger;
logger.log("hello");

The method takes &self, and the value is zero-sized. The method is called like any other method.

The unit-like struct is used when the type is the information and no data needs to be stored.


e. Uses of the unit-like struct

The unit-like struct has several distinct uses.

Marker types. A type that exists only to distinguish two generic instantiations.

struct Celsius;
struct Fahrenheit;

struct Temperature<Unit> {
    value: f64,
    _unit: std::marker::PhantomData<Unit>,
}

The Celsius and Fahrenheit are marker types. They carry no data, and they are used as type parameters to distinguish the two kinds of temperature.

Stateless trait implementations. A trait that has methods but no state, implemented by a unit-like struct.

trait Handler {
    fn handle(&self, request: &str);
}

struct LogHandler;

impl Handler for LogHandler {
    fn handle(&self, request: &str) {
        println!("Logging: {}", request);
    }
}

The LogHandler has no data, but it implements the Handler trait. It can be passed as a type parameter or used as a strategy.

Placeholder values. A unit-like struct can represent the only value of a type when the type has no meaningful data.

enum Status {
    Success,
    Failure(Marker),
}

The Marker variant carries no data, but it is distinct from Success.

The () unit type. The unit type () is a tuple with no elements. It is the type of expressions that produce no value. A unit-like struct is the user-defined equivalent: a type with no fields, but with a name.


f. Methods on tuple and unit-like structs

Both kinds of struct can have methods in an impl block.

struct Meters(f64);

impl Meters {
    fn new(value: f64) -> Meters {
        Meters(value)
    }

    fn value(&self) -> f64 {
        self.0
    }

    fn add(&self, other: Meters) -> Meters {
        Meters(self.0 + other.0)
    }
}

The new associated function creates a Meters from a f64. The value method returns the inner value. The add method takes another Meters and returns a new one.

let a = Meters::new(3.0);
let b = Meters::new(4.0);
let c = a.add(b);
println!("{}", c.value());  // 7.0

The methods are defined on the wrapper, not the inner type. The call site reads as operations on Meters, which is the intent.

struct Counter;

impl Counter {
    fn count(&self) -> usize {
        42
    }
}

let c = Counter;
println!("{}", c.count());  // 42

The Counter unit-like struct has a method. The method does not use self, but it is called as c.count(). The value is zero-sized, so the method call compiles to a function call with no data.


Complete Example Session

// ============================================
// PART 1: DEFINE A TUPLE STRUCT
// ============================================
struct Color(i32, i32, i32);
struct Point(f64, f64);
// ============================================
// PART 2: INSTANTIATE AND ACCESS
// ============================================
fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0);

    println!("{} {} {}", black.0, black.1, black.2);
    println!("{} {}", origin.0, origin.1);
}
// ============================================
// PART 3: MUTABLE TUPLE STRUCT
// ============================================
fn main() {
    let mut p = Point(1.0, 2.0);
    p.0 = 3.0;
    p.1 = 4.0;
    println!("{} {}", p.0, p.1);
}
// ============================================
// PART 4: NEWTYPE PATTERN
// ============================================
struct Meters(f64);
struct Feet(f64);

fn distance_in_meters(m: Meters) {
    println!("{} meters", m.0);
}

fn main() {
    distance_in_meters(Meters(3.0));
    // distance_in_meters(Feet(3.0));  // Error
}
// ============================================
// PART 5: ZERO COST
// ============================================
struct Meters(f64);

fn main() {
    println!("{}", std::mem::size_of::<Meters>());  // 8
    println!("{}", std::mem::size_of::<f64>());     // 8
}
// ============================================
// PART 6: METHODS ON A NEWTYPE
// ============================================
struct Meters(f64);

impl Meters {
    fn to_feet(&self) -> f64 {
        self.0 * 3.28084
    }
}

fn main() {
    let m = Meters(1.0);
    println!("{}", m.to_feet());
}
// ============================================
// PART 7: NEWTYPE FOR FOREIGN TRAIT
// ============================================
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec!["a".to_string(), "b".to_string()]);
    println!("{}", w);
}
// ============================================
// PART 8: UNIT-LIKE STRUCT
// ============================================
struct Marker;

fn main() {
    let m = Marker;
    println!("{}", std::mem::size_of::<Marker>());  // 0
}
// ============================================
// PART 9: METHODS ON A UNIT-LIKE STRUCT
// ============================================
struct Logger;

impl Logger {
    fn log(&self, message: &str) {
        println!("[LOG] {}", message);
    }
}

fn main() {
    let logger = Logger;
    logger.log("hello");
}
// ============================================
// PART 10: MARKER TYPE IN GENERICS
// ============================================
use std::marker::PhantomData;

struct Celsius;
struct Fahrenheit;

struct Temperature<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

fn main() {
    let c: Temperature<Celsius> = Temperature {
        value: 25.0,
        _unit: PhantomData,
    };
    println!("{}", c.value);
}

These ten parts cover defining a tuple struct, instantiating and accessing, a mutable tuple struct, the newtype pattern, the zero-cost property, methods on a newtype, a newtype for a foreign trait, a unit-like struct, methods on a unit-like struct, and a marker type in generics.


Quick Reference

Tuple Struct

AspectDetail
Definitionstruct Color(i32, i32, i32);
InstantiationColor(255, 0, 0)
Field access.0, .1, .2
SizeSame as the inner types
Use caseNewtype, wrapper

Unit-Like Struct

AspectDetail
Definitionstruct Marker;
InstantiationMarker
Size0 bytes
Use caseMarker, stateless trait impl

Newtype Pattern

AspectDetail
Definitionstruct Meters(f64);
PurposeDistinct type, type safety
CostZero (same size as inner)
MethodsDefined on the wrapper
Foreign traitsAllowed because the type is local

Method Receivers

ReceiverMeaning
selfTakes ownership
&selfBorrows immutably
&mut selfBorrows mutably
NoneAssociated function

Comparison

KindFieldsAccessExample
Named-fieldNamed.fieldUser { name: String }
TuplePositional.0Color(i32, i32, i32)
Unit-likeNone—Marker

Best Practices

✅ Do This:

// Use a tuple struct for a newtype
struct Meters(f64);

// Use the newtype to enforce units
fn distance(m: Meters) { }

// Use a unit-like struct for a marker
struct Celsius;

// Implement methods on the wrapper
impl Meters {
    fn to_feet(&self) -> f64 { self.0 * 3.28084 }
}

// Use a newtype to implement a foreign trait
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper { }

❌ Don’t Do This:

// Use a bare f64 for a unit
fn distance(m: f64) { }  // ❌ no type safety

// Use a tuple struct for a domain type with named fields
struct User(String, String, u32);  // ❌ use named fields

// Forget the semicolon on a unit-like struct
struct Marker  // ❌

// Access a field by variable index
let i = 0;
color.i;  // ❌ index must be a literal

Common Pitfalls

PitfallWhy It HappensFix
Cannot access fieldUsed .name on a tuple structUse .0, .1
Index with variableThe index must be a literalUse a match or destructure
Newtype not distinctUsed a type aliasUse struct Meters(f64)
Unit-like struct missing semicolonSyntax errorAdd ;
Method not foundNot in an impl blockDefine the method
Zero-sized struct sizeExpected non-zeroIt is zero by design

Real-World Examples

1. Color

struct Color(u8, u8, u8);
let red = Color(255, 0, 0);

2. Point

struct Point(f64, f64);
let origin = Point(0.0, 0.0);

3. Meters

struct Meters(f64);
fn distance(m: Meters) { }

4. UserId

struct UserId(u64);
fn find_user(id: UserId) { }

5. Email

struct Email(String);
impl Email {
    fn new(s: String) -> Result<Email, String> {
        if s.contains('@') { Ok(Email(s)) } else { Err("invalid".into()) }
    }
}

6. Foreign Trait

struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper { }

7. Marker

struct Celsius;
struct Fahrenheit;

8. Stateless Handler

struct LogHandler;
impl Handler for LogHandler { }

9. PhantomData

struct Temperature<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

10. Destructuring

let Color(r, g, b) = red;
println!("{} {} {}", r, g, b);

Visual

Tuple Struct

┌──────────────────────────────────────────────────────────────┐
│  struct Color(i32, i32, i32);                                │
│                                                              │
│  let red = Color(255, 0, 0);                                 │
│                                                              │
│  ┌──────────────────────────┐                                │
│  │  .0   │  .1   │  .2      │                                │
│  │  255  │  0    │  0       │                                │
│  └──────────────────────────┘                                │
│                                                              │
│  Fields accessed by position.                                │
└──────────────────────────────────────────────────────────────┘

Newtype Pattern

┌──────────────────────────────────────────────────────────────┐
│  WITHOUT NEWTYPE:                                            │
│  fn distance(m: f64) { }                                     │
│  distance(3.0);        // OK                                 │
│  distance(3.0);        // OK — no unit check                 │
│                                                              │
│  WITH NEWTYPE:                                               │
│  struct Meters(f64);                                         │
│  struct Feet(f64);                                           │
│  fn distance(m: Meters) { }                                  │
│  distance(Meters(3.0));  // OK                               │
│  distance(Feet(3.0));    // Error: expected Meters           │
└──────────────────────────────────────────────────────────────┘

Unit-Like Struct

┌──────────────────────────────────────────────────────────────┐
│  struct Marker;                                              │
│                                                              │
│  let m = Marker;                                             │
│  └── size: 0 bytes                                           │
│                                                              │
│  Used as:                                                    │
│  ├── Marker type in generics                                 │
│  ├── Stateless trait implementation                          │
│  └── Placeholder value                                       │
└──────────────────────────────────────────────────────────────┘

Zero Cost of Newtype

┌──────────────────────────────────────────────────────────────┐
│  struct Meters(f64);                                         │
│                                                              │
│  size_of::<Meters>() == size_of::<f64>() == 8               │
│                                                              │
│  The wrapper adds type safety with no runtime cost.          │
│  The field access compiles to a direct read.                 │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Tuple structstruct Color(i32, i32, i32);
Field access.0, .1, .2
Newtype patternstruct Meters(f64);
Newtype costZero
Newtype purposeDistinct type, type safety
Foreign trait implAllowed with a newtype
Unit-like structstruct Marker;
Unit-like size0 bytes
Unit-like useMarker, stateless trait impl
MethodsDefined in an impl block
Marker typeUnit-like struct in generics

Key takeaways:

  • A tuple struct has fields without names, accessed by position. The definition lists the types, and the instantiation lists the values in the same order.
  • The newtype pattern wraps an inner type in a single-field tuple struct. The wrapper is a distinct type, so the compiler enforces the distinction. The pattern is used for units of measure, identifiers, validated strings, and any case where the type is the documentation.
  • The newtype is zero-cost. The wrapper has the same size and layout as the inner type, and the field access compiles to a direct read.
  • The newtype allows foreign trait implementations. The orphan rule prevents implementing a foreign trait for a foreign type, but the newtype is a local type, so the implementation is permitted.
  • A unit-like struct has no fields and is zero-sized. It is used for marker types, stateless trait implementations, and placeholder values.
  • Both kinds of struct can have methods. The methods are defined in an impl block, and the receiver follows the same rules as for named-field structs.
  • A marker type distinguishes generic instantiations. A unit-like struct used as a type parameter carries no data, but it makes two otherwise identical generic types distinct.

Remember: Tuple structs and unit-like structs are the two specialized forms of Rust’s struct system. The tuple struct is for wrappers and newtypes: a distinct type that adds safety without adding cost. The unit-like struct is for markers and stateless implementations: a type that exists only in the type system. Both are real types with real methods, and both are essential in idiomatic Rust. The newtype pattern is the most common use of the tuple struct, and the marker type is the most common use of the unit-like struct. Understanding these two forms is understanding how Rust uses the type system to encode intent.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!