Compact way of create objects on demand

Wondering if there’s a less verbose way of obtaining/creating an optional value than the following:

var object_maybe: ?*Object = null;
for (items) |item| {
    if (isNeeded(item)) {
        const object = object_maybe orelse create: {
            const object = try Object.create(allocator);
            object_maybe = object;
            break :create object;
        };
        // ... add information from item to object
    }
}
if (object_maybe) |object| {
    sendObject(object);
} else {
    // nothing gets sent
}

EDIT: Added missing identializer and placed operation in loop for clarity

Something like this, maybe:

const std = @import("std");

const Object = u8;
const allocator = std.testing.allocator;

test {
    const object_opt: ?*Object = try allocator.create(Object);
    allocator.destroy(object_opt.?);
}

maybe:

var object_maybe: ?*Object = null;

if (object_maybe != null)
    object_maybe = try Object.create(allocator);

or

var object_maybe: ?*Object = Object.create(allocator) catch null;

Interesting. I mean, ignoring an OOM makes my palms sweat, but maybe it’s just me.

I think in this case, since the value is optional pointer, they’ll need to check the value before using it anyway. If we’re going to use it without checking it, why make it optional?

Now, this doesn’t mean they can’t check for the wrong thing (for instance, missing the fact that the value is null because of OOM instead of some other problem). The issue here is that null is an ambiguous signal - there could be valid ways something becomes null and erroneous ways.

So I rather like the suggestion by @dimdin with the caveat that the next check for null also includes information about what could have happened with the allocator. Such as “failed to allocate object”.

Perhaps. Re-encoding error value as null just seems like a redundant design.

Yes - but I think that’s what the suggestion would require. I actually prefer the verbose declaration because you actually have to say what the errors are.

This is an aside, so I’ll be brief, but I think using null to signal errors is an issue.

1 Like

How about this:

to make sure we cleanup the object if we created one:

defer if (object_maybe) |o| allocator.destroy(o);

to ensure the object is created if needed:

object_maybe = object_maybe orelse try Object.create(allocator);

if the try fails we exit that scope so right after we can just assume the object is there:

var object = object_maybe.?;

Everything together:

const std = @import("std");

const Object = struct {
    data: [20]u16 = [1]u16{0} ** 20,

    fn create(allocator: std.mem.Allocator) !*Object {
        var obj = try allocator.create(Object);
        obj.* = .{};
        obj.data[0] = 0b1001101;
        return obj;
    }
};

pub fn isNeeded(x: i32) bool {
    return x == 3;
}

pub fn sendObject(o: *Object) void {
    std.debug.print("sending object: {}\n", .{o});
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const items = [_]i32{ 0, 1, 2, 3, -857, 5335 };

    {
        var object_maybe: ?*Object = null;
        defer if (object_maybe) |o| allocator.destroy(o);
        for (items) |item| {
            if (isNeeded(item)) {
                object_maybe = object_maybe orelse try Object.create(allocator);
                var object = object_maybe.?;
                // ... add information from item to object
                object.data[1] = @intCast(item);
            }
        }
        if (object_maybe) |object| {
            sendObject(object);
        } else {
            // nothing gets sent
        }
    }
}

We could even bundle the sendObject together with the defer cleanup if sendObject can’t fail.

1 Like

@tensorush and @AndrewCodeDev I agree that re-encoding the error as null is meaningless.
But using null to handle an error, actually to ignore an error, is not an issue. For example as error handling strategy for error handlers.

Right, and that’s why I like your approach depending on the circumstance. In this case, it’s obfuscating but using null where an error type is more appropriate is my main point.

I’d be happy to continue talking about this on another thread if we want to take up the discussion :slight_smile:

My first proposal after using your edited code becomes:

var object_maybe: ?*Object = null;
for (items) |item| {
    if (isNeeded(item)) {
        if (object_maybe == null)
             object_maybe = try Object.create(allocator);
        // ... add information from item to object
    }
}
if (object_maybe) |object| {
    sendObject(object);
} else {
    // nothing gets sent
}

While I enjoy the discussion, I understand that we all agree on this, so no other thread is necessary.

1 Like

Thanks! That’s what I’m looking for. Would be nice if we can shrink the syntax down to something like this:

object_maybe ?= try Object.create(allocator);

You can do even one better if you ignore how cursed my helper function looks :laughing::

var object = try ensure(&object_maybe, Object.create, .{allocator});

Lets hide the helper :stuck_out_tongue:

pub inline fn ensure(
    ptr_maybe: anytype,
    comptime func: anytype,
    args: anytype,
) !std.meta.Child(std.meta.Child(@TypeOf(ptr_maybe))) {
    ptr_maybe.* = ptr_maybe.* orelse try @call(.auto, func, args);
    return ptr_maybe.*.?;
}

Well maybe it would look less bad if it had actually some checks about what you are passing in to it.

This function is actually growing on me, I like it better then the ?= operator idea, because that would mean that this operator does weird control flow stuff, instead we just call a function that hides some stuff which is still explicit because of try.

You can also create a version that just returns void if you don’t want to access the value immediately.
[/details]

You can’t pass a block into a function though, which limits its usefulness.

JavaScript has the ??= assignment operator, which does the same thing. Maybe Zig should just copy that syntax, replacing orelse with ??. I think it’s reasonable since the question mark is associated exclusively with optionals.

You still can upgrade the block to an actual function and then it works.

Zig doesn’t want to be a kitchen sink language.

This would be the first operator that conditionally evaluates one of its sub-expressions. I don’t think, just adding features from other languages for more terse code is that helpful, if it doesn’t fit the overall design.

It is also a local thing that you optimize, but why would you have these operators all over the place? (which would be a reason to make it more terse)

I think the code can just be rewritten to avoid needing the same pattern.
If something appears many times you probably can come up with a way to write it differently so that part doesn’t need to be restated over and over again.

I am happy that zig doesn’t strive to become the best code golf language.
I also don’t see a huge benefit in terse-ness, I think APL family languages are interesting (was trying to find a modern version of it I came across like a year ago, but can’t remember what its name was, it had an interesting interactive editor that allowed you to write the name of the function/operator and it would then change it to its glyph, basically teaching you the glyph meanings) and they are very terse.

Maybe we can start a brainstorming topic on expressions/functional-style etc.
Personally I find syntax more and more boring, I am more interested in interesting semantics that have a big impact on how you think about things. So I guess I am more interested in paradigms and computation models then syntax.

The discussions about syntax seem short term and surface level, when they are disconnected from a discussion about semantics and the computation model, the latter seems more important for coming up with a overall vision for the language and without that, the syntax suggestions aren’t grounded in something beyond personal preferences.

Your suggestion makes me wonder, whether you want more of a functional language, where you can build up bigger and bigger expressions with operators. I think zig actively tries to avoid/discourage going towards something like that, encouraging you to instead separate the pieces into separate lines, naming the intermediary results.

With something from the APL family, you go towards another extreme, you don’t even use named parameters anymore, instead you combine functions/operators together (although I guess there are ways to instead select things based on position, swap things around etc.).

Maybe something like this if we insist on using keyword:

ifnull object_maybe = try Object.create(allocator);

I don’t particular like multi-word keywords, but given that orelse already exists, adding a parallel one for assignment purpose doesn’t feel out of place.

The point here is simply to avoid having to write the lvalue twice, so that what actually get assigned to it doesn’t get pushed so far to the right.