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.
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:
- They are defined within a function or local scope
- They cannot be directly named outside that scope
- They can still be returned and used through type inference
- 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:
- Encapsulation: The type cannot be constructed or manipulated outside its defining scope
- Information Hiding: Implementation details are completely hidden
- Type Safety: The compiler can still perform type checking even though the type is unnamed
- Performance: Often allows for better optimization since the type’s usage is constrained
- 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.