Here's an example that compares them: https://pkg.go.dev/braces.dev/errtrace#readme-comparison-wit...
Since the HTTP library uses a separate goroutine to create connections, the stack trace at creation time doesn't have details about the user request that triggered the connection.
I also felt that Go errors where too bare-bones, so I developed a small package (https://github.com/Vanclief/ez) based on an awesome post that I saw here once. I use this package in all Golang code I touch.
I guess the difference we try to make is that we really wanted to make errors that are understandable by users. Each time the error is returned we try to wrap it with an information where and why.
I've asked in their Discord, Andrew Kelley himself passed on commenting (I know his stance, every C++ dev wants their fav feature), but the reality remains that it's just infeasible to do with a DSL so it's just the wrong language for writing graphics code.
It does seem that tensors are one of the core abstractions for modern ML systems. I've heard people joke that AI is just matrix multiplication. Python is such a flexible language that creating abstractions around its syntax was super easy. I believe that was part of the reason Python has come to dominate this space (not the only reason obviously).
I too felt the same as you, but as a distant admirer of Zig. I totally understand that operator overloading can be misused. But I have an intuition that at least providing operators for linear algebra (and probably complex/quaternion) is a really important feature for any languages from this point in history going forward.
1. https://www.youtube.com/watch?v=SEwTjZvy8vw&ab_channel=LLVM
providing operators for linear algebra (and probably complex/quaternion) is a really important feature for any languages from this point in history going forward.
This is why I have started using Fortran for writing AI model inference code. It natively handles array manipulation like python and has a `matmul` intrinsic and compiles into fast code. There are some rough edges, but it's great as a low level matrix programming language which was its original point.Not sure where the joke is, today's deep learning based AI models are literally matrix multiplications with learned weights.
I'm still a happy Zig user though, and hey, there's still time before 1.0.
Seems like there is a bunch of interesting low level languages gaining steam at the moment.
Or is it that the three years it’s been around indicate it will never progress?
Also it seems like even the discussion of Geometric Algebra has been covered before 3 years ago: https://github.com/ziglang/zig/issues/7295#issuecomment-7389...
Unless it's actually possible after all this time (even in some random beta) to do vector code in infix notation, it doesn't feel like it's going to happen :(
Zig won't ship with llvm as part of the standard download, but i imagine it will be easy to get zig+llvm working
a #everything_until_the_next_space b is rewritten in #everything_until_the_next_space(a,b)
So you'd have:
-a #foo b #foo c is foo(foo(a,b),c)
-a #foo b #bar c is forbidden, use parenthesis.
-and maybe #: for the inverse composition where a #:foo b #:foo c is foo(a, foo(b,c))
It's explicit, no hidden overloading, yet it's "short" enough to be usable: (a #* b) #+ c isn't that much worse than a*b+c and is much more readable than madd(mmul(a,b),c)..
If there is no implicit conversion name clashes shouldn't be too bad (both #+ could be usable for matrix and for graphic libraries).
pkg/errors captures a stack trace of when the error occurred, and attaches it to the error. This information doesn't change as the error moves through the program.
errtrace captures a 'return trace'--every 'return' statement that the error passes through. This information is appended to at each return site.
This gives you a different view of the code path: the stack trace is the path that led to the error, while the return trace is the path that the error took to get to the user.
The difference is significant because in Go, errors are just plain values that you can store in a struct, pass between goroutines etc. When the error passes to another goroutine, the stack trace from the original goroutine can become less useful in debugging the root cause of the error.
As an example, the Try it out section (https://github.com/bracesdev/errtrace/#try-it-out) in the README includes an example of a semi-realistic program comparing the stack trace and the return trace for the same failure.
>This repository has been archived by the owner on Dec 1, 2021. It is now read-only.
It's largely complete so it is essentially fine at the moment, but it won't be adapted to future language or community changes. A future landmine.
As far as error handling is concerned, errors as values is the modern thinking. Go is not behind the times here. If you squint, the `(T, error)` return type is very similar to Rust's `Result`, and the `if err != nil` idiom is basically Monadic control flow.
This requires the kind of squinting where 9 x 9 = 81 is basically the same as 9 + 9 = 18 right? I mean, they're roughly the same symbols, albeit one at slightly different angle, and in a different order...
Result is a sum type, as are a lot of key things in Rust. Take the Rust type Result<bool,()> - this has three possible values, Ok(true), Ok(false), Err. The analogous Go product type has four possible values, (false,false) (false, true), (true, false) and (true, true).
I can add stack traces and raise panics all day long in my code and it will never help me trace a deep error in my system. The collective blindness to that in the go world is staggering.
An idiomatic Go function will ensure that T is always useful, regardless of the error state. At very least it will return the zero value for T, to which the Go maxim states: Make the zero value useful. From the perspective of writing the function returning (T, error), there will always be four states, with T and error being independent of each other. Anything else is faulty design.
Unfortunately, some early Go code written before things were well understood left T to be undefined given certain error states, so one cannot assume that T will be valid in all cases for all functions. This does mean that the caller's perspective can only reliably consider three states absent of diving deeper (e.g. reading the documentation).
By 'implicit' I assume you mean 'by convention'? I say this because unless I've misread the go spec (and that's a distinct possibility), function returns are either a single value or a tuple and there is no specification on tuple returns and mutually exclusive values.
FWIW, I mostly like go and work with it practically every day. I absolutely loathe it's ideas on error handling. I have quite strong feelings on the subject that aren't fit for polite discussion.
Its idea is simply that error is just state like any other. I find that to be quite reasonable – and something I miss dearly when I work in other languages which try to get overly fancy to try and hide that fact. There is nothing special about errors. The machine certainly has no concept of errors. Why does it need special constructs?
Bad practices like assuming T and error are dependent do break the ideas, but a language can only hold the hands of poor developers so much. Someone determined enough to write bad code will do so in any language.
Realistically, if there is a handling problem, it is a problem for all values of all kinds. Every single problem one can point to about handling errors is also a problem when handing names, email addresses, random numbers, etc. To single it out as an error handling problem specifically is flawed.
> I have quite strong feelings on the subject that aren't fit for polite discussion.
I'd love to hear more. You won't hurt my feelings. Getting worked up about a programming language discussion is illogical.
tbh I'm not sure what the current popular option is for wrapping with stack traces.
Re join: it isn't a joiner-error, it has no need to do anything for that. Just stdlib-join-then-wrap.
"You can build X by hand too" has little to do with why people choose to create or use libraries.
C# has operator overloading and during my whole career I have never seen it abused so hard that people needed to ban it, let alone write guidelines and a lot of shops adopting it.
I barely see anyone use it not for really good cases like graphics.
The only interesting case was using "/" operator for Path Combines so "home" / "folder1" performs Path.Combine("home", "folder1")
but still, that was PoC or lib, not even prod.
So, is it about community, some culture or actually what?
C++ also allows you to overload short-circuiting operators, but of course your overload can't short-circuit so you just silently destroyed an important feature. Why ?
As others have pointed out, several languages have been able to provide this feature without causing half the mess and disappointment. Ten years ago if you said move assignment semantics are a bad idea you might persuade people because the C++ move semantics are messy, but hey, turns out a fresh language is able to just provide the destructive move developers actually wanted (but couldn't pull off for C++) and that's really nice.
cout << "foo" << endl;
This isn't a pedantic nerd snipe; the point is that operator overloading really is indispensable in certain contexts.
In my experience, "you can build X by hand" is the Go community's preferred approach.
Sure, product types are the wrong shape for several different problems, not just error handling. Go doesn't have any other shape of user defined types so... too bad you're just stuck with something the wrong shape. Errors are an example where it's more obvious to more people.
Also the machine actually does know about errors, for example the x86-64 architecture defines several kinds of faults, for which provided handlers will be executed, Linux actually deliberately makes it impossible to run such fault handlers after any other way to reboot has proved ineffective, then trips a fault, because it knows an x86 CPU which can't run the fault handlers despite a fault will give up and reboot, which is what Linux was trying to achieve anyway. ARM has error interrupts, POWER has layers of nested error handling.
While that is true, the shape has no bearing on the handling problem. Go could have every type shape-defining feature ever conceived and it would still have the very same handling problem – for errors and everything else.
> Errors are an example where it's more obvious to more people.
In my experience, the vast majority of bugs I encounter in the real world are related to the handling problem of non-error values. That is where it is most obvious, and not just in Go. I expect errors only get talked about because it is fashionable to repeat what someone else said, not because anyone put any thought into it.
> Also the machine actually does know about error
Only within the confines of one human interpretation of how the machine functions, just as is the case for code. The machine itself has no such concept. It doesn't have awareness that a given state is an error or a rainbow.
https://en.wikipedia.org/wiki/Computational_complexity_of_ma...
You don't want to know what the innocent-looking `*` in this function compiles to:
fn square(num: u10000) u10000 {
return num * num;
}But that's a strained interpretation of complexity, and not very useful.
And yeah, I get that Add(Add(Add(a, b), Mul(c, 3)), d) is possible, but come on... imagine if you had to write your normal "a + b + c * 3 + d" with ints/floats like that! What's that, suddenly people care, but nobody cares if it's not their field...
Whatever, I will continue to look longingly at Zig for all the bits of C/C++ (and apparently Go, to try bring it back to the original topic) it solves, but missing the trivial and absolutely critical single feature to enable an entire class of performance-critical programming.
u10000 exists only because we want u3, u7, and u13 as builtin types.
u3, u7, and u13 are useful for embedded and systems programming.
You don't need them as a built-in type to write a compiler. They're there because LLVM was the original backend and you essentially get them for free (in the sense that the backend code generation is already handled for you, so why not include them).
Zig does have builtin vec2f, it is spelt @Vector(2, f32).
How is that different from say, a function call? A function may look like a single O(1) operation from the input/output/name, but actually do something much more complex. That seems like the same thing to me, and very common. (and frankly I'm not sure that could even be avoided)
Perhaps I'll rephrase how I understand the philosophy: if it's a function call, it should look like a function call. Operator overloading breaks that.
That said, this isn't my hill to die on.
Edit to clarify my final sentence there: I have zero interest in debating this any further. Pixelpoet, if you're going to be so fussy, go read Harvey and van der Hoeven and stop trying to win language fights, they're tedious.
The ask isn't for general operator overloading (I'm also in favour of not having that), rather just not stopping native algebraic type support at scalars; the C function atan2(y, x) basically just wants to give you the complex argument, for example. Really it would just do so much to unify and simplify everything, besides being able to write vector stuff in a sane way; if every rando has to write their own vector and complex number classes, I'm much less likely to vouch for its correctness.
[0] I've recently been looking into Karatsuba multiplication to reduce it from O(N^2) to O(N^1.58): https://en.wikipedia.org/wiki/Karatsuba_algorithm