Skip to content

Proposal/discussion: Decide on a preference for generic functions #522

@snocl

Description

@snocl

Functions taking generic arguments can be implicitly or duckly typed, taking var and maybe making use of @typeOf:

math.max(a: var, b: var) -> @typeOf(a + b)
mem.writeInt(buf: []u8, value: var, big_endian: bool)

Or they can be explicitly typed, taking the type as an argument:

mem.max(comptime T: type, slice: []const T) -> T
endian.swap(comptime T: type, x: T) -> T

That there are these two different ways to do this seems to go against the Zen of Zig, in particular “Only one obvious way to do things”.

Proposal 1: Ban/disfavor var

For the callee, a var is essentially just a type and a value bundled together, so changing a function to take each separately is usually trivial (there might be an issue with passing integer literals, but I don't believe it would be a big problem to reify them to a concrete integer type instead).

For the caller, things are different: we introduce an entire new argument! This is a loss of ergonomics, but maybe a win in clarity?

Some pain points would be functions generic over multiple types that usually, but not always, are the same, e.g.:

math.max(comptime T: type, a: T, comptime U: type, b: U) -> ???

I’m not sure this is sufficiently better than using

math.max(comptime T: type, a: T, b: T) -> T

and having the arguments be cast to a parent/wrapper type, though.

Functions taking ... arguments would also need to pass the type on to its called functions, but I don't think this would be much of a problem in practice.

Proposal 2: Embrace var

Use var everywhere. This would mean rewriting the above functions to:

mem.max(slice: var) -> @typeOf(slice).Child
endian.swap(x: var) -> @typeOf(x)

This will certainly make the functions more ergonomic to call, but there are two major problems:

  • Using var essentially bypasses the explicit type system. In particular, it's now impossible to declare mem.max's slice to be a slice; you'll have to rely on duckly typing or casts/assertions in the function body itself.

  • The error messages will generally be less helpful, since it's impossible to type-check arguments up front.

As a very minor point, this is equally unergonomic as Proposal 1 when passing integer literals to functions:

endian.swap(i32, 1)
endian.swap(i32(1))

In addition, some functions will still require an explicit type to be passed:

mem.readInt(bytes: []const u8, comptime T: type, big_endian: bool) -> T
ArrayList(comptime T: type) -> type

Proposal 3: Compromise, somehow

Embrace that some kinds of functions should take explicit types, while others should take implicit types, essentially separating function-like functions from macro-like functions. I can't think of a good metric for what functions would fit in which categories.


I’m partial towards Proposal 1, but am aware it might be the most extreme of the three.

I think it’s important to decide on a policy; not necessarily one of these three. If I’m misunderstanding something, or I seem confusing/confused, please let me know!

I’m very curious to see how the language will evolve. :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions