Hacker News new | ask | show | jobs
by bla2 3079 days ago
Poor Rob Pike. He always tries to make things simple, and over they time entropy always does its thing. You can hear his frustration in his cited comment.
3 comments

Yeah but Rob Pike's idea of simplicity is him personally not having to implement things. If every one else in the world has to implement the same thing a thousand times a day he still thinks his thing is simple.
I think this is confusing simple (as in simplex) with facile.

Simplicity implies a small set of features; it's a property of design.

Facility implies ease of use; it's a property of operation.

Go unambiguously favors simplicity, at the expense of facility. This is why we're given raw CSP primitives (sharp edges included, e.g.: goroutine leaks) instead of a full-fledged actor model.

What are the differences between raw CSP primitives and ull-fledged actor model?
In an Actor model, you can explicitly send messages to actors (send), monitor their state (executing, failed, completed), and take actions against them (kill, spawn).

A fallout from this is that you can have one actor supervise another, to protect your program from errors: https://hexdocs.pm/elixir/Supervisor.html

The Golang runtime provides only these actions: (spawn). It’s up to the author to create communication channels, and it’s impossible to query for the status of a Goroutine. This means you have no explicit control over goroutines, so you can’t kill misbehaving ones like you could in another language.

I feel raw channels provide more possibilities than Actor model. In fact, you can build such an Actor model by warpping CSP channels. It is not too hard.
Yes, I think this is precisely the idea.

Anecdotally, 99% of the complaints I read WRT Go can be solved by using libraries that already exist. If one wants a batteries-included-in-the-stdlib form of facility, one should look at languages like Python.

Actor model cannot be implemented wrapping CSP, because CSP is bounded and synchronous, while Actor model is unbounded and asynchronous. But you can implement CSP on top of Actor model.
In Go I can write to a channel that nothing will ever read, and I can attempt to read from a channel to which nothing will ever write.

In a pure actor model raw channels wouldn't be directly available and I would be required to push to and read from a concrete goroutine.

I wish I could upvote this comment a dozen times.

Much of go’s “simplicity” is a Faustian bargain that comes at the cost of unnecessary complexity in each and every project that winds up being written with it.

This is called trade-off. The reality world is never perfect.
Okay, but a trade-off that forces complexity into hundreds of thousands of programs to avoid complexity in one is poorly-conceived.
Care to illustrate your point with an example? I’m wondering what kind of “unnecessary complexity” you’re talking about.
I mean, generics are kind of go's whipping boy. Lacking generics means you end up with copy/pasted code for utility functions that should be part of the go stdlib in virtually every project. It also means copy/pasted code for any data structure you might want to use that's fancier than an array.

Go's "simplicity" of error handling (read: lack of any actual error handling abstractions) means you don't get useful things like stack traces and have to manually grep through code for nested error messages. It also makes go code difficult to read at a glance, since virtually every statement winds up wrapped in repetitive error-handling code that doubles or even triples the amount of code in the happy path.

The error-handling pattern of using tuples, but no syntactical ability to operate on data within a tuple means you almost never have the ability to chain function calls like `a.b().c().d()`. Instead you have to manually unwrap the value and error, return if there's an error, call the next function, manually unwrap the value and error, ad nauseam. The "idiom" of gift-wrapping error messages is absurd — you are replacing machine-based exception handlers with expensive, slow, error-prone, and less-capable meat-based exception handlers.

Having a half-baked type system means you end up having to frequently write type-switches which are checked at runtime to do any sort of generic code. There's no functionality in the language to ensure that all possible options for that type switch are exhausted, so you are virtually guaranteed to get runtime bugs when a new type gets written and later is passed in.

Speaking of type switches, they interact poorly with go's indefensible decision to have interfaces implemented implicitly rather than explicitly. I have seen types get matched to the wrong typeswitch in producion code because a new method implemented on one type caused it to accidentally "implement" an interface used elsewhere in a typeswitch. Good luck ever catching this before it hits you in production.

Go's concurrency primitives are useful, but the lack of ability to abstract over them means that you have "advanced go concurrency patterns" dozens of lines long and involving multiple synchronization primitives for what amounts to `a | b | c` (https://gist.github.com/kachayev/21e7fe149bc5ae0bd878). God help you if you want to implement something like parallel map. God help you if you want to implement something like parallel map for n > 1 types.

Go requires you to manually remember to release resources you've acquired with `defer`, instead of sanely having There is no capacity in the language to enforce that you've done so, and it is virtually impossible to find e.g., a missing `defer fd.Close()` in a large code base. God help you if you leak file descriptors and need to track down the source.

Go's inability to perform any meaningful abstractions also means that you have to know all the details of code you import. It's difficult to make code a black box. Case in point: to do something as painfully simple as reading a file, you need to import bufio, io, io/util, and os.

During the course of writing this post, I forgot more examples than I listed — I literally could not remember them all in my head as I was writing them down. This isn't simplicity, this is utter madness.

In the same spirit of my above comment, I think you're conflating the terms simple and easy.
Yes, it is a tradeoff. Why do you think it's a good tradeoff?
I think the point he's trying to make is that it's a poorly-justified trade-off.
The cost of complexity in a programming language is paid not only by the developers of the language but also by the programmers that use it.
The cost of poorly designed complexity maybe.

This is the same mentality as people who throw up their hands and say government is broken, so we should deprive it of resources to make it as small as possible. Doing this just winds up makes the problem worse, when there’s plenty of evidence that well-funded governments can work well.

It’s also the same broken mentality behind schemaless databases. Schemas are hard, so let’s get rid of them. This backfires because you haven’t actually rid yourself of schemas, they’re just implicit and now you lack any tools to operate on them meaningfully.

“Hard problems are hard, so let’s just avoid dealing with them” is not a sustainable solution in the long term. Sometimes they’re really hard and ignoring them makes it worse. Sometimes they’re only hard because we haven’t thought about them in the right context. And sometimes hard problems can be sidestepped entirely with a bit of cleverness. But outright ignoring them and hoping they go away just punts the hard problems to others.

If every one else in the world has to implement the same thing a thousand times a day he still thinks his thing is simple.

Smalltalk all over again!

Actually even Smalltalk is more feature rich than Go.
The community had a real "Not Invented Here" problem in our earlier years, which we never really overcame.
I see, as I only used Smalltalk during university for project assignments (Digitalk Smalltalk/V), I never got to experience that.
All languages that tried to fight complexity either grew up to adopt complexity and stay relevant, or faded away.

Programming languages don't get complex just for fun, their designers are tackling actual relevant issues.

Go community doesn't seem to have learned much from the past.

> All languages that tried to fight complexity either grew up to adopt complexity and stay relevant, or faded away.

> Programming languages don't get complex just for fun, their designers are tackling actual relevant issues.

Yes and yes.

> Go community doesn't seem to have learned much from the past.

Well... if you start with something simple, and complexity comes, you can still try to keep it as simple as possible. But if you started with something that was already complex (but complex in ways that your theory said it needed to be, not in the ways that the real world said it needed to be), and you try to fix that, you wind up with something really complicated. Ditto if you start off with complexity to handle all the use cases of the past.

Go started off simple, and is letting real-world use push them into becoming more complicated. That's a defensible approach, even today.

> That's a defensible approach, even today

Not if you are dishonest about it. Not if you refuse to learn from the past couple decades.

In what way is Go dishonest about it? In what way have they refused to learn from the past couple decades?
I think insisting that they need more use cases for generics is dishonest when you consider they used generics to implement library data types themselves.

I don't mean they are outright lying, or are bad people or anything like that.

I suspect the Go developers just have a very different idea of what the past is. Given the state of software engineering, I'm not quite as likely to put a positive spin on the things we've built since then.
This is true for languages that try to be all things to all people (a la Java). All languages are DSLs, and if you target just a few specific domains and beat back the masses who want the language to expand beyond its intended purpose, than simplicity remains possible.
DSLs shouldn't be turing complete and turing complete languages shouldn't try to be DSLs.

Ant was a DSL that managed to become turing complete and the results were pretty horrible.

Turing completeness is a symptom, not a cause. No one would argue that SQL is bad because some implementations are Turing-complete [0].

[0]: https://stackoverflow.com/a/7580013

>No one would argue that SQL is bad because some implementations are Turing-complete

They do actually. Though when people do say that it tends to be phrased "keeping business logic in stored procedures is a bad idea".

People argue that all the damn time.

Accidental turing completeness usually signals a design flaw somewhere (would you also consider it too controversial to argue that C++ templates mentioned in your link are nasty and people complain about them a lot?).

>All languages that tried to fight complexity either grew up to adopt complexity and stay relevant, or faded away.

And yet, to this day, C is just as, if not more popular, than C++. Why is that? I can do so much more in C++, but I, and my colleagues, pick plain-old C every time.

Thanks Linux.

C was already on the way out when Linus created Linux.

Apple was migrating from Object Pascal to C++.

IBM had CSet++ for OS/2.

Borland, Microsoft, Zortech, Symatec were selling C++ frameworks.

UNIX vendors were playing with Taligent and CORBA.

BeOS and Symbian were developed in C++.

Then came Linus, made Linux with GNU on top.

GNU project for a long time always mentioned that the go to language for GNU projects should be C.

All major C compilers are written in C++ nowadays, there is hardly any reason to stay with C outside UNIX world.

C was already on the way out when Linus created Linux.

That seems a little... fanciful. There was a lot of C++ and it was a great way to show how modern and forward-looking you were (and to sell compilers, tools, frameworks) but standardization hadn't got far, interoperability was poor, problems great and small abounded. A number of the things you mention above were spectacularly unsuccessful.

Yes, C++ was a pain to write portability before 2000, but so was C, in spite of having been standardized in 1990, most compilers were a mix of K&R C and ANSI C.

Nevertheless, all major desktop OSes were going C++ for their application frameworks, before the widespread adoption of GNU software.

> A number of the things you mention above were spectacularly unsuccessful.

Mostly due to politics between corporations and very little to do with C++ itself.

> but standardization hadn't got far

That's true, but not really fair: even ANSI C standard was only 2 years old at that time, despite C being way older than C++. Standardization takes a lot of time …

You're in the minority. For new projects, C is much less popular than C++.
It might not be that easy to tell. The C++ I write (for myself) is essentially plain C. No templates, dynamic dispatch, constructors, or exceptions. Most of the standard libs I use begin with the letter 'c'.

It uses some C++ features, but it's philosophically much closer to C code.

If it only compiles with a C++ compiler, it is C++, regardless of the amount of language features being used.
That's fine, but I don't think that's what people mean when they say C++. I certainly wouldn't call myself a C++ coder and, if I applied for a C++ job, I'm pretty sure that, after I had explained that I don't do exceptions, virtuals, or the STL, I'd be politely shown the door.
> Programming languages don't get complex just for fun, their designers are tackling actual relevant issues.

Have you ever used C++ templates? I mean every popular languages have issues related to design complexity.

Some complexity is avoidable, other isn't.

Besides C++ templates are a result of creating a conceptually simple, one size fits all solution for generics, metaprograming, library tuning, and some dozens of other problems that other languages have specialized tools to solve. Turns out that the complex set of features works better.

Is it a complex set of features, or rather a set of focused tools?
It is a large set of simple tools. It is conceptually complex because each tool is different and you must learn them all.
I did my first C++ steps with Turbo C++ 1.0 for MS-DOS.

My first use of C++ templates was in Turbo C++ 3.5 for Windows 3.1.

Go's community doesn't learn from the past, but good languages fight complexity even as they add expressive power. (They at least try to get the most expressive power per complexity cost.)

Pragmas, incidentally, aren't really a source of bad complexity. Per the abstract definition of the language, they really indeed do have no effect at all and are just comments. Yay!

Implementations have properties too—they aren't just rude practicalities. Compiler's, in particular, connect one language (the input) to another (the output). Programas mediate how those additional properties apply to the language at hand. It's an interesting mental challenge to formalize them in the absence of a compiler having an comcrete stable ABI.

So in conclusion, go people once again don't understand good design. Pragmas are not an ugly wart, but actually a great example of layering—a rare example of an abstraction that doesn't leak!

I wouldn't go as far to say that having undocumented magic comments doesn't add complexity. From a very surface level, sure, the parser is the same, but now every tool that works with the Go language needs to be aware of these. Linters, for example, need to not complain about the missing space in front of the comment, but only if the comment starts with "go:".

Ultimately anything that changes how the program is executed is going to add complexity, so they might as well "make it official" and add a keyword for it.

In Perl and Javascript pragmas are language level and are used to help people avoid some mistakes at certain stages of software development. This is fine, no leaky abstractions. In Go they are lower level and therefore are side effects of leaky abstractions in compiler and language design. So they should be fixed, not kept or turned into pragmas in the spec. The choices I can think of: either make Go lower level itself or move low level stuff into another intermediate lower level language.
There's another choice, which is to keep doing things the way they are being done, simply not add another 20 pragmas, and get on with life because there isn't actually a problem here. None of the problems with pragmas I've seen in other languages are present in Go, since the pragmas are simple and mostly used only by the implementation and/or compiler itself, and there's no interactions, or massive code complexity from ifdefs, or string-concatenation-based macro disasters, or any of the other real problems caused by pragmas, with the possible faint exception of pragmas not being cleanly delineated from the comment syntax, which is still not causing any huge problems I can see, nor is that likely to change in the future.

The problems that C has with pragmas, and that C++ imported from pragmas, can not be naively imputed to other languages without demonstrating there's actually a problem here. This wouldn't even make my top 10 issues with Go; I'm not sure it's even an issue at all.

> In Perl [...] pragmas are language level

No, actually. The syntax that people use to invoke the pragma ("use strict [arg]...") is not a pragma at the language level, it's just the syntax for importing symbols from modules. For example,

  use strict ('vars', 'refs');
expands to

  BEGIN { require 'strict'; strict->import('vars', 'refs'); }
because that's how the "use" statement is defined. `BEGIN{...}` cause the statements in the block to be executed as soon as the BEGIN block has been fully parsed [1]. `require 'strict'` loads the module `strict.pm` from the library path (the source code is on CPAN at [2], if you're interested), then its `import()` method is called with two string arguments. The implementation of that method is:

  sub import {
    shift;
    $^H |= @_ ? &bits : all_bits | all_explicit_bits;
  }
There's a lot of weird Perl syntax in there, but the gist is that it modifies the $^H variable. And THAT is the actual pragma which is defined by the language. [3] The module strict.pm is just a wrapper around $^H to make things a bit more user-friendly.

I know that's sorta kinda off-topic, but since we're talking about language design, I figured I'd contribute this small anecdote that illustrates really well how the more recent parts of Perl are designed: a ton of metaprogramming on top of relatively small changes to the core language. If you want another example, have a look at how object-oriented programming was tacked on to Perl as a tiny afterthought, yet the way it interacts with all the other parts of the language makes hugely powerful OOP frameworks like Moose possible. (OTOH, that approach also makes the language pretty messy, but it always gets the job done for me, at many scales.)

[1] Usually, execution only begins when the entire file has been parsed, but this code needs to run earlier because it changes the parser's behavior.

[2] https://metacpan.org/source/SHAY/perl-5.26.1/lib/strict.pm

[3] Notably, $^H behaves differently from other variables: Every assignment to it is scoped only to the current block, whereas regular variables need to be shadowed explicitly. This is particularly useful to temporarily lift a strictness requirement for a single statement, similar to how `unsafe` is used in Rust:

  use strict;
  ...
  my $function_name = 'implementation_' . ($x + 2 * $y);
  $function_name(); //error: cannot call string value
  {
    no strict 'refs'; //"no" is like "use", but in reverse (calls the module's unimport() instead of import())
    $function_name(); //works: calls the function with the name stored in the variable
  }
Sure, this is all correct from implementation perspective as levels are not strictly defined and are open to interpretation. However, we were talking about users' perspective, which is kind of the whole point of leaky abstractions. In this context language level features are those that users can understand about the language without any assumptions about other levels, like how compilers represent things internally.
The word pragma in perl has always been used to refer to that specific syntax sugar, just read the start of "perldoc perlpragma".

Furthermore the $^H (there's also %^H) facility you mention is just one way pragmas are implemented, e.g. "use overload" is a core pragma that doesn't use that method at all, instead it defines special functions in the importing package which the compiler is aware of.

Then there are other "pragmas" that are really just utility wrapper functions, e.g. "use autouse". The "Pragmatic modules" section of "perldoc perlmodlib" has the full list.

Guy Steele, "Growing a Language", https://www.youtube.com/watch?v=_ahvzDzKdB0
Lisp?
His concern was directed at the magic comments though. I don't understand why they didn't just create new syntax for pragmas since they're already parsing something. e.g. #noescape

# could be treated as syntactic sugar for //go: until v2 if they want