-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
error sets #632
Comments
It would be handy for readability if these new keywords could be also written as |
Proposal iteration:
Remove the fn foo(x: i32) fail -> i32 {
if (x < 0)
return error.ExpectedPositiveNumber;
if (x % 2 == 0)
return error.ExpectedOddNumber;
return x - 1;
} Here, we introduce the
Now there is no more Now it becomes straightforward to allow multiple return values from a block: fn div(a: i32, b: i32) fail -> i32, i32 { ... } Now consider: var xxx: error = undefined;
fn bar() fail {
return xxx;
} This is a compile error; Zig cannot determine the set of possible error codes. To fix: var xxx: error = undefined;
fn bar() fail(OutOfMemory, SomeOtherError) {
return xxx;
} Now there is a runtime safety check that makes sure that You might want to make the set of possible errors depend on some other function: var xxx: error = undefined;
fn bar() fail(OutOfMemory, SomeOtherError, @errorsOf(foo)) {
return xxx;
} Now I'm thinking about how the |
+1 for separating the error channel from the return values. We may want to avoid conflating the terms "error" and "fail". This gets especially confusing when writing test harnesses, so it'd be nice to stick to just one term. Other languages use the term "exception" for this kind of thing, or even "signal". I think "error" is a good word for this. Thanks to I think the |
|
Other syntax which eliminates
If and only if compiler cannot deduce which error is returned it has to be supplied manually by the programmer. Compiler would then check for obvious mistakes. This moves the error list into the most relevant place. Local or non-local change do not affect function signature.
Pessimization (when in doubt label it as If there's annotation which may be useful and non-fragile it would be
|
Personally I prefer using sum types (was enum, now maybe enum struct?) to handle this:
In the calling code you would use switch to take apart the return type. The compiler can check all possible types returned from Note that this also covers the case that a function needs to return different types than just errors and one other type. The caller still has to This does not cover the !!/%% case but there is probably something to be done there that would be relatively simple. |
For someone who actually likes the % sigils and status-quo error handling, I quite like the direction that @andrewrk's proposal is going, and I agree with @thejoshwolfe that we should stick with one keyword. @thejoshwolfe: Could you elaborate on why you think it's particularly valuable to separate the return values from the error channel? On the other hand, I share @kyle-github's appreciation of sum types. The error enum concept is nice, and if the main objective is to remove the sigils, could just make them into actual keywords instead of changing the semantics of error handling. A proposal with keyword based syntax:
Remove Change Change Change
Providing a set of valid errors:
Basically, like in @kyle-github's example:
Now there is no more % to mean error, and everything still works the same way. |
I like @raulgrell's points here. I am also a fan of the existing There are a few things that are really nice about the current system:
So, how does the addition of these keywords and removal of Here is another approach to handling errors that is similar to what happens now, but also does not require extra sigils. First, we make
In this code, you simply use return as normal and the compiler figures out what you want to do. You do need to declare that you return an error or an i32. I use the When using this you get:
I am conflating a few idea I have with respect to defer here but they can be ignored for now. (I like the idea of providing the arguments to the deferred function so that you can easily implement things like error counters etc. OOB. This can also be extended to deferring a block instead of a function call though local functions would solve most of that too.) Or you could use switch:
This shows a couple of things I was thinking about. While I really like the short syntax of the error defers and returns, either you need special syntax to support it or you should drop down to using existing keywords and syntax. One thing I am not sure about is how to avoid magic in handling the types. In my examples there is an implicit cast of
But I am not sure I like that because it is somewhat hacky version of allowing multiple return values. Rust is approaching this problem by starting off with very few shortcuts and then adding them as the idioms become clear and accepted. It might make sense for Zig to do the same at first. Premature optimization is the root of all evil etc. |
Ideal error handling for me There is conceptually one global error variable in the program (well, one per thread). This sets it:
Its the simplest possible way. Why not make error handling similarly easy? There's no need for ceremony: the compiler always knows what errors can a function return (with some help in edge cases), and will always be able to check that all these errors are (somehow) covered.
|
I don't hate the % sigil, but if we are to ditch it, I prefer keywords that are easy to remember. Replace The mnemonic is: The thing inside parens is executed on error. Also, Java's checked exception is a controversial feature, and I don't want Zig to go that way. |
Proposal iteration:
const Errors = error {
OutOfMemory,
InvalidInput,
};
fn foo(x: u32) error -> u32 {
// This could be declared outside the function, or in, doesn't matter.
const Errors = error {
/// The input was the value 0, which is unsupported.
Zero,
/// Did you think 1 was ok? It's right out.
One,
};
if (x == 0)
return Errors.Zero;
if (x == 1)
return Errors.One;
return x - 2;
}
const Something = struct {
condition1: bool,
condition2: bool,
err: error, // Bad choice for the type of this field because now `bar`'s error set is the global error set.
};
fn bar(ptr: &Something) error -> u32 {
if (ptr.condition2) {
return ptr.err;
}
return 1234;
}
const Something = struct {
condition1: bool,
condition2: bool,
err: PossibleErrors, // better choice for the type of this field
};
const PossibleErrors = error {
OutOfMemory,
InvalidUserInput,
};
fn bar(ptr: &Something) error -> u32 {
const Errors = error {Condition1WasTriggered};
if (ptr.condition1) {
return Errors.Condition1WasTriggered;
}
if (ptr.condition2) {
return ptr.err;
}
return 1234;
}
// MorePossibleErrors is all the errors from PossibleErrors, all the errors foo can return, and Derp.
// @errors(x) returns the error set of x
const MorePossibleErrors = error(PossibleErrors, @errors(foo)) {
Derp,
};
const OtherErrorSet = error {
OutOfMemory,
Unique,
};
comptime {
assert(MorePossibleErrors.Derp == error.Derp);
assert(MorePossibleErrors.OutOfMemory == PossibleErrors.OutOfMemory);
assert(MorePossibleErrors.OutOfMemory == OtherErrorSet.OutOfMemory);
assert(MorePossibleErrors.Unique == OtherErrorSet.Unique); // compile error: error set MorePossibleErrors has no field Unique
}
fn foo(x: u32) error -> u32 {
const Fail = error {
/// The user specified numbers that are not allowed because they are too big.
InvalidInput,
};
if (x > @maxValue(u32) - 10) {
return Fail.InvalidInput;
}
return (try bar(x)) + 10;
}
fn bar(x: u32) error -> u32 {
const Fail = error {
/// The user specified the number 0 which is not allowed.
InvalidInput,
OutOfMemory,
};
if (x == 0) {
return Fail.InvalidInput;
}
return 1234;
} The documentation for the Note also that |
One more optional thing: Maybe make an error set with only 1 field implicitly cast to its value instantiation. So then you could do: fn foo() error {
return error{ItBroke};
} Which both creates the ItBroke error and returns it. This reduces overhead of using errors, making people more likely to use them. |
Another adjustment: I ran into an issue with syntax. We want function prototypes to be able to specify the error set, for example if you accept a function pointer: const ErrorSet = error { A, B };
fn foo(func: fn() ErrorSet -> i32) {
// the function passed can only return errors A and B
}; This means fn proto has an optional fn foo() {
} Is the Here's how I'm going to fix it, at least for now: // function that can return an error or an i32
// the error set is determined automatically by the compiler
fn foo() -> i32 !! {
}
// function that can return an error or void
// the error set is determined automatically by the compiler
fn foo() !! {
}
// function that returns void
fn foo() {
}
// function that must return an error in ErrorSet or void
const ErrorSet = error { A, B };
fn foo() !! ErrorSet {
}
// function that must return an error in ErrorSet or i32
const ErrorSet = error { A, B };
fn foo() -> i32 !! ErrorSet {
}
// function that can return i32 or any error in the entire global error set
fn foo() -> i32 !! error {
} This mirrors the way unwrapping a function call with an error works. |
I was actually thinking of a feature related to error sets. The idea was to be able to have error categories, so you could check if an error was part of a category like As far as i've read on other issues, we like to have non pseudo code examples if possible, so here is an example based on C code i've had to write before using SDL.
|
@Hejsil: would it be enough to have structured error names, like |
@PavelVozenilek What ever syntax works best, though idk if I want It could also be implemented in userspace if we are able to get a slice of all values in an enum/error set at runtime:
|
@Hejsil this I think we might add a more generic reflection function for that. Maybe Either way, we'll make sure that this use case is covered. Oh, and your userland solution would work, with the use of |
fn HashMap(comptime K: type, comptime V: type) -> type {
return struct {
const Self = this;
// ...
fn get(self: &const Self, key: K) -> %V {
if (something) return error.KeyNotFound; // supposed to be failure
var actual_value: V = something;
return actual_value; // supposed to be success
}
};
}
fn main() {
var posix_error_codes = HashMap(i32, error).init();
posix_error_codes.put(2, error.NotFound);
posix_error_codes.put(5, error.IoError);
// ...
var posix_error: error = posix_error_codes.get(errno) %% |err| error.Unknown;
} If your I propose we use a different keyword for returning something along the error channel. Instead of |
I am getting a bit confused (a normal state) by why this is being done... If you want to move toward multiple return values, then separating errors out a la Go makes more sense to me and fits with "one obvious way to do it." Then you can use
If you want to move toward something like a union, why not just use a union?
This feels a little like the "optimized" syntax is being discussed first without solving the underlying question of how errors need to be handled. Are errors integral values that are passed along a channel? Are they a sort of enum that is an alternate value (result is union-like)? Are they individual types in a union (also union-like but with the possibility of carrying more data)? @thejoshwolfe, thanks for the example with returning %error... Hmm... |
this will be fixed in a better way later by #632
I do not like the !! proposal. What makes it symmetrical, the object is different (value versus error type). Also you introduce it as a way to remove ambiguity between expression and function body, then add !! without error type which reintroduces the ambiguity. Kyle's _ proposal is nice. |
See #632 better fits the convention of using keywords for control flow
See #632 better fits the convention of using keywords for control flow
Is there a new Issue about failable functions? |
Sorry, it's a bit disorganized. Here's what is actually accepted in this issue:
Failable functions vs error union type I think will be a separate issue. I think this issue is complicated enough that I need to implement the stuff that I'm more confident about, and then see how it feels, and then iterate from there. Sorry for the instability. That's why we're only at 0.1.1. Some things, such as error sets, and concurrency, will be experimental for a little while longer while we narrow in on how to make zig the best language it can be. |
here is error sets as I plan to implement them: // void functions must be declared explicitly
fn foo() -> void {
}
// remove the -> since we always have return type
fn foo() void {
}
// error union looks like this
// this could be any error in the entire program
const x: error!i32 = 1234;
// declare an error set
const MyErrSet = error {OutOfMemory, FileNotFound};
// error union with an error set
const y: MyErrSet!i32 = 5678;
const z1: MyErrSet!i32 = MyErrSet.OutOfMemory;
// error set of size 1 implicitly casts to instance of
// any error set which contains it
const z2: MyErrSet!i32 = error{OutOfMemory};
// leave off the error set in a function return type to
// have it infer the error set
fn foo() !i32 {
// this declares the ItFailed error
return error{ItFailed};
}
// the error set of foo is foo.errors
const bar1: foo.errors!i32 = 42;
const bar2 = foo.errors.ItFailed;
// merge error sets
const ErrSetA = error{
/// ErrSetA doc comment
BadValue,
Accident,
};
const ErrSetB = error{
//// ErrSetB doc comment
BadValue,
Broken,
};
// doc comment of MergedErrSet.BadValue is "ErrSetA doc comment"
// MergedErrSet contains {BadValue, Accident, Broken}
const MergedErrSet = ErrSetA || ErrSetB; |
The purpose of this is: * Only one way to do things * Changing a function with void return type to return a possible error becomes a 1 character change, subtly encouraging people to use errors. See #632 Here are some imperfect sed commands for performing this update: remove arrow: ``` sed -i 's/\(\bfn\b.*\)-> /\1/g' $(find . -name "*.zig") ``` add void: ``` sed -i 's/\(\bfn\b.*\))\s*{/\1) void {/g' $(find ../ -name "*.zig") ``` Some cleanup may be necessary, but this should do the bulk of the work.
I think losing Also, error types before main return type seems visually wrong, too. And I guess multiple of my comments just emphasize that looking/working like other languages can be good when there's no strong reason to break from them. I very much agree that a syntax for inferring the error set would be good, if error sets really are needed at all, since you expect the compiler to report missed handling anyway. Generated docs would show them, too, I presume. |
Maybe |
Your suggestion of And on You also have a handful of cases where you return Anyway, I've spammed enough and I'm just some new guy here, so I better stop for now. |
@tjpalmer I think you raised some good points about syntax. Sounds like you're on board with the semantics. I'm in the middle of implementing this, and so I'll finish up and we can try out the semantics, and then make an adjustment to the syntax. A couple of things to consider:
|
Thanks for the reply. On semantics, are you okay with general union/sum types? I think going beyond just error types would be good here. General principles that happen to apply to errors would be nice. And you might be meaning this, but I'm not 100% sure. On syntax, understood for the spiral rule, which is why I figured a shortcut modifier would still be a prefix, as you suggest. For And still I recommend familiarity where possible. I don't ever use On set operators, bitwise operators are set operators. So, I'm not sure if intersection types have any place in Zig ever, but using the common notation for union types could be handy. And I don't expect sets in core Zig, either. I just made the point about sets (including in Python) to point out that this notion of bitwise operators for sets is fairly pervasive. Anyway, too much chatting on my part, again. |
In my comments, I was forgetting that types appear as first-class values in standard expressions at compile time in Zig, but I think the intent would still be clear and well-aligned with other languages. (As an aside, I'm here exploring Zig land because I think you've made a lot of great decisions. After years of not trying to make a language of my own, I'd started again recently, but it's hard to make a progress on spare time. I'm very impressed with what you've gotten done already, and I'm more interested in something in this space succeeding than in being the exact language I'd design myself. The only things I'm inclined to push here are those that I think can work with your existing vision.) |
Have you looked at how unions work? http://ziglang.org/documentation/master/#union
A precedent for |
I did see the union thing previously. I was imagining this as a slight variation on that but with the same underlying and compatible mechanism (and for more than errors). But I also understand that you try to have one way rather than multiple ways to say the same thing (whereas having both And understood on Anyway, thanks for reply again. |
A note about using You're absolutely right about bitwise As a bonus, As nice as this "double operator" pattern is, I'm not sure how far it will go beyond |
See #632 (comment) the current iteration of this proposal.
everybody hates %. what can we do about it?leave%
alone as far as types go.%T
is still how you make an error union on T.replace%%x
withtryfail x
replace%return x
withtryret x
replacea %% b
witha tryor b
replace%defer
withtrydefer
The text was updated successfully, but these errors were encountered: