In a PR comment, it was mentioned that there are some design issues with jsonParse (and friends) that are difficult to solve. Namely, that the presence/absence of an arbitrary method silently change the behavior of an API.
For instance, if I just happened to add a jsonParse(...) decl to a struct, std.json would try to use it whether I intended it to or not. Additionally if I accidentally typo’d jsonPears(...), it would silently fail – the code would still compile, but std.json wouldn’t use my jsonPears(...) function.
I was thinking about how other languages solve this problem. Thinking to high-level languages like Java and JavaScript, they both have great ways of tackling this problem. Java allows classes to implement interfaces (therefore tightly-coupling the class to the gson package, for example), and JavaScript uses a Symbol datatype, which must be used from the variable on which it’s defined (or a copy of it), i.e. to make a value iterable, you’d add obj[Symbol.iterator] = myIterFunction to the object. There is no other way to gain access to Symbol.iterator other than through Symbol.iterator, or some copy of it.
I actually really like JavaScript’s solution. We can mix the solutions that both Java and JavaScript use, allowing us to tightly couple a jsonParse function to the std.json package, without complex language features like nominally-typed interfaces. The way I’d do this in Zig (as mentioned in this comment) is something like the following:
// std/json.zig
pub const parseFn = @symbol();
pub fn innerParse(...) {
// ...
if (std.meta.hasSymbolDecl(T, parseFn)) {
const parseDecl = @field(T, parseFn);
return try parseDecl(...);
}
// ...
}
// main.zig
pub const Foo = struct {
foo: i32,
pub fn [std.json.parseFn](...) !Foo {
return .{ .foo = 5 };
}
};
This would force the Foo struct to be tightly coupled to std.json, having a few great benefits. No more duck-typing on std.json’s side – now std.json can be confident that Foo is meant to be parsed. Additionally, readers of Foo know that jsonParse isn’t just some random function, they know it’s a special function which is used in some way by std.json. Additionally, since there’s an explicit identifier being exported, there’s a great spot for parseFn documentation to go, rather than people having to somehow guess that the documentation for jsonParse is located at std.json.parse.
And while a language feature, it’s not an overly-difficult one to understand with some basic rules:
- No two instantiations of
@symbol()are==to each other. (i.e.,assert(@symbol() != @symbol()) - A copy of a symbol is
==to its original. (i.e.,const a = @symbol(); const b = a; assert(a == b); - Symbols must be able to be used as decls (likely the most complex part of the feature).
- Syntax can be anything, but here’s just some brainstorming syntax for now.
- Declaring a function decl:
pub fn [someSymbol](...) ReturnType {} - Declaring a const decl:
pub const [someSymbol] = value; - Getting the value of a decl: I think reusing
@field(T, someSymbol)is fine for this
Feedback is welcome! This proposal was originally an offshoot of a stdlib proposal I was making, so it’s still pretty rough and I’m sure there’s some drawbacks (other than “language change”) that I’m not thinking of.