#Is there any way to pass bound functions still?

1 messages ยท Page 1 of 1 (latest)

granite elk
#

E.g. I have some struct with a pub fn print(self: Self, comptime fmt: []const u8, args: anytype) void debug printing function

And I want to pass it into one or more fn some(whatever: Data, print: fn(fmt: []const u8, args: anytype) void) void pieces.

Looks like there used to be a BoundFn thing, but that's on the way out, but I haven't yet found mention of how to solve this use case without BoundFn?

robust fulcrum
#

that wouldn't have worked properly with a BoundFn as far as I'm aware

#

unless you mean you just wanted to clip off the first argument of the function implicitly

#

which also doesn't make a lot of sense, and it's not clear how that would work

granite elk
#

ahh no wonder the feature was dropped then, since function binding from other languages would imply it fixes the first argument... so is there a way to achieve function binding? in general I'm really struggling with zig whenever I try to pass functions around at all...

robust fulcrum
#

not really, functional style programming isn't very well suited to zig

#

it very much promotes imperative style programming

#

wherein, the closest you really get to "functional" programming is callbacks

#

the usual approach to allowing access to data from a callback is an erased context parameter (either statically, by using anytype, or runtime polymorphically, with *anyopaque)

#

an example of the former is std.sort.sort

granite elk
#

gotcha, in this case, what I'm trying to do is just pass around a generic "debug printer function", not really do fancy monad faffery ๐Ÿ˜‰

robust fulcrum
#

for what reason do you need the debug printer?

#

usually when one wants to pass around a way to write to some output stream in zig, you use a writer

#

as in, an instance of std.io.Writer

granite elk
#

yes, and how to pass those around has also currently escaped me... sometimes anytype works, sometimes it doesn't... I'm really yearning for some sort of interface / trait / vtable / whatever yall want to call "A thing with this method set as an abstraction" ๐Ÿ˜‰

robust fulcrum
#

so your some function would look like

fn some(whatever: Data, writer: anytype) void {
    writer.print(...) catch {}; // if you don't care about error handling
}
robust fulcrum
granite elk
#

yeah, that's probably where it blew up on me, when I tried to accept one in constructor and stash it into a struct field

robust fulcrum
#

this is because it's not actually a type

granite elk
#

then I have to decide if it's really worth it to make that struct a generic with a type-fn or not...

robust fulcrum
#

I mean, you don't necessarily need it to be returned from a function

granite elk
#

most time so far, I just give up and live without the observability

#

or add std.debug.print temporariily, then remove it later

#

but I'd rather leave the debug print code paths in, just branching on whether or not we have a printer r.n

robust fulcrum
#
fn doubleIt(foo: anytype) struct {
    a: @TypeOf(foo),
    b: @TypeOf(foo).
} {
    return .{ .a = foo, .b = foo };
}
granite elk
#

oh I see, interesting, haven't seen that kind of pattern in any other code so far

robust fulcrum
#

it's certainly strewn about the stdlib

#

in particular, std.mem.Allocator functions make use of the fact that types are just values, that can be calculated in arbitrary comptime expressions, to be able to do stuff like

pub fn realloc(self: Allocator, old_mem: anytype, new_n: usize) t: {
    const Slice = @typeInfo(@TypeOf(old_mem)).Pointer;
    break :t Error![]align(Slice.alignment) Slice.child;
} {
    return self.reallocAdvanced(old_mem, new_n, @returnAddress());
}
#

note the block, it returns a type, which is then the return type of the function

#

you can also see this in some other generic code working with readers and writers, that do e.g.
fn foo(writer: anytype) ?(@TypeOf(writer).Error || error{MyError})!void

#

once you understand and wield the power of types as values, you can start to create very expressive zig code

granite elk
#

speaking of fancy types, here's what I'm doing actually:

const Op = union(enum) {
    x: void,
    value: u32,
    add: [2]*@This(),
    mul: [2]*@This(),

    const Self = @This();

    pub fn eval(self: Self, x: u32) u32 {
        return switch (self) {
            .x => x,
            .value => |n| n,
            .add => |expr| expr[0].eval(x) + expr[1].eval(x),
            .mul => |expr| expr[0].eval(x) * expr[1].eval(x),
        };
    }

    // NOTE: implementing traced versions of eval() that print what they're doing to a debugging writer is what I'm doing
};

was actually really happy when I found out how to do self-referential types likes that union

robust fulcrum
granite elk
#

am already doing that by just alloc(*T, 2)ing it then casting the array back out

robust fulcrum
#

that sounds like you may be doing something wrong

#

you have an array, which is a value type

#

[2]T is not a pointer, it is just two Ts stacked on top of each other

granite elk
#

note the *, so it's a [2]*T

robust fulcrum
#

just meant generically

#

whether or not T is a pointer

#

so what you have right now is two pointers stacked on top of each other

#

and each one points to what can be a distinct object

#

that means you end up with quite a bit of fragmentation, depending on how deep your tree is

granite elk
#
            '*' => {
                if (prio > 1) return left;
                cur.i += 1;
                var right = try self.parseOpTerm(cur, 1);
                var legs = try allocator.alloc(Op, 2);
                legs[0] = left;
                legs[1] = right;
                return Op{ .mul = .{ &legs[0], &legs[1] } };
            },

is how its' being built right now fwiw, so allocate 2 values at a time, then stash the pointers to them in the parent

robust fulcrum
#

oh I see, interesting design choice

#

fair enough, that ought to be fine, though it is a bit odd. How do you free them?

granite elk
#

ideally I'd write more of a slab allocation deal, wouldn't actually be a zig std.mem.Allocator, so I hesitate to call it a slab allocator, but what it would do would be to allocate say 32 element chunks at a time, then hand them out one at a time

#

all of this lives in an arena so no need to traverse the object graph at destruction time ; that way lies insanity

robust fulcrum
#

fair enough

granite elk
#

everytime I use a std.TailQueue or SInglyLinkedList, I'm also yearning for that kind of slab allocation

robust fulcrum
#

though if you were to ever free them you'd have to do => |*expr| allocator.free(@ptrCast([*]@This(), expr)[0..2])

granite elk
#

seems so waseful to always be creating list nodes oneat a time

robust fulcrum
#

which is why instinctively, I would just start out doing try allocator.create([2]@This())

#

since the deallocation for that is just naturally allocator.destroy(expr)

robust fulcrum
granite elk
#

yep that's what tcmalloc and Go's allocator have acclimated me to expect long ago, but this is zig,now I get to care about where the bytes are! ๐Ÿ˜

robust fulcrum
#

lol for sure, but it doesn't hurt to have a couple comments like

/// This is function benefits from an allocator that uses fixed size classes
granite elk
#

hmm, everytime I try to put a triple-slash comment in, zls baps me on the head, so I've stopped trying ๐Ÿ˜‰

robust fulcrum
#

they're only allowed above declarations

#

so, never inside a function

granite elk
#

yeah that was the issue

robust fulcrum
#

and always above something that is declared const, var or fn

#

so e.g. not above a test or a comptime block

#

you can also attach them to parameters, and fields

#

aaand that's 'bout the gist of it

stable loom
granite elk
#

good qn, I've not even cracked the tin on std.log yet, good note