Defer Expressions?

I often find myself making variables to just be able to free them. That adds a lot of unnecessary boilerplate to the code and makes it less readable/understandable. I think something like deferExpression would be really nice as a keyword or something like that. Which could just generate the variable and defer statement. A short time solution could be to setup zig fmt to make the defer statement behind the variable statement. Example:

const std = @import("std");
const Allocator = std.mem.Allocator;

pub fn expressionToFree(allocator: Allocator) ![]const u8 {
    var list = std.ArrayList(u8).init(allocator);
    try list.appendSlice("hello");
    return try list.toOwnedSlice();
}

const testing = std.testing;

test "much boilerplate" {
    const allocator = testing.allocator;

    const result = try expressionToFree(allocator);
    defer allocator.free(result);

    try testing.expectEqualStrings("hello", result);
}

test "less boilerplate" {
    const allocator = testing.allocator;

    const result = try expressionToFree(allocator); defer allocator.free(result);

    try testing.expectEqualStrings("hello", result);
}

fn deferExpression(allocator: Allocator, expr: []const u8) []const u8 {
    allocator.free(expr);
    return expr;
}
test "deferExpression" {
    const allocator = testing.allocator;

    // This leaks memory since the value is freed before it is used
    try testing.expectEqualStrings("hello", deferExpression(allocator, try expressionToFree(allocator)));
}

What do you think about this? Is there already something similar?

You can use an allocator that automatically frees the memory for you, like an Arena allocator:

// You have the boilerPlate only once per function/test block
const arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

try testing.expectEqualStrings("hello", try expressionToFree(arena.allocator()));
2 Likes

Thanks for big functions this really helps. For small functions I would still like to have something like defer Expression.

Well I personally think that the explicit defer is more readable, because it’s immediately obvious that something is freed.

But if you really want to cram everything together, you could do something like this:

threadlocal var local: []const u8 = undefined
pub fn createLocal(val: []const u8) []const u8 {
    local = val;
    return val;
}
pub fn destroyLocal(allocator: Allocator) void {
    allocator.free(local);
}
...
try testing.expectEqualStrings("hello", createLocal(try expressionToFree(allocator)));
destroyLocal(allocator);

I think this is not good possible with procedures, then the explicit defer is definitely better. Maybe as a keyword than everything knows what is going on.

How would that work as a keyword? Do you want a keyword that’s specialized for allocator.free?

Yes, something similar to this:

test "deferExpression" {
    const allocator = testing.allocator;

    try testing.expectEqualStrings("hello", deferExpression try expressionToFree(allocator): allocator);
}

To this:

test "deferExpression" {
    const allocator = testing.allocator;

    const expr_1 = try expressionToFree(allocator)
    defer allocator.free(expr_1);
    try testing.expectEqualStrings("hello", expr_1);
}

In this example it only saves 2 lines, but if you have this more can be really valuable.

I don’t think it’s a good idea to specialize a syntax for one specific standard library struct.
Like what if expressionToFree returns an ArrayList, or a nullterminated C string, or what if I want to use my own Allocator interface (I do)?

Furthermore this is really different from Zig’s usual syntax.
Maybe something like this could be better?

try testing.expectEqualStrings("hello", try expressionToFree(allocator) defer |v| allocator.free(v));

But I don’t think it really is worth it. It does save two lines, but at the cost of making one line significantly more complex and adding yet another new syntax you have to know about.

In my experience, “saving lines” is not something Zig is particularly interested in. It very much embraces the “just write the X” way of programming (just write the loop, just wrap it in a block, just assign the temporary to a const, etc).

This ties into this part of the Zig zen:

Favor reading code over writing code.

8 Likes

I’m also not in favor of this idea, because when stuff is crammed together on one line like was demonstrated I can very easily skip over the extra stuff by accident and then I go “but where is memory being freed?” or similar.

1 Like

It always starts with one little features, and then we get to this.

template <typename T>
concept Arithmetic = is_arithmetic_v<T>;

template <Arithmetic T, size_t N, typename F>
constexpr auto convolutedFunction(T x, F&& f) -> tuple<vector<T>, T> {
    vector<T> v(N);
    random_device rd; mt19937 rng(rd()); uniform_int_distribution<T> dist(1, 100);
    ranges::generate(v, [&]() { return f(dist(rng)) * x; });
    return {v, reduce(v.begin(), v.end(), T(0), plus<>())};
}

template <typename T, size_t N>
void execute() {
    auto [vec, sum] = convolutedFunction<T, N>(3, [](T n) { return n % 2 ? n : n / 2; });
    ranges::for_each(vec, [](auto n) { cout << n << ' '; });
    cout << "\nSum: " << sum << '\n';
}

int main() {
    execute<int, 10>();
    return 0;
}

Joke aside, I think it’s more legible to have defer on it’s own line, I understand the frustration of typing it over and over again, but I believe it’s better than trying to figure out later on what the hell is wrong with some code and nothing make sense, until you remember that some overloaded definition of a function behaves slightly differently now that you have changed the type of your parameter. My point is simplicity has a lot of values, there are tons of features that could (emphasis on the could) improve Zig, but there is always a price to pay. Happy new year btw :slight_smile:

5 Likes

Why do we always get frustrated about having to type when we could make a tool that inserts the line we want automatically or we could make a special command that does it or we could program it into our keyboard? We’re programmers, right? Just use or make a tool to make it easier.

2 Likes

The more code I write the more I would like this feature, but that is probably just my coding style. I think this is the best idea, so far:

For me it is not the frustration when writing it, but when reading it. To quote zig zen:

Favor reading code over writing code.

It seems like most people don’t want this feature, so it would be not worth, maybe in the future.

Following that idea, one could also just write a helper function that also frees the input parameter:

fn expectEqualStringsAndFree(expected: []const u8, allocator: std.mem.Allocator, actual: []const u8) !void {
    defer allocator.free(actual);
    return testing.expectEqualStrings(expected, actual);
}
...
try expectEqualStringsAndFree("hello", allocator, try expressionToFree(allocator));

With some comptime code it might even be possible to generalize the creation of these helper functions, then it could look like this

andFree(testing.expectEqualStrings)(.{"hello", allocator, try expressionToFree(allocator)});

If this can basically be done in user space, then I don’t think it is justified to add a new keyword for it.

1 Like