Exploring Programming Languages — Zig
Let’s continue our exploration of programming languages with a new, interesting one: Zig.
Exploring Programming Languages — Zig

Generated by Google’s Gemini
Let’s continue our exploration of programming languages with a new, interesting one: Zig.
Not a Medium subscriber? 😕 You can read this post for free here 🥳
Zig can be considered a young language as it was released in 2016, which means just 10 years ago.
I became aware of it as soon as it came out, but it seems too complex to invest time learning it. What changed? Nothing…I just thought I needed to learn it one day 😅 I still think its cubbersome and complex.
Fun fact: Zig is aimed to be a C replacement.
Fun fact: When Zig claims to be a simple programming language, it doesn’t mean is easy to learn, it means it doesn’t have hidden constructs.
Ok, let’s jump into our main topic, which is creating an LED Numbers application.
The application should work like this: we input 1977, and it returns:

LED Numbers output
Now, keep in mind that we want to show this to newcomers to Zig, not to seasoned developers.
The first thing we’re going to do is to create a HashMap that holds lists(Similar to a Map or a Dictionary in other languages), to hold all the components of the LED numbers:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var map = std.StringHashMap([]const u8).init(allocator);
defer map.deinit();
try map.put("0", " _ ,| | ,|_| ");
try map.put("1", " , | , | ");
try map.put("2", " _ , _| ,|_ ");
try map.put("3", " _ , _| , _| ");
try map.put("4", " ,|_| , | ");
try map.put("5", " _ ,|_ , _| ");
try map.put("6", " _ ,|_ ,|_| ");
try map.put("7", " _ , | , | ");
try map.put("8", " _ ,|_| ,|_| ");
try map.put("9", " _ ,|_| , _| ");
}
Yup…already complex and with some weird stuff…let’s analyze line by line:
- In the first line, we’re importing the std library and assigning it to the std constant. We need this for the allocators, hashmaps, and more.
- On the second line, we’re declaring a public (pub) function that can return an error (!void) if found.
- On the third line, we’re declaring a General Purpose Allocator that will help us manage heap memory.
- On the fourth line, we’re freeing the internal allocator state, so there’s no memory flying around. We call deinit() for this. And this happens when defer is called, that means when our main() function exits.
- On the fifth line, we define a new allocator that will be handled by the allocator constant.
After all this, we can start with our HashMap:
- On the first line, we define a HashMap with String keys. The value will be a String as well ([]const u8). The .init(allocator) will initialize the HashMap.
- On the second line, we’re going to clear the map HashMap and free its memory using deinit(), and this happens when defer is called, which means when our main() function exits.
- In the following lines, we try to add both the key and the content of each HashMap entry.
We’re separating each number into three lines. So, for example, if we were to print the number 6, we could do it like this:
var row1 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row2 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row3 = try std.ArrayList(u8).initCapacity(allocator, 0);
defer row1.deinit(allocator);
defer row2.deinit(allocator);
defer row3.deinit(allocator);
const digit: []const u8 = map.get("6").?;
var it = std.mem.splitScalar(u8, digit, ',');
const part1 = it.next().?;
const part2 = it.next().?;
const part3 = it.next().?;
try row1.appendSlice(allocator, part1);
try row2.appendSlice(allocator, part2);
try row3.appendSlice(allocator, part3);
std.debug.print("\n{s}\n", .{row1.items});
std.debug.print("{s}\n", .{row2.items});
std.debug.print("{s}\n", .{row3.items});
And of course, we need to explain what’s going on…
- On the first, second, and third lines, we’re pretty much creating an ArrayList ready to accept elements.
- On the next three lines, we’re freeing the memory from these three variables.
- On the next line, we’re reading our HashMap with the key 6. The .? means I will crash if I don’t find that key.
- The next line will simply split the content of the HashMap value using splitScalar.
- On the next three lines, we’re going to read each line from the split and assign them to each variable, so part1, part2, and part3.
- On the next three lines, we’re copying from part1, part2, and part3 into row1, row2, and row3. Why? Because part1, part2, and part3 are just placeholders until we allocate some memory and assign the values.
- On the last 3 rows, we’re printing the values.
The result is going to be:
_
|_
|_|
Of course, we would like to print more than one number, so the idea would be to concatenate each number’s line and then print them:
var row1_1 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row2_1 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row3_1 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row1_2 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row2_2 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row3_2 = try std.ArrayList(u8).initCapacity(allocator, 0);
defer row1_1.deinit(allocator);
defer row2_1.deinit(allocator);
defer row3_1.deinit(allocator);
defer row1_2.deinit(allocator);
defer row2_2.deinit(allocator);
defer row3_2.deinit(allocator);
const digit1: []const u8 = map.get("6").?;
var it1 = std.mem.splitScalar(u8, digit1, ',');
const digit2: []const u8 = map.get("2").?;
var it2 = std.mem.splitScalar(u8, digit2, ',');
const part1_1 = it1.next().?;
const part2_1 = it1.next().?;
const part3_1 = it1.next().?;
const part1_2 = it2.next().?;
const part2_2 = it2.next().?;
const part3_2 = it2.next().?;
try row1_1.appendSlice(allocator, part1_1);
try row2_1.appendSlice(allocator, part2_1);
try row3_1.appendSlice(allocator, part3_1);
try row1_2.appendSlice(allocator, part1_2);
try row2_2.appendSlice(allocator, part2_2);
try row3_2.appendSlice(allocator, part3_2);
std.debug.print("\n{s}{s}\n", .{row1_1.items, row1_2.items});
std.debug.print("{s}{s}\n", .{row2_1.items, row2_2.items});
std.debug.print("{s}{s}\n", .{row3_1.items, row3_2.items});
Here, we just duplicated every line and changed the variables, adding a _1 or _2. Not a funny thing to do, but works.
The result is going to be:
_ _
|_ _|
|_| |_
Of course, we need a way to automate this, as the number gets longer, the source code is going to be longer, with more variables and more complexity 🤨, and we don’t want anything hardcoded.
Ok, we have then 3 lines that need to be printed. But an unknown number. It’s important to know at runtime how many digits our number has so that we act accordingly. Let’s say we want to print the number 1593, so we have 4 digits, and we need to print 3 lines, each line with sections of those 4 digits, so here’s our application code:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var map = std.StringHashMap([]const u8).init(allocator);
defer map.deinit();
try map.put("0", " _ ,| | ,|_| ");
try map.put("1", " , | , | ");
try map.put("2", " _ , _| ,|_ ");
try map.put("3", " _ , _| , _| ");
try map.put("4", " ,|_| , | ");
try map.put("5", " _ ,|_ , _| ");
try map.put("6", " _ ,|_ ,|_| ");
try map.put("7", " _ , | , | ");
try map.put("8", " _ ,|_| ,|_| ");
try map.put("9", " _ ,|_| , _| ");
var buffer: [100]u8 = undefined;
std.debug.print("Enter a number: ", .{});
const stdin = std.fs.File.stdin();
const bytes_read = try stdin.read(&buffer);
var num = buffer[0..bytes_read];
if (num.len > 0 and num[num.len - 1] == '\n') {
num = num[0..num.len - 1];
}
var row1 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row2 = try std.ArrayList(u8).initCapacity(allocator, 0);
var row3 = try std.ArrayList(u8).initCapacity(allocator, 0);
defer row1.deinit(allocator);
defer row2.deinit(allocator);
defer row3.deinit(allocator);
for (num) |char| {
var key_buf: [1]u8 = .{char};
if (map.get(key_buf[0..])) |digit| {
var it = std.mem.splitScalar(u8, digit, ',');
const part1 = it.next().?;
const part2 = it.next().?;
const part3 = it.next().?;
try row1.appendSlice(allocator, part1);
try row2.appendSlice(allocator, part2);
try row3.appendSlice(allocator, part3);
} else {
std.debug.print("\nInvalid character: {c}\n", .{char});
return;
}
}
std.debug.print("\n{s}\n", .{row1.items});
std.debug.print("{s}\n", .{row2.items});
std.debug.print("{s}\n", .{row3.items});
}
Let’s explain this by sections, starting with asking the user for a number:
- var buffer: [100]u8 = undefined; We’re declaring a fixed array of 100 bytes.
- On the second line, we ask the user to enter a number.
- On the third line, const stdin = std.fs.File.stdin(); this is going to read the keyboard input.
- On the fourth line, const bytes_read = try stdin.read(&buffer); this is going to read the pointer to the buffer array, and fill bytes_read with the number of bytes needed.
- On the fifth line, var num = buffer[0..bytes_read]; this is going to read the bytes from buffer and assign the value to num.
- On the following two lines, we’re going to make sure that the input is not empty, and if there’s a new line character at the end, we need to remove it.
Now, let’s move into the interesting part, which is the for loop:
for (num) |char| {
var key_buf: [1]u8 = .{char};
if (map.get(key_buf[0..])) |digit| {
var it = std.mem.splitScalar(u8, digit, ',');
const part1 = it.next().?;
const part2 = it.next().?;
const part3 = it.next().?;
try row1.appendSlice(allocator, part1);
try row2.appendSlice(allocator, part2);
try row3.appendSlice(allocator, part3);
} else {
std.debug.print("\nInvalid character: {c}\n", .{char});
return;
}
}
std.debug.print("\n{s}\n", .{row1.items});
std.debug.print("{s}\n", .{row2.items});
std.debug.print("{s}\n", .{row3.items});
- On the first line, we iterate over each character of our number input. We’re going to use 1593, so we’re going to read 1,5, 9, and 3.
- On the second line, var key_buf: [1]u8 = .{char}; we need to grab the character and store it into a small array, because that’s what our HashMaps expect as a key.
- On the third line, if (map.get(key_buf[0..])) |digit| if we can get the value with the key (we get the first element), then assign it to digit.
- On the fourth line, var it = std.mem.splitScalar(u8, digit, ‘,’); we split the value of the HashMap.
- On the next three lines, we’re going to read each line from the split and assign them to each variable, so part1, part2, and part3.
- On the next three lines, we’re copying from part1, part2, and part3 into row1, row2, and row3. Why? Because part1, part2, and part3 are just placeholders until we allocate some memory and assign the values.
- In the else section, we return an error if the value cannot be printed.
- On the last 3 rows, we’re printing the values.
For context, let’s debug our application and see which values would be present on each iteration:
- For the first digit, which is 1, we’re going to read the content of the map and extract the value. We’re going to append the values to the variables row, row2, and row3.
|
|
- For the second digit, which is 5, we’re going to read the content of the map and extract the value. We’re going to append the values to the variables row, row2, and row3.
_
| |_
| _|
- For the third digit, which is 5, we’re going to read the content of the map and extract the value. We’re going to append the values to the variables row, row2, and row3.
_ _
| |_ |_|
| _| _|
- For the fourth and last digit, which is 3, we’re going to read the content of the map and extract the value. We’re going to append the values to the variables row, row2, and row3.
_ _ _
| |_ |_| _|
| _| _| _|
If we name our application LEDNumbers.zig, we can call it in the terminal like this:
zig run LEDNumbers.zig

That’s it 🤓 I hope you liked it, although it was quite a ride 🤓, and I will see you again on the next installment, which will present Fennel.
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
Follow me on X: @Blag Follow me on BlueSky: @blag.bsky.social Connect with me on LinkedIn: Blag aka Alvaro Tejada Galindo
메타데이터
- post_id
- 2e3e8a08e341
- slug
- exploring-programming-languages-zig-2e3e8a08e341
- url
- https://blog.devgenius.io/exploring-programming-languages-zig-2e3e8a08e341
- canonical_url
- https://blog.devgenius.io/exploring-programming-languages-zig-2e3e8a08e341
- author_url
- https://medium.com/@atejada
- status
- ok
- fetched_at
- 2026-06-14 11:28:49