Yen Wang

0x20Writing

Voldemort Type in Rust, C/C++, Zig, Go and Haskell

A type that must not be named: what Voldemort types are, and how Rust, Zig, C, C++, Go and Haskell express them.

Published 02 November 2025 Read 2 min Tags rust

Originally published on Medium (Medium). Read the original ↗

Based on the Wiktionary page, a Voldemort type is a programming concept named after the villain in Harry Potter, who “must not be named.” It refers to a data type that has no directly speakable name, meaning it cannot be explicitly named or referenced outside of its defining scope, but can still be used through type inference or other indirect means.

The key characteristics of Voldemort types are:

  1. They are defined within a function or local scope
  2. They cannot be directly named outside that scope
  3. They can still be returned and used through type inference
  4. They’re useful for encapsulation and creating types that exist only for specific operations

Rust

Voldemort Types in Rust:

  • A Voldemort type is a type the caller is prohibited from naming, either because no syntax exists to express it (closures), or because the API deliberately seals it behind impl Trait in return position.
  • In both cases the effect on the caller is the same: you cannot annotate, store, or depend on the concrete type directly.

Closure Type

The closure move |y| x + y has a type the caller cannot write. impl Fn(i32) -> i32 is the only way to return it without boxing.

Link: https://doc.rust-lang.org/reference/types/closure.html

fn create_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    let adder = create_adder(5);
    println!("{}", adder(3)); // 8
}

impl Trait in Return Position

When a function returns impl Trait, the concrete type is sealed from the caller by design.

fn make_greeting() -> impl Display {
    String::from("hello")
}

The caller knows they have something that implements Display. They cannot name the concrete type String from the return type alone.

Note: https://doc.rust-lang.org/reference/types/impl-trait.html

Zig

const std = @import("std");
// Example 1: Function returning anonymous struct
fn createPoint(x: f32, y: f32) @TypeOf(.{ .x = x, .y = y }) {
    return .{ .x = x, .y = y };
}
// Example 2: Generic function with unnamed type
fn createContainer(comptime T: type, value: T) struct { data: T } {
    return struct { data: T }{ .data = value };
}
// Example 3: Function returning anonymous enum
fn createStatus() @TypeOf(enum { ok, error }) {
    const Status = enum { ok, error };
    return Status.ok;
}
pub fn main() void {
    const point = createPoint(1.0, 2.0);
    std.debug.print("Point: ({}, {})\n", .{ point.x, point.y });
    
    const container = createContainer(i32, 42);
    std.debug.print("Container data: {}\n", .{container.data});
}

C

// C doesn't have true Voldemort types, but we can simulate with static functions
// and function pointers

#include <stdio.h>
#include <stdlib.h>
// Example 1: Function returning function pointer to static function
typedef int (*operation_t)(int, int);
operation_t create_adder() {
    // This static function is "unnamed" from outside perspective
    static int add_impl(int a, int b) {
        return a + b;
    }
    return add_impl;
}
// Example 2: Macro creating unnamed struct type
#define CREATE_POINT(x, y) \
    (struct { int x, y; }){ .x = (x), .y = (y) }

// Example 3: Function with local struct definition
// Note: Caller must free the returned pointer
typedef struct container_t container_t;

void* create_data_container(int value) {
    struct container {  // This struct type is local and unnamed outside
        int data;
        void (*print)(void*);
    };
    
    static void print_container(void* ptr) {
        struct container* c = (struct container*)ptr;
        printf("Data: %d\n", c->data);
    }
    
    struct container* c = malloc(sizeof(struct container));
    if (c == NULL) return NULL;
    
    c->data = value;
    c->print = print_container;
    return c;
}

int main() {
    operation_t op = create_adder();
    printf("Result: %d\n", op(5, 3));
    
    auto point = CREATE_POINT(10, 20);  // Type cannot be named
    printf("Point: (%d, %d)\n", point.x, point.y);
    
    // Example 3 with proper cleanup
    void* container = create_data_container(42);
    if (container != NULL) {
        // Access through function pointer stored in the container
        // Note: In real code, you'd need a way to safely call methods
        free(container);
    }
    
    return 0;
}

C++

#include <iostream>
#include <functional>
// Example 1: Lambda with unnamed type
auto create_multiplier(int factor) {
    return [factor](int x) { return x * factor; };
    // Lambda type cannot be named directly
}
// Example 2: Function returning auto with complex type
auto create_complex_operation() {
    return [](auto x, auto y) -> decltype(x + y) {
        return x * 2 + y * 3;
    };
}
// Example 3: Template function with unnamed return type
template<typename T>
auto create_wrapper(T value) {
    struct wrapper {
        T data;
        void print() const { std::cout << "Value: " << data << std::endl; }
    };
    return wrapper{value};
}
// Example 4: Function returning unnamed iterator type
auto create_filtered_range(const std::vector<int>& vec) {
    return vec | std::views::filter([](int x) { return x > 0; })
              | std::views::transform([](int x) { return x * 2; });
}
int main() {
    auto mult = create_multiplier(3);
    std::cout << mult(4) << std::endl; // 12
    
    auto op = create_complex_operation();
    std::cout << op(2, 3) << std::endl; // 13
    
    auto wrapper = create_wrapper(42);
    wrapper.print();
    
    return 0;
}

Go

package main

import "fmt"
// Example 1: Function returning anonymous struct
func createPoint(x, y float64) struct{ X, Y float64 } {
    return struct{ X, Y float64 }{x, y}
}
// Example 2: Function returning closure
func createPrinter(msg string) func() {
    return func() {
        fmt.Println(msg)
    }
}
// Add Print method to the anonymous struct
func (p struct{ message string }) Print() {
    fmt.Println(p.message)
}
// Example 3: Function returning channel of anonymous type
func createDataChannel() <-chan struct{ ID int; Name string } {
    ch := make(chan struct{ ID int; Name string }, 1)
    ch <- struct{ ID int; Name string }{1, "test"}
    close(ch)
    return ch
}
// Example 4: Function with local type definition
func createProcessor() func(int) int {
    type processor struct {
        multiplier int
    }
    
    p := processor{multiplier: 2}
    
    return func(x int) int {
        return x * p.multiplier
    }
}
func main() {
    point := createPoint(1.5, 2.5)
    fmt.Printf("Point: (%.1f, %.1f)\n", point.X, point.Y)
    
    printer := createPrinter("Hello, Voldemort!")
    printer() // Call the closure
    
    ch := createDataChannel()
    data := <-ch
    fmt.Printf("Data: ID=%d, Name=%s\n", data.ID, data.Name)
    
    proc := createProcessor()
    fmt.Printf("Processed: %d\n", proc(5))
}

Haskell

-- Example 1: Function with local data type
createCounter :: Int -> (Int -> Int, Int -> Int)
createCounter initial = (increment, decrement)
  where
    -- Local data type that cannot be named outside
    data Counter = Counter Int
    
    increment x = case Counter (initial + x) of
      Counter n -> n
    
    decrement x = case Counter (initial - x) of
      Counter n -> n

-- Example 2: Function returning existential type
{-# LANGUAGE ExistentialQuantification #-}
data SomeShow = forall a. Show a => SomeShow a
createShowable :: Int -> SomeShow
createShowable x = SomeShow (LocalType x)
  where
    -- This type exists only in this scope
    data LocalType = LocalType Int
    instance Show LocalType where
      show (LocalType n) = "LocalType " ++ show n

-- Example 3: Function with local newtype
createProcessor :: String -> (String -> String)
createProcessor prefix = process
  where
    -- Local newtype that cannot be accessed outside
    newtype ProcessedString = PS String
    
    process :: String -> String
    process s = case PS (prefix ++ s) of
      PS result -> result

-- Example 4: Rank-2 polymorphism creating unnamed types
{-# LANGUAGE RankNTypes #-}
createApplier :: (forall a. Show a => a -> String) -> Int -> String
createApplier f x = f (LocalInt x)
  where
    -- Local data type
    data LocalInt = LocalInt Int
    instance Show LocalInt where
      show (LocalInt n) = "Local: " ++ show n

-- Example 5: Function returning record with unnamed type
-- Example 5: Function returning tuple with operations on unnamed type
createRecord :: Int -> (Int, Int -> Int)
createRecord x = (getValue, transform)
  where
    data LocalData = LocalData Int
    extract (LocalData n) = n
    
    localData = LocalData x
    getValue = extract localData
    transform y = extract (LocalData (x + y))

main :: IO ()
main = do
  let (inc, dec) = createCounter 10
  print $ inc 5  -- 15
  print $ dec 3  -- 7
  
  let showable = createShowable 42
  case showable of
    SomeShow x -> print x
  
  let processor = createProcessor "Prefix: "
  putStrLn $ processor "test"  -- "Prefix: test"
  
  let applier = createApplier show
  putStrLn $ applier 100
  
  let (value, transform) = createRecord 10
  print value              -- 10
  print $ transform 5      -- 15

Key Benefits of Voldemort Types:

  1. Encapsulation: The type cannot be constructed or manipulated outside its defining scope
  2. Information Hiding: Implementation details are completely hidden
  3. Type Safety: The compiler can still perform type checking even though the type is unnamed
  4. Performance: Often allows for better optimization since the type’s usage is constrained
  5. API Design: Forces users to interact with your code through specific interfaces

Voldemort types are particularly useful in functional programming and when designing APIs where you want to provide functionality without exposing implementation details.