Simple function chaining

It would be a real shame if that happened, huh.

Ok. Uncle. I can’t see how to do it without ugly try-chaining or orelse-chaining or the likes. Perhaps something emerged in the try-chain vein and I’m just missing it… or perhaps I missed the inflection in your comment. And perhaps I’m not motivated to try hard enough because I decided to abandon the pattern. :slight_smile:

test "chaining" {
   const gpa = std.testing.allocator;
   const k = try gpa.create(K);
   defer gpa.destroy(k);
   _ = (try (try (try k.*.foo()).bar()).baz());
}

pub const K = struct {
   pub fn foo(self: *K) !*K {
      return self;
   }
   pub fn bar(self: *K) !*K {
      return self;
   }
   pub fn baz(self: *K) !*K {
      return self;
   }
};

edit:

Ah, I think this is your hint:

const thing = try e_thing.ok();

… that is, just store up the errors and then check the error-state at the end. Ug. No. Not interested. :slight_smile:

I think there’s no way to express this in Zig that’s both concise and elegant.

If you’re the one in charge of the implementation, you can use builder type(s) which is a union of success and errors but not an actual error union, with an expect function at the end of the chain.

const Builder = union(enum) {
    pass: Pass, 
    fail: Fail, 

    const Fail = FooError || BarError || BazError;

    pub fn foo(b: Builder) Builder {
        return switch (b) {
            .fail => |fail| .{ .fail = fail },
            .pass => |pass| if (pass.foo()) |yay|
                .{ .pass = yay }
            else |nay|
                .{ .fail = nay },
        };
    }

    pub fn bar(b: Builder) Builder {
        return switch (b) {
            .fail => |fail| .{ .fail = fail },
            .pass => |pass| if (pass.bar()) |yay|
                .{ .pass = yay }
            else |nay|
                .{ .fail = nay },
        };
    }

    pub fn baz(b: Builder) Builder {
        return switch (b) {
            .fail => |fail| .{ .fail = fail },
            .pass => |pass| if (pass.baz()) |yay|
                .{ .pass = yay }
            else |nay|
                .{ .fail = nay },
        };
    }

    pub fn expect(b: Builder) Fail!Pass {
        return switch (b) {
            .pass => |pass| pass,
            .fail => |fail| fail, 
        };
    } 
};

_ = try k.builder()
    .foo()
    .bar()
    .baz()
    .expect();

Concise and elegant for the user.

2 Likes

But if those methods can error they most likely also need to be cleaned up in some way. So you’d likely need to introduce some state that records, what is initialized and what failed, or where in the pipeline something failed.

Honestly I think that not being able to do function chaining “elegantly”(for whatever that means) with failing operations is a plus point of zig for me. Because code like this

var foo = try Foo.init();
try foo.addBar(bar);
try foo.addBarRasterizer(r);
try foo...

should look like this because it’s precisely doing just that. It’s easy to debug by adding intermediate logs/prints or setting a breakpoint in the debugger. You can add an errdefer inbetween if something needs to be cleaned up.

I’ve seen it often in (mostly Rust) codebases that method chains eventually will get broken to add some printing or something inbetween or because it just became unreadable. So I think it’s better to just not chain from the start.

1 Like

Possibly it is neither of those things, except maybe at the point of use.

const Builder = union(enum) {
     obj: *Foo,
     err: ErrorSet,

    pub fn create(allocator: Allocator) Builder {
         if (allocator.create(Foo)) |foo| {
             // Happy path
             return .{ .obj = foo };
         } else |err| { 
             // Sad path
            return .{ .err = err };
         }
    }

    pub fn build1(b: Builder, bar: []Bar) Builder {
        switch (b) {
            .err => return b,
            .obj => |foo| {
                   // build
                   // Remember to free any
                   // foo'ish resources if anything
                   // else fails!
                  foo.bar = bar;
                  return .{ .obj = foo };
            },
        }
    }

    pub fn ok(b: Builder) ErrSet!*Foo {
        return switch(b) {
            .err => |e| e,
            .obj => |foo| foo,
        };
    }
}

Hence “railroad pattern”: if there are failures, the logic shunts over to just conveying the errors forward, after cleaning everything up. Otherwise it keeps chugging down the tracks.

Worth it? I dunno, maybe. Personally I’ve yet to write Zig this way. But it’s there if you need it.

Yes, and I’m pretty sure this is what @mnemnion was pointing to… but holding error as state and requiring the final .expect() is not attractive to me. I might be converted with more thought, but… But I appreciate that this is a way to do it “more elegantly” than chaining try’s. Also, this is NOT a downer for me; I may conclude that exceptions, rather than error return values, would give me what I want, but I would NOT want zig to implement exceptions so that I could have what I want.

By the way, when I looked into it a little more, I saw “railway” (as opposed to “railroad”) used more(?) often, fwiw.

This seems “elegant enough”… and, agreed: zig doesn’t make it impossible (or even ugly on the interface, as long as you don’t mind a little ugliness in the implementation)… so, no shutdown. But still not super appealing to me. I do see that the final expect() wouldn’t necessarily be required - you could treat the chain like a “normal” (singular) function call and handle the final result (/error) in an unassuming way (even if not the normal !Thing way). The only real awkwardness, then, is the extra scaffolding in the innards, which could also be somewhat abstracted out. So. Yeah. Meh. But better than I anticipated in my OP, here at the end of the journey.

I am not British, as it happens.

1 Like

:slight_smile: and I only mentioned the afterthought for reference/posterity purposes. (I’m not even Canadian.)

1 Like