Newbie: How to deal with a !? type?

Hello,

I am trying to see how one deals with a value of !? type. Tried to find an example with no success. Initially I started with this very basic example:

    const a: ?f32 = null;
    const fallback_value: f32 = 0;
    const b = a orelse fallback_value;
    try expect(b == 0);
    try expect(@TypeOf(b) == f32);

Works as expected. Later I tried the following:


pub const Save = struct {
    lives: u8,
    level: u16,

    pub fn loadLast() !?Save {
        //todo
        // return null;
        // return .{
        //     .lives = 30,
        //     .level = 100,
        // };
        return C.NotDir;
    }

    pub fn blank() Save {
        return .{
            .lives = 3,
            .level = 1,
        };
    }
};

const OpenError = error{
    AccessDenied,
    NotFound,
};

const A = error{ NotDir, PathNotFound };
const B = error{ OutOfMemory, PathNotFound };
const C = A || B;
pub fn main() !u8 {
...
    {
        const save: Save = try Save.loadLast() orelse Save.blank();
        std.debug.print("{any}\n", .{save});
    }
...
}

I expected the output of blank() but get error: NotDir printed to console. Using:

        const save: Save = (try Save.loadLast() orelse Save.blank());

and

        const save: Save = (try Save.loadLast()) orelse Save.blank());

produces the same results. I also tried to reduce this to a minimal example with:

    const a1: !?f32 = null;
    const fallback_value1: f32 = 0.1;
    const b1 = a1 orelse fallback_value1;
    try expect(b1 == 0.1);
    try expect(@TypeOf(b) == f32);

but compilation fails with:

src/example_5.zig:186:15: error: expected type expression, found '!'
    const a1: !?f32 = null;

So I have 2 questions. First, what is the idiomatic way of dealing with !? types? Second, how can one declare a type !?f32 to make the last experiment work?

TIA

! is a error union, ? is a optional.
You have a error union which payload is a optional.

I think what you want to do is not return ? at all, but rather only !Save and do this:

const save: Save = Save.loadLast() catch Save.blank();

If you really want the optional Save, then you have to handle the error and unwrapping of the optional separately.

    const a1: anyerror!?f32 = null;
    const fallback_value1: f32 = 0.1;
    const b1 = a1 catch null orelse fallback_value1;
    try expect(b1 == 0.1);
    try expect(@TypeOf(b1) == f32);

First, values have to have specified error sets. Functions can infer them, but values cannot. catch is essentially the null unwrap for error expressions, so chaining them like this will give you the fallback value for either null or error.

2 Likes

There is also if (foo) |unwrapped| { ... } else |err| { ... } for error unions and if (bar) |unwrapped| { ... } else { ... } for optionals if you need more flexibility.

@Cloudef The goal here is to learn how to handle such a situation. I have seen mention of the !? and assumed this should be easily handled. Naturally I would avoid such a situation if possible as you have suggested. Thanks for the suggestions.

@Zambyte Ok, I have tested this and it works. And so does:

    const fallback_value1: f32 = 0.1;
    const a2: anyerror!?f32 = null;
    const b2: f32 = try a2 orelse fallback_value1;
    try expect(b2 == 0.1);
    try expect(@TypeOf(b2) == f32);
    std.debug.print("a2 = {!?}, b2 = {d}\n", .{ a2, b2 });

So why does this not work as in my first example? Maybe something to do with type inference of the function return type?

@Justus2308 Something seems amiss here. The need for double unwrapping seems to make sense, however the example above seems to show otherwise. In addition to this I could not get the following to work:

    const fallback_value2: f32 = 0.2;
    var a3: anyerror!?f32 = undefined;
    a3 = null;
    const b3: f32 = if (a2) |n| {
        if (n) |v| {
            v;
        } else {
            fallback_value2;
        }
    } else |_| {
        fallback_value1;
    };
    try expect(b3 == 0.1);
    try expect(@TypeOf(b3) == f32);

In this case I get the error:

src/example_5.zig:232:13: error: value of type 'f32' ignored
            fallback_value2;
            ^~~~~~~~~~~~~~~
src/example_5.zig:232:13: note: all non-void values must be used
src/example_5.zig:232:13: note: to discard the value, assign it to '_'

I must be missing something fundamental on how captures work.

I’m not actually sure about the first example. I do want to add for the iff you: your if is using the statement syntax instead of the expression syntax, which is why it’s saying the value is ignored (you have a statement that is just referencing a variable and not doing anything with it). You can either use the expression syntax like this:

const fallback_value2: f32 = 0.2;
var a3: anyerror!?f32 = undefined;
a3 = null;
const b3: f32 = if (a3) |n|
    if (n) |v|
        v
    else
        fallback_value2
else |_|
    fallback_value2;
try expect(b3 == 0.2);
try expect(@TypeOf(b3) == f32);

Or use named blocks to break with the value using if statements like this:

const fallback_value2: f32 = 0.2;
var a3: anyerror!?f32 = undefined;
a3 = null;
const b3: f32 = blk: {
    if (a3) |n| {
        if (n) |v| {
            break :blk v;
        } else {
            break :blk fallback_value2;
        }
    } else |_| {
        break :blk fallback_value2;
    }
};
try expect(b3 == 0.2);
try expect(@TypeOf(b3) == f32);
3 Likes

Thank you. I was incorrectly assuming all if’s were expressions.

You were correct in assuming all if statements are expressions, it’s just that using a block as an if statement’s expression will cause the if statement to evaluate to the block’s value, which is void by default. Try the following:

const swag = if (true) {
    // put any code here
};
std.debug.print("{}\n", .{swag});

This will always print void.

However, if you change the code to this, it will print 123:

const swag = if (true) blk: {
    break :blk 123;
};
std.debug.print("{}\n", .{swag});
3 Likes

@milogreg Coming from Scala, this feels strange. I have looked at 3 tutorials and this was not mentioned. I now understand @Zambyte’s last example. Thank you.

Just to put this to rest. My problem was that I was using a:

    const b2: f32 = try a2 orelse fallback_value1;

with a2 as an error, the test that followed was not executed. I tried to use the parenthesis incorrectly. The problem is that orelse only unwraps optionals, and try, catch and else |_| work only on error sets. The if |_| is the only one that can unwrap either (which confused me). The line above first unwraps errors and then optionals.

So the code above never reached the orelse when a2 was an error. When a2 is a null, the try returns the optional and orelse does the “optional” unwrapping.

Appreciate everyone’s’ feedback.

2 Likes