1. 1

    The guy is heading in the right direction but he isn’t saying anything new. This kind of critique has existed since forever and he’s argumenting it in a very weak way, arguably doing more harm than good. That said, if all the STEMlords were like him, even without renouncing their framework of thought as STEMlords, there would be much less bullshit about AI around the internet and the IT industry. So in the end, kudos, but read more tech critique.

    1. 2

      would you be able to point to a stronger/ better worded/ more comprehensive argument similar to or at least in the vein of the points I tried to raise in said article ?

      I wouldn’t mind updating it or even scrapping it and replacing it with a re-direct or a link at the top if those same ideas are manifest in someone else writing in a more cohesive manner.

      1. 4

        Sure.

        Here’s one: http://rodneybrooks.com/the-seven-deadly-sins-of-predicting-the-future-of-ai/ This one is somehow related: https://medium.com/@mijordan3/artificial-intelligence-the-revolution-hasnt-happened-yet-5e1d5812e1e7

        And this other paper is already beyond the AGI narrative and tries to understand why this and other narratives about AI (and implicitly AGI) came to be. I’m not 100% you will find it related, but it’s such a great paper that I think it’s worth reading: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3078224

        edit: uh, and another great one: https://jods.mitpress.mit.edu/pub/resisting-reduction This is probably even closer to your argument, in some way. It doesn’t say that we don’t need AGI because we have humans, but it takes the narrative about AGI and singularity to highlight flaws in the vision of the human from the perspective of the singularists.

        1. 1

          From your first link.

          Even if there is a lot of computer power around it does not mean we are close to having programs that can do research in Artificial Intelligence, and rewrite their own code to get better and better.

          That’s a strawman. The argument is exactly - AGI is by definition capable of doing research in AI, ergo it can improve itself. And that is dangerous. By saying “but that’s far away” you are not defeating the argument.

    1. 6

      You know someone is over-hyping Rust (or is just misinformed) when you see statements like

      Which means there’s no risk of concurrency errors no matter what data sharing mechanism you chose to use

      The borrow checker prevents data races which are involved in only a subset of concurrency errors. Race conditions are still very possible, and not going away any time soon. This blog post does a good job explaining the difference.

      Additionally, I have my worries about async/await in a language that is also intended to be used in places that need control over low level details. One library that decides to use raw I/O syscalls on some unlikely task (Like error logging) and, whoops, there goes your event loop. Bounded thread pools don’t solve this (What happens if you hit the max? It’s equivalent to a limited semaphore), virtual dispatch becomes more of a hazard (Are you sure every implementation knows about the event loop? How can you be sure as a library author?), what if you have competing runtime environments (See twisted/gevent/asyncio/etc. in the Python community. This may arguably be more of a problem in Rust given it’s focus on programmer control), and the list goes on. In Go, you literally never have to worry about this, and it’s the greatest feature of the language.

      1. 1

        You know someone is over-hyping Rust (or is just misinformed) when you see statements like

        It doesn’t help that they state (or did state until recently) on their website that Rust was basically immune to any kind of concurrency error.

        1. -1

          That definition of “race condition - data race” essentially refers to a operational logic error on the programmer’s side. As in, there’s no way to catch race conditions that aren’t data races via a compiler, unless you have a magical business-logic-aware compiler, at which point, you wouldn’t need a programmer.

          As far as the issues with async I/O go… well, yes. Asyncio wouldn’t solve everything. But asyncio also wouldn’t necessarily have to be single threaded. It could just meant that a multi-threaded networking application will now spend less resources on context-switching between threads. But the parallelism of threads > cpu_count still comes useful for various blocking operations which may appear here and there.

          As far as GO’s solution goes, their solution to the performance issue isn’t that good. Since goroutines have significant overhead. Much less than a native thread, but still, considerably more overhead than something like MIO.

          The issue you mentioned as an example, hidden sync I/O syscall by some library, can well happen in a goroutine run function just as well, the end-result of that will essentially be a OS native thread being blocked, much like in Rust. At least, as far as my understanding of goroutine goes, that seems to be the case.

          Granted, working with a “pool” of event loops representing multiple threads might be harder than just using goroutines, but I don’t see it as being that difficult.

          1. 5

            That definition is the accurate, correct definition. It’s important to state that Rust helps with data races, and not race conditions in general. Even the rustonomicon makes this distinction clear.

            The discussion around multiple threads seems like a non-sequitur to me. I’m fully aware that async/await works fine with multiple threads. I also don’t understand why the performance considerations of goroutines were brought into the picture. I’m not making any claims about performance, just ease of use and programmer model. (Though, I do think it’s important to respond that goroutines are very much low enough overhead for many common tasks. It also makes no sense to talk about performance and overhead outside of the context of a problem. Maybe a few nanoseconds per operation is important, and maybe it isn’t.)

            The issue I mentioned does not happen in Go: all of the syscalls/locks/potentially blocking operations go through the runtime, and so it’s able to deschedule the goroutine and let others run. This article is another great article about this topic.

            It’s great that you’re optimistic about the future direction Rust is taking with it’s async story. I’m optimistic too, but that’s because I have great faith in the leadership and technical design skills of the Rust community to solve these problems. I’m just pointing out that they ARE problems that need to be solved, and the solution is not going to be better than Go’s solution in every dimension.

            1. 0

              The issue I mentioned does not happen in Go: all of the syscalls/locks/potentially blocking operations go through the runtime, and so it’s able to deschedule the goroutine and let others run.

              Ok, maybe I’m mistaken here but:

              “Descheduling a goroutine”, when a function call is blocking, descheduling a goroutine has the exact same cost as descheduling a thread, which is huge.

              Secondly, go is only using a non-blocking syscall under the hood for networking I/O calls at the moment. So if I want to wait for an operation on any random file or wait for an asynchronous prefetch call, I will be unable to do so, I have to actually block the underlying thread that the goroutine is using.

              I haven’t seen any mention of “all blocking syscalls operations” being treated in an async manner, they go through the runtime, yes, but the runtime may just decide that it can do nothing about it other than let the thread be de-scheduled as usual. And, as far as I know, the runtime is only “smart” about networking I/O syscalls atm, the rest are treated like a blocking operation/

              Please correct me if this is wrong.

              1. 2

                descheduling a goroutine has the exact same cost as descheduling a thread, which is huge.

                A goroutine being descheduled means it yields the processor and calls into the runtime scheduler, nothing more. What happens to the underlying OS threads is another matter entirely. This can happen at various points where things could block (e.g. chan send / recv, entering mutexes, network I/O, even regular function calls), but not at every such site.

                the runtime is only “smart” about networking I/O syscalls atm

                Yes, sockets and pipes are handled by the poller, but what else could it be smarter about? The situation may well be different on other operating systems, but at least on Linux, files on disk are always ready as far as epoll is concerned, so there is no need to go through the scheduler and poller for those. In that case, I/O blocks both the goroutine and the thread, which is fine for Go. For reference, in this situation, node.js uses a thread pool that it runs file I/O operations on, to avoid blocking the event loop. Go doesn’t really need to do this under the covers, though, because it doesn’t have the concept of a central event loop that must never be blocked waiting for I/O.

                1. 2

                  Descheduling a goroutine is much cheaper than descheduling a thread. Goroutines are cooperative with the runtime, so they ensure that there is minimal state to save when descheduling (no registers, for example). It’s on the order of nanoseconds vs microseconds. Preemptive scheduling helps in a number of ways, but typically causes context switching to be more expensive: you have to be able to stop/start at any moment.

                  Go has an async I/O loop, yes, but it runs in a separate managed thread by the runtime. When a goroutine would wait for async I/O, it parks itself with the runtime, and the thread the goroutine was running on can be used for other goroutines.

                  While the other syscalls do in fact take up a thread, critically, the runtime is aware when a goroutine is going to enter a syscall, and so it can know that the thread will be blocked, and allow other goroutines to run. Without that information, you would block up a thread and waste that extra capacity.

                  The runtime manages a threadpool and ensures that GOMAXPROCS threads are always running your code, no matter what syscalls or I/O operations you’re doing. This is only possible if the runtime is aware of every syscall or I/O operation, which is not possible if your language/standard library are not desiged to provide. Which Rust’s doesn’t, for good reasons. It has tradeoffs with respect to FFI speed, control, zero overhead, etc. They are different languages with different goals, and one isn’t objectively better than the other.

                  1. 2

                    And, as far as I know, the runtime is only “smart” about networking I/O syscalls atm, the rest are treated like a blocking operation/

                    Pretty much everything that could block goes through sockets and pipes though. The only real exception is file I/O, and file I/O being unable to be epolled in a reasonable way is a kernel problem not a Go problem.

            1. 2

              I’ve found the laptop market to be full of gotchas. Some models skim on the costs by having DDR3, others don’t have a PCIE m.2, others have rubbish battery & cooling or weight too much.

              Personally I ended up going with tux computers, which I’d recommend if you are based in Europe. They resell laptops from one of the like 3 Chinese companies that actually make the case+screen+board+CPU bundles and they add components for s very tiny markup.

              To give an example, a i7 kabylake r m with 16gb at 2666ghz (and good latencies for that fq, don’t remember what they are right now), 512GB of Samsung 970pro nvme, ~8h working battery life, with a weight between 1.3 and 1.5kg, will cost you 1.1 - 1.3k depending on the exact model of screen& chassi and a few other tweaks.

              They also have a nice lack of anti-features, batteries and screens that are easy to replaces, no components soldered in, cases that are easy to open… Etc.

              The real selling point is that they have a penguin on the window key.

              But, if you want to go cheap and are happy with a sata.m2 and mehish battery, look into an Asus. Besides those two issues, they have some too-good-to-be-true stuff in the 500-700$ range.

              The xiaomi stuff is also catching up, give it 1-2 years and their ultra-portable laptops will offer the same value proposition as their phones

              1. 20

                The “lacks” of Go in the article are highly opinionated and without any context of what you’re pretending to solve with the language.

                Garbage collection is something bad? Can’t disagree harder.

                The article ends with a bunch of extreme opinions like “Rust will be better than Go in every possible task

                There’re use cases for Go, use cases for Rust, for both, and for none of them. Just pick the right tool for your job and stop bragging about yours.

                You love Rust, we get it.

                1. 2

                  Yes, I would argue GC is something that’s inherently bad in this context. Actually, I’d go as far as to say that a GC is bad for any statically typed language. And Go is, essentially, statically typed.

                  It’s inherently bad since GC dictates the lack of destruction mechanisms that can be reliably used when no reference to the resource are left. In other words, you can’t have basic features like the C++ file streams that “close themselves” at the end of the scope, then they are destroyed.

                  That’s why Go has the “defer” statement, it’s there because of the GC. Otherwise, destructors could be used to defer cleanup tasks at the end of a scope.

                  So that’s what makes a GC inherently bad.

                  A GC, however, is also bad because it “implies” the language doesn’t have good resource management mechanisms.

                  There was an article posted here, about how Rust essentially has a “static GC”, since manual deallocation is almost never needed. Same goes with well written C++, it behaves just like a garbage collected language, no manual deallocation required, all of it is figured out at compile time based on your code.

                  So, essentially, a GC does what language like C++ and Rust do at compile time… but it does it at runtime. Isn’t this inherently bad ? Doing something that can be done at CT during runtime ? It’s bad from a performance perspective and also bad from a code validation perspective. And it has essentially no upsides, as far as I’ve been able to tell.

                  As far as I can tell the main “support” for GC is that they’ve always been used. But that doesn’t automatically make them good. GCs seem to be closer to a hack for a language to be easier to implement rather than a feature for a user of the language.

                  Feel free to convince me otherwise.

                  1. 11

                    It’s inherently bad since GC dictates the lack of destruction mechanisms that can be reliably used when no reference to the resource are left.

                    Why do you think this would be the case? A language with GC can also have linear or affine types for enforcing that resources are always freed and not used after they’re freed. Most languages don’t go this route because they prefer to spend their complexity budgets elsewhere and defer/try-with-resources work well in practice, but it’s certainly possible. See ATS for an example. You can also use rank-N types to a similar effect, although you are limited to a stack discipline which is not the case with linear/affine types.

                    So, essentially, a GC does what language like C++ and Rust do at compile time… but it does it at runtime. Isn’t this inherently bad ?

                    No, not necessarily. Garbage collectors can move and compact data for better cache locality and elimination of fragmentation concerns. They also allow for much faster allocation than in a language where you’re calling the equivalent of malloc under the hood for anything that doesn’t follow a clean stack discipline. Reclamation of short-lived data is also essentially free with a generational collector. There are also garbage collectors with hard bounds on pause times which is not the case in C++ where a chain of frees can take an arbitrary amount of time.

                    Beyond all of this, garbage collection allows for a language that is both simpler and more expressive. Certain idioms that can be awkward to express in Rust are quite easy in a language with garbage collection precisely because you do not need to explain to the compiler how memory will be managed. Pervasive use of persistent data structures also becomes a viable option when you have a GC that allows for effortless and efficient sharing.

                    In short, garbage collection is more flexible than Rust-style memory management, can have great performance (especially for functional languages that perform a lot of small allocations), and does not preclude use of linear or affine types for managing resources. GC is hardly a hack, and its popularity is the result of a number of advantages over the alternatives for common use cases.

                    1. 1

                      What idioms are unavailable in Rust or in modern C++, because of their lack of GC, but are available in a statically typed GC language ?

                      I perfectly agree with GC allowing for more flexibility and more concise code as far as dynamic language go, but that’s neither here nor there.

                      As for the theoretical performance benefits and real-time capabilities of a GCed language… I think the word theoretical is what I’d focus my counter upon there, because they don’t actually exist. The GC overhead is too big, in practice, to make those benefits outshine languages without runtime memory management logic.

                      1. 9

                        I’m not sure about C++, but there are functions you can write in OCaml and Haskell (both statically typed) that cannot be written in Rust because they abstract over what is captured by the closure, and Rust makes these things explicit.

                        The idea that all memory should be explicitly tracked and accounted for in the semantics of the language is perhaps important for a systems language, but to say that it should be true for all statically typed languages is preposterous. Languages should have the semantics that make sense for the language. Saying a priori that all languages must account for some particular feature just seems like a failure of the imagination. If it makes sense for the semantics to include explicit control over memory, then include it. If it makes sense for this not to be part of the semantics (and for a GC to be used so that the implementation of the language does not consume infinite memory), this is also a perfectly sensible decision.

                        1. 2

                          there are functions you can write in OCaml and Haskell (both statically typed) that cannot be written in Rust because they abstract over what is captured by the closure

                          Could you give me an example of this ?

                          1. 8

                            As far as I understand and have been told by people who understand Rust quite a bit better than me, it’s not possible to re-implement this code in Rust (if it is, I would be curious to see the implementation!)

                            https://gist.github.com/dbp/0c92ca0b4a235cae2f7e26abc14e29fe

                            Note that the polymorphic variables (a, b, c) get instantiated with different closures in different ways, depending on what the format string is, so giving a type to them is problematic because Rust is explicit about typing closures (they have to talk about lifetimes, etc).

                            1. 2

                              My God, that is some of the most opaque code I’ve ever seen. If it’s true Rust can’t express the same thing, then maybe it’s for the best.

                              1. 2

                                If you want to understand it (not sure if you do!), the approach is described in this paper: http://www.brics.dk/RS/98/12/BRICS-RS-98-12.pdf

                                And probably the reason why it seems so complex is because CPS (continuation-passing style) is, in general, quite hard to wrap your head around.

                                I do think that the restrictions present in this example will show up in simpler examples (anywhere where you are trying to quantify over different functions with sufficiently different memory usage, but the same type in a GC’d functional language), this is just a particular thing that I have on hand because I thought it would work in Rust but doesn’t seem to.

                                1. 2

                                  FWIW, I spent ~10 minutes trying to convert your example to Rust. I ultimately failed, but I’m not sure if it’s an actual language limitation or not. In particular, you can write closure types in Rust with 'static bounds which will ensure that the closure’s environment never borrows anything that has a lifetime shorter than the lifetime of the program. For example, Box<FnOnce(String) + 'static> is one such type.

                                  So what I mean to say is that I failed, but I’m not sure if it’s because I couldn’t wrap my head around your code in a few minutes or if there is some limitation of Rust that prevents it. I don’t think I buy your explanation, because you should technically be able to work around that by simply forbidding borrows in your closure’s environment. The actual thing where I got really hung up on was the automatic currying that Haskell has. In theory, that shouldn’t be a blocker because you can just introduce new closures, but I couldn’t make everything line up.

                                  N.B. I attempted to get any Rust program working. There is probably the separate question of whether it’s a roughly equivalent program in terms of performance characteristics. It’s been a long time since I wrote Haskell in anger, so it’s hard for me to predict what kind of copying and/or heap allocations are present in the Haskell program. The Rust program I started to write did require heap allocating some of the closures.

                    2. 5

                      It’s inherently bad since GC dictates the lack of destruction mechanisms that can be reliably used when no reference to the resource are left. In other words, you can’t have basic features like the C++ file streams that “close themselves” at the end of the scope, then they are destroyed.

                      Deterministic freeing of resources is not mutually exclusive with all forms of garbage collection. In fact, this is shown by Rust, where reference counting (Rc) does not exclude Drop. Of course, Drop may never be called when you create cycles.

                      (Unless you do not count reference counting as a form of garbage collection.)

                      1. 2

                        Well… I don’t count shared pointers (or RC pointers or w/e you wish to call them) as garbage collected.

                        If, in your vocabulary, that is garbage collection then I guess my argument would be against the “JVM style” GC where the moment of destruction can’t be determined at compile time.

                        1. 8

                          If, in your vocabulary, that is garbage collection

                          Reference counting is generally agreed to be a form of garbage collection.

                          I guess my argument would be against the “JVM style” GC where the moment of destruction can’t be determined at compile time.

                          In Rc or shared_ptr, the moment of the object’s destruction can also not be determined at compile time. Only the destruction of the Rc itself; put differently the reference count decrement can be determined at compile time.

                          I think your argument is against tracing garbage collectors. I agree that the lack of deterministic destruction is a large shortcoming of languages with tracing GCs. It effectively brings back a parallel to manual memory management through the backdoor — it requires manual resource management. You don’t have to convince me :). I once wrote a binding to Tensorflow for Go. Since Tensorflow wants memory aligned on 32-byte boundaries on amd64 and Go allocates (IIRC) on 16-byte boundaries, you have to allocate memory in C-land. However, since finalizers are not guaranteed to run, you end up managing memory objects with Close() functions. This was one of the reasons I rewrote some fairly large Tensorflow projects in Rust.

                          1. 2

                            However, since finalizers are not guaranteed to run, you end up managing memory objects with Close() functions.

                            Hmm. This seems a bit odd to me. As I understand it, Go code that binds to C libraries tend to use finalizers to free memory allocated by C. Despite the lack of a guarantee around finalizers, I think this has worked well enough in practice. What caused it to not work well in the Tensorflow environment?

                            1. 3

                              When doing prediction, you typically allocate large tensors relatively rapidly in succession. Since the wrapping Go objects are very small, the garbage collector kicks in relatively infrequently, while you are filling memory in C-land. There are definitely workarounds to put bounds on memory use, e.g. by using an object pool. But I realized that what I really want is just deterministic destruction ;). But that may be my C++ background.

                              I have rewritten all that code probably around the 1.6-1.7 time frame, so maybe things have improved. Ideally, you’d be able to hint the Go GC about the actual object sizes including C-allocated objects. Some runtimes provide support for tracking C objects. E.g. SICStus Prolog has its own malloc that counts allocations in C-land towards the SICStus heap (SICStus Prolog can raise a recoverable exception when you use up your heap).

                              1. 1

                                Interesting! Thanks for elaborating on that.

                          2. 3

                            So Python, Swift, Nim, and others all have RC memory management … according to you these are not GC languages?

                        2. 5

                          One benefit of GC is that the language can be way simpler than a language with manual memory management (either explicitly like in C/C++ or implicitly like in Rust).

                          This simplicity then can either be preserved, keeping the language simple, or spent on other worthwhile things that require complexity.

                          I agree that Go is bad, Rust is good, but let’s be honest, Rust is approaching a C++-level of complexity very rapidly as it keeps adding features with almost every release.

                          1. 1

                            you can’t have basic features like the C++ file streams that “close themselves” at the end of the scope, then they are destroyed.

                            That is a terrible point. The result of closing the file stream should always be checked and reported or you will have buggy code that can’t handle edge cases.

                            1. 0

                              You can turn off garbage collection in Go and manage memory manually, if you want.

                              It’s impractical, but possible.

                              1. 2

                                Is this actually used with any production code ? To my knowledge it was meant to be more of a feature for debugging and language developers. Rather than a true GC-less option, like the one a language like D provides.

                                1. 1

                                  Here is a shocking fact: For those of us who write programs in Go, the garbage collector is actually a wanted feature.

                                  If you work on something where having a GC is a real problem, use another language.

                          1. 4

                            My counterargument here is that programming can be closer to learning a language than solving and algebra problem.

                            I think it’s all but proven that listening to a language being spoke and reading it (reading code), engaging in conversation and writing (writing code) and trying to navigate the world using said language (solving problems by using a programming language), is the best way to teach a human language.

                            So, unless we want to drop the “language” part of programming, which, I would claim, is the most relevant bit. I think it might make more sense to compare learning programming to language learning.

                            There’s little “theory” behind concepts like what constitutes a good name for a thing, or what level of delegation makes code to fractured to be easily readable. But they are very important concepts nonetheless.

                            Even when it comes to the more “mathematical” parts of programming, there’s still a lot of subconscious thought going on.

                            I think most programmers have a “hunch” when they can optimize a O(n^2) loop to be O(n) or O(n log n), for example. This “hunch” has it’s roots in understanding how various algorithms work (e.g. knowing how complex a search or sort algorithm should be on a given data structure) and knowing how various data structures look and behave. But, at the end of the day, it’s still a “hunch”.

                            Maybe that’s a bit of a trivial example, but a lot of complex problems, I find, are partially solved subconsciously. By recognizing and thinking about some patterns that can be hard to reason about using words, but, once our mind have somehow integrated their detection into our subconsciousness, become really easy to pick up on.

                            1. 6

                              Personally, I’m a fan of pair programming.

                              Both as a learning tool and as a way to generate quality code.

                              Most of the time, I find that the amount of code that has to be written is rather small. However, even when using a “safe” language and/or paradigm like Rust or functional-style Scala and C++, the old idiom holds true and most of the time is spent debugging. Maybe not debugging memory errors or concurrency issues, but fixing faulty logic or benchmarking and improving underperforming code.

                              And I “feel” that pair programming helps with generating a better foundation for that “debugging” phase (or, improvement phase, or whatever you want to call it).

                              However, trying to prove this “scientifically” is an exercise in futility at best as far as I am concerned. There are too many confounding factors when looking at practice differences between teams and companies.

                              Any closed experiment will have the issue of problems which aren’t complex enough and any study of “real” problems will have the issue of confounding factors.

                              I believe programming practices is one of those areas where we ought to reason about things, rather than base them on analyzing data. Same goes for most complex social phenomenons really. You either end up with too much noise or with data that is too unrealistic by trying to experiment in a closed setting.

                              And, again, I say this as someone who loves pair programming and makes a living by analyzing data.