1. 2

    One should not only focus on what errors a language allows in theory but what errors people using the language make in practice.

    Obviously, if you can prevent errors at compile time without making the language more complicated, that’s the best solution but usually there are costs. And that you have to think about the trade off.

    I didn’t even program golang much and the error ignoring code still stands out. Just because other languages have exceptions, it doesn’t mean that a minimally trained golang developer would ignore this. I didn’t continue reading after that point.

    1. 1

      Yea, someone not checking or ignoring an error (res, _) will stick out like a sore thumb after a few weeks with Go.

      No such luck with Exceptions getting thrown around, maybe someone catches them, maybe they won’t.

    1. 4

      I’ve written some Go and some Rust. I feel like I usually enjoy Rust more, though I struggle to explain why.

      I think, for Rust, I find the error handling really ergonomic. Using ? in a function that does a bunch of things that can fail is just so much nicer than having every other line be a if err == nil { return err }. I also find it easier to follow how references work in Rust, oddly enough maybe. And using modules through Cargo is just so nice, while Go modules is kind of a messy hack in comparison. Oh and the macros are just so nice too.

      But on Go’s side, Go concurrency is really awesome and smooth, especially compared to the half-complete hacks that are tokio and the Rust async system. Did I mention how nice the built-in channels are, and how a bunch of places in the standard lib use them? And easy cross-compilation is pretty nice too. And you gotta love that massive standard library. And I suppose not having to wrestle with complex too-clever generic hierarchies is nice sometimes too.

      1. 16

        side-note: i think it’s a bit off-topic (and meme-y, rust strike force, etc. :) to compare to rust when the article only speaks of go :)

        Using ? in a function that does a bunch of things that can fail is just so much nicer than having every other line be a if err == nil { return err }.

        i really like the explicit error handling in go and that there usually is only one control flow (if we ignore “recover”). i guess that’s my favorite go-feature: i don’t have to think hard about things when i read them. it’s a bit verbose, but that’s a trade-off i’m happy to make.

        1. 7

          i really like the explicit error handling in go

          I would argue that Go’s model of error handling is a lot less explicit than Rust’s - even if Go’s is more verbose and perhaps visually noticeable, Rust forces you to handle errors in a way that Go doesn’t.

          1. 1

            I have just read up un rusts error handling, it seems to be rather simila, except that return types and errors are put together as “result”: https://doc.rust-lang.org/book/ch09-00-error-handling.html

            my two cents: i like that i’m not forced to do things in go, but missing error handling sticks out as it is unusual to just drop errors.

            1. 4

              Well since it’s a result, you have to manually unwrap it before you can access the value, and that forces you to handle the error. In Go, you can forget to check err for nil, and unless err goes unused in that scope, you’ll end up using the zero value instead of handling the error.

              1. 1

                i like that i’m not forced to do things in go, but missing error handling sticks out as it is unusual to just drop errors

                The thing is, while it may be unusual in Go, it’s impossible to “just drop errors” in Rust. It’s easy to unwrap them explicitly if needed, but that’s exactly my point: it’s very explicit.

            2. 3

              The explicit error handling is Very Visible, and thus it sticks out like a sore thumb when it’s missing. This usually results in better code quality in my experience.

              1. 2

                It did occur to me that it may come off like that :D It’s harder to make interesting statements about a language without comparing it to its peers.

                IMO, Rust and Go being rather different languages with different trade-offs that are competing for about the same space almost invites comparisons between them. Kind of like how temping it is to write comparisons between Ruby, Python, and Javascript.

                1. 1

                  I think Swift fits in quite well in-between. Automatic reference counting, so little need to babysit lifetimes, while using a powerful ML-like type system in modernised C-like syntax.

              2. 15

                But on Go’s side, Go concurrency is really awesome and smooth

                Concurrency is an area I feel Go really lets the programmer down. There is a simple rule for safe concurrent programming: No object should be both mutable and shared between concurrent execution contexts at the same time. Rust is not perfect here, but it uses the unique ownership model and the send trait to explicitly transfer ownership between threads so you can pass mutable objects around, and the sync trait for safe-to-share things. The only safe things to share in safe rust are immutable objects. You can make other things adopt the sync trait if you’re willing to write unsafe Rust, but at least you’re signposted that here be dragons. For example, the ARC trait in Rust (for atomic reference counting), which gives you a load of read-only shared references to an object and the ability to create a mutable reference if there are no other outstanding references.

                In contrast, when I send an object down a channel in Go, I still have a pointer to it. The type system gives me nothing to help avoid accidentally aliasing an object between two threads. To make things worse, the Go memory model is relaxed consistency atomic, so you’re basically screwed if you do this. To make things even worse, core bits of the language semantics rely on the programmer not doing this. For example, if you have a slice that is in an object that is shared between two goroutines, both can racily update it. The slice contains a base and a length and so you can see tearing: the length from one slice and the base from another. Now you can copy it, dereference it and read or write past the end of an array. This is without using anything in the unsafe package: you can violate memory safety (let alone type safety) purely in ‘safe’ Go, without doing anything that the language helps you avoid.

                I wrote a book about Go for people who know other languages. It didn’t sell very well, in part because it ended up being a long description of things that Go does worse than other languages.

                1. 2

                  That’s a worthwhile point. I haven’t been bitten by the ability to write to Go object that have already been sent down a channel yet, but I haven’t worked on any large-scale long-term Go projects. I’ve found it straightforward enough to just not use objects after sending. But then, the reason why we build these fancy type systems with such constraints is that even the best developers have proved to be not very good at consistently obeying these limits on large-scale projects.

                  I’m hoping that the Rust issues with async and tokio are more like teething pains for new tech than a fundamental issue, and that eventually, it will have concurrency tools that are both as ergonomic as Go’s and use Rust’s thread safety rules.

                  1. 5

                    I’ve found it straightforward enough to just not use objects after sending.

                    This is easy if the object is not aliased, but that requires you to have the discipline of linear ownership before you get near the point that sends the object, or to only ever send objects allocated near the sending point. Again, the Go type system doesn’t help at all here, it lets you create arbitrary object graphs with N pointers to an object and then send the object. The (safe) Rust type system doesn’t let you create arbitrary object graphs and then gives strong guarantees on what is safe to send. The Verona type system is explicitly designed to allow you to create arbitrary (mutable or immutable) object graphs and send them safely.

                2. 9

                  And using modules through Cargo is just so nice, while Go modules is kind of a messy hack in comparison.

                  I have always found Rust’s module system completely impenetrable. I just can’t build a mental model of it that works for me. I always end up just putting keywords and super:: or whatever in front in various combinations until it happens to work. It reminds me of how I tried to get C programmes to compile when I was a little kid: put more and more & or * in front of expressions until it works.

                  And of course they changed in Rust 2018 as well which makes it all the more confusing.

                  1. 3

                    Yeah, I’ve had the same experience. Everything else about Cargo is really nice, but modules appear to be needlessly complicated. I have since been told that they are complicated because they allow you to move your files around in whatever crazy way you prefer without having to update imports. Personally I don’t think this is a sane design decision. Move your files, find/replace, move on.

                    1. 2

                      And of course they changed in Rust 2018 as well which makes it all the more confusing.

                      One of the things they changed in Rust 2018, FYI, was the module system, in order to make it a lot more straightforward. Have you had the same problem since Rust 2018 came out?

                    2. 6

                      For me Go is the continuation of C with some added features like CSP. Rust is/was heavily influenced by the ML type of languages which is extremely nice. I think ML group is superior in my ways to the C group. ADTs are the most trivial example why.

                      1. 4

                        I generally agree. I like ML languages in theory and Rust in particular, but Rust and Go aren’t in the same ballpark with respect to developer productivity. Rust goes to impressive lengths to make statically-managed memory user-friendly, but it’s not possible to compete with GC. It needs to make up the difference in other areas, and it does make up some of the difference in areas like error handling (?, enums, macros, etc and this is still improving all the time), IDE support (rust-analyzer has been amazing for me so far), and compiler error messages, but it’s not yet enough to get into a competitive range IMO. That said, Rust progresses at a remarkable pace, so perhaps we will see it get there in the next few years. For now, however, I like programming in Rust–it satisfies my innate preference to spend more time building something that is really fast, really abstract, and really correct–but when I need to do quality work in a short time frame in real world projects, I still reach for Go.

                        1. 9

                          To me Go seems like a big wasted opportunity. If they’d only taken ML as a core language instead of a weird C+gc hybrid, it would be as simple (or simpler) as it is, but much cleaner, without nil or the multi-return hack. Sum types and simple parametric polymorphism would be amazing with channels. All they had to do was to wrap that in the same good toolchain with fast compilation and static linking.

                          1. 2

                            Yeah, I’ve often expressed that I’d like a Go+ML-type-system or a Rust-lite (Rust with GC instead of ownership). I get a lot of “Use OCaml!” or “Use F#”, but these miss the mark for a lot of reasons, but especially the syntax, tooling, and ecosystem. That said, I really believe we overemphasize language features and under-emphasize operational concerns like tooling, ecosystem, runtime, etc. In that context, an ML type system or any other language feature is really just gravy (however, a cluster of incoherent language features is a very real impediment).

                            1. 1

                              Nothing is stopping anyone from doing that. I’d add that they make FFI to C, Go, or some other ecosystem as easy as Julia for the win. I recommend that for any new language to solve performance and bootstrapping problem.

                            2. 3

                              Then, you have languages like D that compile as fast as Go, run faster with LLVM, have a GC, and recently an optional borrow checker. Contracts, too. You get super productivity followed by as much speed or safety as you’re willing to put in effort for.

                              Go is a lot easier to learn, though. The battle-tested, standard libraries and help available on the Internet would probably be superior, too.

                              1. 4

                                I hear a lot of good things about D and Nim and a few others, but for production use case, support, ecosystem, developer marketshare, tooling, etc are all important. We use a lot of AWS services, and a lot of their SDKs are Python/JS/Go/Java/dotnet exclusively and other communities have to roll their own. My outsider perspective is that D and Nim aren’t “production ready” in the sense that they lack this sort of broad support and ecosystem maturity, and that’s not a requirement I can easily shrug off.

                                1. 2

                                  I absolutely agree. Unless easy to handroll, those kind of things far outweigh advantages in language design. It’s what I was hinting at in 2nd paragraph.

                                  It’s also why it’s wise for new languages to plug into existing ecosystems. Clojure on Java being best example.

                        1. 7

                          Great write-up.

                          static linking makes binaries easy to deploy

                          I always wonder why Go gets the fame for this as if it’s the only language that can do this. You can statically link a C or C++ program. It’s not the default, but you can. There’s definitely something to be said for good defaults and how they encourage behaviour, but I’m still puzzled as to how Go managed to get this particular slice of the marketing pie.

                          when you make something simple, you move complexity elsewhere

                          This is a very good point, and one I’ve made before. Whatever logic your program needs to do is still complicated. Making the language simple just makes user code have to do more of the work. There’s a sweet spot, but that lies on different points in the spectrum for different people. Haskell is a step too far for me, and I like Haskell.

                          1. 13

                            You can statically link a C or C++ program. It’s not the default, but you can.

                            Defaults matter, especially in cases where you want to use others’ libraries, because there’s a decent chance that the non-default configuration doesn’t work. For example, glibc doesn’t support static linking at all, and more than a few other libraries only work with glibc.

                            1. 4

                              Defaults matter

                              I agree, which is why I mentioned how good defaults encourage good behaviour.

                              1. 6

                                I agree that defaults matter. That’s why I don’t use Go. The fact that print still exists, but prints to stderr is craptastic.

                                Imagine if you asked for a knife to chop things for a salad, and someone handed over a simple kitchen knife. As you start to slice the tomato, you realize something is terribly wrong. You feel an edge blunter than a butter knife. You turn around to your friend confused, and they smack their forehead and go “ohhhhh, for a tomato! Hold on.” And they wander off to another room and bring back a chef’s block with a nearly identical set of actually sharp knives. “You just have to go get these from the pantry when you want actual sharp knives. We don’t keep them in the kitchen, for obvious reasons.”

                                As a noob to the language, I was tinkering with some code. At one point, flipping between python and Go, I put a print() statement in Go. I then spent the next couple of minutes confused about why the next program in the pipe couldn’t see the output from my Go.

                                For those that have never used Go or are just starting out: you need to import “fmt”, and then use fmt.Print. Yes, it’s in all the tutorials. Yes, you will likely still beans it up and use print() by accident, especially if you come from other languages. No, there aren’t any warnings when you do this and then go run or go build. Go just does something different for the concept of “print” and “println” in silence. I don’t need that kind “tee hee, GOTCHA!” in my life.

                                1. 1

                                  Go just does something different for the concept of “print” and “println” in silence. I don’t need that kind [of] “tee hee, GOTCHA!” in my life.

                                  Maybe Go could use some of Ruby’s POLA philosophy.

                              2. 11

                                I always wonder why Go gets the fame for this as if it’s the only language that can do this. You can statically link a C or C++ program. It’s not the default, but you can. There’s definitely something to be said for good defaults and how they encourage behaviour, but I’m still puzzled as to how Go managed to get this particular slice of the marketing pie.

                                My theory is that it’s because Ruby, Python, and Node lowered the bar so tremendously on this that anything else is seen as a huge leap forward.

                                1. 5

                                  and the c/c++ toolchains are not exactly easy to handle. i won’t even try to build something static that wasn’t intended to. that’s why many things which are distributed as binary use linker magic and ship their own set of libs.

                                2. 2

                                  You can statically link C and C++, yes.

                                  But with Go you can also cross-compile that static executable with zero effort to multiple platforms.

                                  1. 2

                                    What does Cross compilation have anything to do with static linking or what I said?

                                1. 1

                                  Man likes Rust more than Go. News at 11.

                                  Nothing new here.

                                  1. 18

                                    Ah, yes. The old “this person has the same opinion that some other people have, and I disagreed with them, so I can’t possibly learn anything new from this person’s perspective”, well known for being both correct and insightful.

                                  1. 3

                                    I find it interesting that it’s so strict about things like “always require else”, but they allow if !found … else … (logically simpler is if found … else …).

                                    Also, this whole idea makes me happy. I’ve become an advocate for more verbose programming over the past few years. Typing on the keyboard is literally the cheapest thing I do at work. It confounds me that people what to make code harder to read so they can save a few keystrokes.

                                    1. 9

                                      If verbosity and understandability were the same thing, I’d agree. Verbosity gives you more to read. You can hone in on particulars. But I notice that when you get used to some terseness, you can see the particulars in context.

                                      1. 3

                                        Isn’t the crux of the issue here the intended audience? If you don’t know who’s going to be reading your code you ought to err on the side of verbosity, ISTM.

                                        1. 5

                                          We can write at a level that inspires people to learn.

                                          1. 1

                                            If shit is actively hitting the fan and I need to fix the damn code, I don’t want to be handed something that “inspires me to learn”.

                                            I want something I can grasp, understand what it does and why and figure out the problem.

                                            I can get inspired and learn on my own dime, not when I’m maintaining a broken piece of code.

                                            1. 6

                                              Let’s say that you are a very junior developer and you encounter a library function you’ve never seen before. You want to understand the code so you look up that function. Once you learn it, you discover that you can use it in many places and remove a lot of boiler-plate code the next time you do something similar.

                                              Believe it or not, for some people that function is ‘map.’ That’s the “inspiration to learn” I was talking about.

                                            2. 1

                                              This is true, but again, you have to take the audience into consideration. Do you intend the code to be instructive as well as functional? Not all code needs or ought to be so.

                                            3. 4

                                              Verbosity means you can see less code at a time.

                                              Seeing less code makes people brave – if they believe they can understand what they see, they often assume they can understand what they don’t see.

                                              If that code isn’t relevant, then it might be ok, but if the code is relevant, you’re setting them up for failure.

                                              That’s why generally you’ll be more successful giving them as little to read as necessary.

                                              That qualifier “necessary” is tricky because sometimes seeing an algorithm “spelled out” makes it easier to understand – and I suspect your experiences are more like this. Here’s a situation where spelling it out is a disaster waiting to happen:

                                              I’ve got some code at the moment that unwinds tags and then performs an intrinsic operation on them, or calls the nested function. Just handing the intrinsic operation looks something like this (all unrolled):

                                              #define FOR(n,a) {size_t i=0,_n=(n);for(;i<_n;++i){a}}
                                              #define TAGGED(x) (((uintptr_t)(x))&1)
                                              #define UNTAG(x) (((intptr_t)(x))>>1)
                                              #define MAKE_TAGGED(x) ((A)((((intptr_t)(x))<<1)|1))
                                              #define F(f,O) A f(A x,A y){ A r;\
                                                if(TAGGED(x)&&TAGGED(y))return MAKE_TAGGED(UNTAG(x) O UNTAG(y)); \
                                                if(TAGGED(x)){ \
                                                  if(y->ref == 0){ FOR(y->size,y->data[i] = UNTAG(x) O y->data[i]); return y; } \
                                                  r = make(y->size); FOR(r->size, r->data[i] = UNTAG(x) O y->data[i]); unref(y); return r; }} \
                                                if(TAGGED(y)){ \
                                                  if(x->ref == 0) { FOR(x->size,x->data[i] O ## = UNTAG(y)); return x; } \
                                                  r = make(x->size); FOR(r->size, r->data[i] = x->data[i] O UNTAG(y)); unref(x); return r; }} \
                                                if(x->size != y->size) return ERROR_SIZE; \
                                                if(!x->ref) { FOR(x->size,x->data[i] = x->data[i]  O y->data[i]); unref(y); return x; } \
                                                if(!y->ref) { FOR(x->size,y->data[i] = x->data[i]  O y->data[i]); unref(x); return y; } \
                                                r = make(x->size); FOR(r->size, r->data[i] = x->data[i] O UNTAG(y)); unref(x); unref(y); return r; }
                                              

                                              Note I want to check the reference count and reuse any object whenever possible since they could be very large (multi-terabyte!) so I save a lot of RAM/swapping here. However as I mentioned I also need a check for whether the object is a nested array. “A” is my array type and it looks like this:

                                              struct A;typedef struct A*A;struct A{ int is_data, ref; size_t size; union { long long data[1]; A members[1]; }};
                                              

                                              That means each of the untagged paths above also need a:

                                              if(!x->is_data){ /* repeat above for f(X,Y) instead of X O Y */ }
                                              

                                              This quickly gets this mere macro larger than a screen – and in the real implementation, I have about twenty types (not just data and array, and some of those types require prep on the arguments, e.g. are timestamps) or so that I want constructed implementations for. Madness! Testing all those combinations is really hard, and it’s too big to read (is there a bug in the above? There could be!)

                                              The only way I can keep the code understandable is to be less verbose. In my case that means a few macros, load some temporary variables, and rely on gcc to eliminate the dead loads and built optimised assembly (it does).

                                              1. 1

                                                But what’s the intended audience of the code above? Is it fellow practitioners, or is it more workaday journeymen like myself? If the problem is genuinely complex, the solution will also be complex – and it is, as you say, a positive good not to try and obfuscate the complexity.

                                                I guess my point is, there is no single rule that encompasses all cases, but a useful heuristic for me in my career has been to constrain the code I’m writing to the capabilities of the expected audience. My greatest failures have come when I’ve ignored this advice to myself.

                                              2. 2

                                                The crux of it is this: maintainability. The intended audience is ‘anyone and everyone who reads this code and has to work on it now and in the future’.

                                                Confuscated, difficult to understand code is another thing. In this case, control over the codebase is usually the issue. You can’t have high-performance, expensive things, without control.

                                              3. 2

                                                It seems to me there’d be a place for a tool which allowed the viewer to hide/display comments at different levels, similar to filtering on logging levels.

                                            1. 2

                                              What’s wrong with current’s implementation written in ANSI C? Seems like a waste of human resources, in my opinion.

                                              1. 32

                                                What’s wrong with current’s implementation written in ANSI C?

                                                If you want to script a Go application, it’s a hell of a lot easier if the VM is native instead of trying to fight with C interop issues.

                                                Seems like a waste of human resources, in my opinion.

                                                Incredible that with zero insight into the business requirements or technical motivations behind a project, you still consider yourself qualified to judge the staffing allocations behind it

                                                1. 7

                                                  Incredible that with zero insight into the business requirements or technical motivations behind a project, you still consider yourself qualified to judge the staffing allocations behind it.

                                                  You are definitely right about this.

                                                  If you want to script a Go application, it’s a hell of a lot easier if the VM is native instead of trying to fight with C interop issues.

                                                  As @tedu commented, I understand the motivation behind it, thanks for pointing it out too.

                                                  1. 2

                                                    They give some further explanation of the technical requirements that led them not to pick up Shopify’s version here, which I found intersting: https://github.com/Azure/golua/issues/36.

                                                    The TL;DR is they’ve been scripting Helm for a while using another Lua-in-Go engine, but it isn’t updating to 5.3, which they very much want, along with closer conformance to the Lua spec, plus they have some design ideas that they feel would make debugging etc easier.

                                                    1. 3

                                                      To each their own, but I’m a little perplexed that strict 5.3 conformance would be a driver. I’m using gopher-lua which is like 5.1 but not exactly, but it’s just fine. What I need is some scripting language to embed, it’s close enough to lua it’s not really learning a whole new whatever, and it interfaces nicely with go (nicer than a strict port of the stack api would).

                                                      I don’t know jack about helm, but after seven seconds of research I can’t reverse engineer a requirement for lua 5.3 accept no substitutes.

                                                      1. 1

                                                        It’s possible that they have some need of true 64-bit integers, which only came to Lua in 5.3. That’s the only important change I can think of.

                                                2. 6

                                                  Writing memory-safe C is an artform and a craft.

                                                  Writing safe code with Go is something a trained orangutang can do.

                                                  1. 3

                                                    Lua bytecode is memory-unsafe in the reference implementation, for starters.

                                                    1. 2

                                                      Do you have a link or more details?

                                                      1. 1

                                                        My upload server was down, sorry I didn’t link this originally. Here’s a slide deck (pdf) of a presentation I gave at Houston Area Hackers with almost all the details you could want.

                                                        I still have to update this; there are quite a few more tricks you can pull. I ended up creating a pure-Lua dlsym() implementation for the project that spurred all this research.

                                                        1. 2

                                                          Hmmm… Do reimplementations like this one remove support for lightuserdata then? I’m having a hard time imagining a lua interpreter for arbitrary bytecode that supported lightuserdata but could nevertheless guarantee that those pointers were safe.

                                                          1. 1

                                                            Looks like it’s unimplemented. lightuserdata is a pretty damn leaky Lua API. Bytecode itself is even more leaky in the Lua C implementation - arbitrary Lua bytecode is as expressive as C. No surprise that MS seemingly wanted to make an API/ABI compatible implementation that fixed some unnecessary leakiness.

                                                            1. 1

                                                              Yeah. In gopher-lua, userdata actually looks kind of like lightuserdata, which doesn’t exist. When both host and lua state share an allocator, the difference becomes less meaningful.

                                                      2. 2

                                                        It’s annoying to interface with from go code.

                                                        1. 1

                                                          seems like something that should be fixed in Go rather than reimplementing everything written in C.

                                                          1. 1

                                                            For better or worse, it’s a result of deliberate design trade-offs in go, so it’s unlikely to change.

                                                      1. 11

                                                        Over the course of using only Go at work for the last year-ish, my opinion went from:

                                                        Go has some weird opinions and safety rails, but the advantages are still nice

                                                        to:

                                                        I love Go’s restrictiveness and can’t imagine any other way

                                                        Not sure if it’s stockholm syndrome by now but it’s definitely grown on me.

                                                        For example, I tried adapting some C++ code to Go last week, and the person was doing mystring[len(mystring)] and it worked with no out of bounds errors :(

                                                        1. 4

                                                          From the onset I’ve been viewing Go as the better C I needed. It did take me time to get used to the fascist gofmt, though. Today I mostly have gripes with the standard library, debugging and trying to get away from thinking in the event loop model (that took me so much time to internalize). Still enjoying the language and the tools.

                                                          1. 5

                                                            The best thing about gofmt is that there is nothing you CAN configure. It’s the gofmt way or the highway.

                                                            1. 2

                                                              I think the fascism of gofmt is great. After using go for a while, I miss a similar tool for other languages. I get by with clang-format for C, but it requires quite a bit of configuration, and still doesn’t get some things right.

                                                          1. 1

                                                            “If we took our previous set of microservices and we were to consider the likes of Go for these microservices, we could cut down the memory footprint for one of these microservices from 1GB per instance of an to something like 64MB. This represents a massive saving and would allow us to achieve the same resiliency that we needed before in 756MB worth of RAM. Less than the total cost of 1 instance of our Java based microservice.”

                                                            1. 4

                                                              Why though? I have no love for Java, but it isn’t inherently memory inefficient. Sounds like they just suck at writing code.

                                                            1. 4

                                                              Please don’t submit articles that are just advertising.

                                                              1. 1

                                                                Especially since Activestate stuff has been horrible since the dawn of time.