Skip to content

Callable structs #19025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Jarred-Sumner opened this issue Feb 21, 2024 · 2 comments
Closed

Callable structs #19025

Jarred-Sumner opened this issue Feb 21, 2024 · 2 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.

Comments

@Jarred-Sumner
Copy link
Contributor

Jarred-Sumner commented Feb 21, 2024

Say you have a lot of platform-specific code (like for supporting windows and posix), which frequently have different return types based on the platform. You want to use the return type optimized for the platform (such as [:0]const u16 on Windows and [:0]const u8 on posix). Ideally, the platform-specific differences are handled by default by the platform-specific functions instead of the callers of the functions (reducing complexity and making it more maintainable)

What are the options today?

Use the same type for all platforms

example: std.os.open

pub fn open(file_path: []const u8, flags: u32, perm: mode_t) OpenError!fd_t 

Then, when you want the more specific type, suffix the function with the type name at the end

pub fn openZ(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t 
pub fn openW(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t 

This means that on Windows, open must convert to []const u16 on every call to open - which is lossy (utf8 != utf16) + costs performance & memory, and it's ambiguous what exactly is the generic type in use without going through @typeInfo on the open fn

Use a wrapper struct

This might look something like:

pub const open = struct { 
  pub const T = if (isWindows) u16 else u8;

   pub fn path(file_path: []const T, flags: u32, perm: mode_t) OpenError!fd_t { // ... }
   pub fn Z(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t { // ... }
   pub fn W(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t  { // ... }
};

test {
   _ = try open.path("foo", 0, 0);
   _ = try open.Z("foo", 0, 0);
   _ = try open.W(&.{'f', 'o', 'o' }, 0, 0);
}

But, this doesn't make the code easier to read. open.path obscures the intent. Does that mean Z and W are not opening the path?

You could also use a comptime fn that returns a new type, but that ends up looking pretty similar to the wrapper struct approach

Proposal: callable structs

One idea: if a struct defines a function called call, that struct becomes callable like a function

pub const open = struct  { 
  pub const T = if (isWindows) u16 else u8;
   
   // open(".", 0, 0) is the equivalent of open.call(".", 0, 0)
   pub fn call(file_path: []const T, flags: u32, perm: mode_t) OpenError!fd_t { 
          // ... 
   }

   pub fn Z(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t {  // ... }
   pub fn W(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t  { // ... }
};

test {
   // equivalent of open.call("foo", 0, 0);
   _ = try open("foo", 0, 0);
   _ = try open.Z("foo", 0, 0);
   _ = try open.W(&.{'f', 'o', 'o' }, 0, 0);
}

This lets us avoid expensive, buggy conversions without significantly complicating the codebase, and the expected type is unambiguous by convention: open.T.

Alternative syntax

Another idea: specify the function to call in the struct definition, similar to packed struct except when not packed, its the function to call:

pub const open = struct (myFnNameHere)  { 
  pub const T = if (isWindows) u16 else u8;
   
   // open(".", 0, 0) is the equivalent of open.myFnNameHere(".", 0, 0)
   fn myFnNameHere(file_path: []const T, flags: u32, perm: mode_t) OpenError!fd_t { 
          // ... 
   }

   pub fn Z(file_path: [*:0]const u8, flags: u32, perm: mode_t) OpenError!fd_t {  // ... }
   pub fn W(file_path_w: []const u16, flags: u32, perm: mode_t) OpenError!fd_t  { // ... }
};

test {
   // equivalent of open.myFnNameHere("foo", 0, 0);
   _ = try open.myFnNameHere("foo", 0, 0);
   _ = try open.Z("foo", 0, 0);
   _ = try open.W(&.{'f', 'o', 'o' }, 0, 0);
}
@squeek502
Copy link
Collaborator

squeek502 commented Feb 21, 2024

Just so I understand, how is this:

   _ = try open.path("foo", 0, 0);

or this:

   _ = try open("foo", 0, 0);

not a compile error on Windows? "foo" doesn't coerce to []const u16


Side note only relevant to the motivation, not the proposal itself: you might be interested in #19005

@squeek502 squeek502 added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Feb 21, 2024
@castholm
Copy link
Contributor

Maybe I'm missing something, but I fail to see how the example use case motivates the need for callable structs. It seems like you want to expose a single function that takes/returns types that are optimal for the target OS. Wouldn't something like this accomplish what you are asking for?

pub const PathString = if (builtin.os.tag == .windows) PathStringW else PathStringZ;
pub const PathStringZ = [:0]const u8;
pub const PathStringW = [:0]const u16;

pub fn open(path: PathString, flags: u32, perm: mode_t) OpenError!fd_t {
    return if (builtin.os.tag == .windows)
        openW(path, flags, perm)
    else
        openZ(path, flags, perm);
}

pub fn openZ(path: PathStringZ, flags: u32, perm: mode_t) OpenError!fd_t {}
pub fn openW(path: PathStringW, flags: u32, perm: mode_t) OpenError!fd_t {}

test {
    _ = try open("foo", 0, 0); // ignoring that this doesn't work on Windows for the reasons outlined above
    _ = try openZ("foo", 0, 0);
    _ = try openW(&.{ 'f', 'o', 'o' }, 0, 0);
}

@Jarred-Sumner Jarred-Sumner closed this as not planned Won't fix, can't repro, duplicate, stale Feb 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

3 participants