@import() code style

I am curious to see how others use @import() with regard to code style. This is a purely subjective matter - unless you’re doing something really weird, I guess - so please forgive the noise.

I used to import modules such that symbols would be referenced at a maximum depth of 1 namespace. For example, when using std.debug.print, I would import std.debug as debug and use it directly.

const std = @import("std");
const debug = std.debug;

pub fn main() void {
    debug.print("Hello, world!\n", .{});
}

After a while, I decided that I could be more explicit in by referring to each namespace absolutely. However, this has some code style side-effects, such as making nested, long-lined conditional statements look a bit janky.

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}

Currently, I still refer to the absolute namespace and additionally put my imports at the bottom of the file.

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}

const std = @import("std");

The community is divided between those that put imports on top and those that put imports on bottom of the files. The arguments are similar to those from Liliput and those from Blefuscu (from Gulliver’s Travels)
I personally prefer to put the imports on the top/beginning of the file, because I used to do it that way since forever.

Most of the times I am using the full std package name.
There are some exceptions, two examples are:

  • For assert, I am declaring it as const assert = std.debug.assert; and use it with the function name. Depending on the context I might declare other functions like this, for example when I am doing decimal arithmetic I am using dec as a decimal number constructor, instead of the full name decimal.Decimal.dec.
  • For testing, when I have testing files (test cases only - not mixed code with tests) I am declaring the package as const testing = std.testing; Depending on the context it is common to have a package referenced like that.

On all exceptions it is really easy for the reader to understand the full package name or the function’s package.

2 Likes

I might be an oddball. When it comes to content from std, I almost always import only std itself and don’t mind repeatedly writing out their full namespace where needed. But with my own modules, I’m much more casual.

I often copy some code from std and reimplement certain use cases to meet my needs. So when it comes to stuff from std, I really care about being able to tell at a glance whether the code is using std.

For third-party modules, I will maintain the same approach I use with std.

2 Likes

I’m the opposite. I rarely, if ever use the std namespace directly. I like seeing which parts of standard I’m using up at the top of the file and I prefer the shorter nomenclature throughout each module. That’s all just personal preference, nothing I’d push as “guidance.”

Your point about reimplementing parts of std makes a lot of sense to me though. Zig is the first language I’ve used where doing so seems common. Being able to easily differentiate between your implementation and the original at a glance makes a lot of sense.

1 Like

I haven’t written much Zig, but I’m currently refactoring a rust project into Zig, and how I like to lay out my files is:

Code
---
Imports
---
Tests

As for how to name imports, I’d argue that Zig’s “no shadowing” rule nudges you towards preferring top level imports (i.e. const std = @import("std")), so that you can always be sure that meaning stays consistent throughout your program.

That being said, I also like my lines to be shorter than the Zig formatter typically breaks at, so if I have a chunk of code with lots of nesting that has the same function call over and over, I’ll alias the function just inside the nearest block…turns out I learned this habit from reading std code :sweat_smile::

// From bit_reader.zig (No longer in std)

test "api coverage" {
    const mem_be = [_]u8{ 0b11001101, 0b00001011 };
    const mem_le = [_]u8{ 0b00011101, 0b10010101 };

    var mem_in_be = std.io.fixedBufferStream(&mem_be);
    var bit_stream_be = bitReader(.big, mem_in_be.reader());

    var out_bits: u16 = undefined;

    const expect = std.testing.expect;
    const expectError = std.testing.expectError;

    try expect(1 == try bit_stream_be.readBits(u2, 1, &out_bits));
    try expect(out_bits == 1);
    try expect(2 == try bit_stream_be.readBits(u5, 2, &out_bits));
    try expect(out_bits == 2);
    try expect(3 == try bit_stream_be.readBits(u128, 3, &out_bits));
    try expect(out_bits == 3);
    try expect(4 == try bit_stream_be.readBits(u8, 4, &out_bits));
    try expect(out_bits == 4);
    try expect(5 == try bit_stream_be.readBits(u9, 5, &out_bits));
    try expect(out_bits == 5);
    try expect(1 == try bit_stream_be.readBits(u1, 1, &out_bits));
    try expect(out_bits == 1);

    // ...
}

It is quite refreshing to use a language with a strong standard library that encourages you to “namespace” your imports. It’s shocking how many lines your imports take up in languages like Rust, Haskell, or JS, where you’re encouraged to import things with the shortest path possible (i.e. import { foo, baz } from "bar/bat.js).

1 Like

Zig is the first language I’ve used where doing so seems common.

Zig is the first language I’ve used that ships with its entire standard library in a form that’s easy to inspect and build, and the lazy compilation model means you can usually just yoink standard library code into your own and It Just Works™.

That plus being a very simple and local language by design means I can usually understand the standard library code as well. That makes vendoring it a breeze compared to almost any other language.

3 Likes

I don’t mind to have imports at the top or bottom, but I always try to keep only one level of namespace depth, This is the one thing I don’t do in C++, but in Zig it’s cheap, also you don’t need in my opinion to type std.debug.print, or std.Io.net.Address, all my files often look like this

const std = @import("std");
const mem = std.mem;
const heap = std.heap;
const log = std.log;
const debug = std.debug;
const testing = std.testing;
const process = std.process;
const Io = std.Io;
const json = std.json;

This is of course a matter of personal taste, there is no wrong or good answer, I do think that one level of namespace does make the code cleaner, sometimes 2, if the child namespace could be confused with another child namespace, but other than that, I try to avoid littering the code with std everywhere.

6 Likes