Zig Project: Generic Library
A generic library is a reusable collection of code that works with many types. It is written once with a comptime type parameter and then used with integers, text, or any other type.
A generic stack
The stack is generic over its element type. Pushing 10, 20, and 30, then popping twice, prints the last two values in reverse order.
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
fn init(alloc: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(alloc) };
}
fn deinit(self: *Self) void {
self.items.deinit();
}
fn push(self: *Self, value: T) !void {
try self.items.append(value);
}
fn pop(self: *Self) ?T {
return self.items.pop();
}
};
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
var stack = Stack(i32).init(alloc);
defer stack.deinit();
try stack.push(10);
try stack.push(20);
try stack.push(30);
const out = std.io.getStdOut().writer();
try out.print("{d} {d}\n", .{ stack.pop().?, stack.pop().? });
}Testing the stack
Tests check the stack without printing anything. Three tests cover push and pop order, popping an empty stack, and peeking without removing.
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
fn init(alloc: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(alloc) };
}
fn deinit(self: *Self) void {
self.items.deinit();
}
fn push(self: *Self, value: T) !void {
try self.items.append(value);
}
fn pop(self: *Self) ?T {
return self.items.pop();
}
fn peek(self: *Self) ?T {
if (self.items.items.len == 0) return null;
return self.items.items[self.items.items.len - 1];
}
};
}
test "push then pop returns last first" {
var stack = Stack(i32).init(std.testing.allocator);
defer stack.deinit();
try stack.push(1);
try stack.push(2);
try std.testing.expectEqual(@as(i32, 2), stack.pop().?);
try std.testing.expectEqual(@as(i32, 1), stack.pop().?);
}
test "pop from empty stack is null" {
var stack = Stack(i32).init(std.testing.allocator);
defer stack.deinit();
try std.testing.expect(stack.pop() == null);
}
test "peek keeps the value" {
var stack = Stack(i32).init(std.testing.allocator);
defer stack.deinit();
try stack.push(7);
try std.testing.expectEqual(@as(i32, 7), stack.peek().?);
try std.testing.expectEqual(@as(i32, 7), stack.pop().?);
}One library, two types
The same stack code serves a stack of words and a stack of flags. Each test uses a different element type, which is the whole point of a generic library.
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
fn init(alloc: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(alloc) };
}
fn deinit(self: *Self) void {
self.items.deinit();
}
fn push(self: *Self, value: T) !void {
try self.items.append(value);
}
fn pop(self: *Self) ?T {
return self.items.pop();
}
};
}
test "words stack pops last word first" {
var stack = Stack([]const u8).init(std.testing.allocator);
defer stack.deinit();
try stack.push("red");
try stack.push("blue");
try std.testing.expectEqualStrings("blue", stack.pop().?);
try std.testing.expectEqualStrings("red", stack.pop().?);
}
test "flags stack pops last flag first" {
var stack = Stack(bool).init(std.testing.allocator);
defer stack.deinit();
try stack.push(true);
try stack.push(false);
try std.testing.expectEqual(false, stack.pop().?);
try std.testing.expectEqual(true, stack.pop().?);
}Example
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: std.ArrayList(T),
const Self = @This();
fn init(alloc: std.mem.Allocator) Self {
return .{ .items = std.ArrayList(T).init(alloc) };
}
fn deinit(self: *Self) void {
self.items.deinit();
}
fn push(self: *Self, value: T) !void {
try self.items.append(value);
}
fn pop(self: *Self) ?T {
return self.items.pop();
}
fn peek(self: *Self) ?T {
if (self.items.items.len == 0) return null;
return self.items.items[self.items.items.len - 1];
}
};
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const alloc = arena.allocator();
var history = Stack(i32).init(alloc);
defer history.deinit();
try history.push(100);
try history.push(200);
const undone = history.pop().?;
const current = history.peek().?;
const out = std.io.getStdOut().writer();
try out.print("undone: {d}\ncurrent: {d}\n", .{ undone, current });
}Key points
- Generic library: a generic library is code that is written once and reused with different types.
- Stack: a stack is a collection where the last value pushed on is the first value popped off.
- A generic stack takes its element type as a comptime parameter and stores that type only.
- Push adds a value to the top of the stack and pop removes and returns the top value.
- Peek reads the top value without removing it, so the stack stays unchanged.
- A stack built on an array list grows as values are added and frees its storage on deinit.
- Tests prove a generic library works by checking it with more than one element type.
The program uses the generic stack as an undo history for scores. Pushing 100 and then 200 makes 200 the top value. Popping removes 200 as the undone score, and peeking then reads 100 as the current score without removing it.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A generic stack holds 5, then 9, then 13. What does the first pop return?