> Languages like Go and Elm spurn extravagance. They resist overcomplication. They force me to solve real problems instead of fighting compiler errors and stylistic differences.
I hate this mindset. "<my favorite language> is used to solve REAL problems, other languages are not as good for creating VALUE. Any powerful language features, ergonomic syntactical sugar or tooling that does not exist in <my favorite language> is useless". So strange
I think you're just reading it as "Elm/Go good, $yourFavLang bad".
Maybe this makes more sense:
Coming from more expressive languages, one of the good things I could say about Go's developer UX is that you are so helpless to circlejerk over abstractions that there is simply nothing else to do than to write concrete code to solve the problem.
Elm is similar versus the huge type expressive power of Haskell. Elm's type system is so minimal compared to similar languages that you can't spend hours perfecting your higher kinded advanced type abstraction that perfectly annotates and cordons off the problem domain because there isn't enough typing power to do that. You have to go "welp, good enough" and move on.
Not to diss on Haskell, but to illustrate the difference: There's an entire HaskellX 2022 talk about "language extensions we use, extensions we are cautious about, extensions we never use". There are many ways to solve problems in Haskell; and mostly only one or two ways to do things in Elm. The most choice I can think of is in context of parent-child communication (Config pattern, OutMsg, flat Msg type). Most everything else has a single obvious solution.
Yes, sometimes the simplicity of the language leaves you wanting a bit more power (typeclasses, macros) but the code quality seems (to me) better with the constrained language than with the unconstrained one.
That's an unfortunate problem that Go has, admittedly; it's far from a perfect language. I'm just glad they resist adding language features for isolated use cases.
One example of code generators is enums, I'd love for Go to add that as a feature, I frequently use them in other languages. Go's equivalent is a list of consts with a shared prefix, e.g. StatusOK, StatusBadRequest etc for HTTP error codes [0], then functions to do anything with them (like getting a status text for the aforementioned HTTP status codes; see [0] and scroll down a bit).
I mean it's not difficult code; nobody has to learn any new syntax or methodologies to understand enums if they already know consts / variables, functions and switch/case statements. It's just inelegant and it feels unsafe - you need a linter [1] to check for exhaustiveness of the aforementioned switch/case, on top of a ton of other linters [2] that IMO should be language features or compiler checks.
My personal gripe is struct tags that are just strings that no tooling or compile-time checks will check, they're left up to a library (or the standard library) to interpret at runtime. In other languages like e.g. Java they introduced annotations to add metadata to properties.
In my experience, real-world Go can be just as complicated as real-world Python or C++. The lack of abstractions is making the problem worse, if anything.
Regarding Go, it didn't have generics until recently, partly because some of the community didn't think it was beneficial. They've been nothing but positive in our project as we've been able cut down on substantial amounts of boilerplate in some areas.
Isn't that the grandparent's point though? Features that other languages have are first considered "not beneficial" and are in fact mocked as being useless or extravagant. Then, once said feature is introduced to one's favourite language, the feature is considered useful and it turns out that maybe those other languages had a reason for including them beyond mere extravagance.
> partly because some of the community didn't think it was beneficial
And partly because it needed more thought. I'm all for more well thought out features, but there's also value in a language that doesn't move fast and doesn't break things. And I say that while Python is my main work horse, so I understand what breaking things mean.
This post from Russ Cox was really insightful to explain that Go's generics and the delay in implementing them were a technical issue, not a political or opinion based one:
> We have spoken to a few true experts in Java generics and each of them has said roughly the same thing: be very careful, it's not as easy as it looks, and you're stuck with all the mistakes you make.
The Go team does their homework, and they do it thoroughly. I respect that more than anything about the language. Compare with Java, that had been stagnant in committees for nearly a decade, or with Javascript, that has been bolting on features left and right; I'm mainly thinking of the half-baked OOP addition.
It’s funny you call out Java, when it is exactly the language famous for taking advantage of being the last mover — even its original intros mentioned it!
Also, languages don’t start in a vacuum, they shouldn’t have to go through the whole process from zero each time — Go absolutely failed by not incorporating it from the first version.
The Go developers, many years before implementing generics, made it clear they were open to the idea. It was more about how it would fit into the language and them agreeing on it.
To their credit, they don't just throw things into the language because it's trendy or "cool" at the moment. Even if people disagree with their decisions, they are well considered and thought about decisions.
I'm gonna take a bit of a "people-pleaser" stance on this and say that there's merit in both mindsets.
Personally, I tend to prefer using and abusing all the sugar and tools that a compiler gives me. I find that I'm not a terribly smart person most of the time, and if a compiler engineer has a figured out a good bit of abstraction to make the code safer or more readable or faster, I'm inclined to use it.
There are plenty of success stories with this approach. Love it or hate it, I think SQL is overall a reasonably pleasant language, and almost completely removed from the underlying hardware. You think relationally with SQL, not really in terms of for loops or memory allocation.
All that being said, I will admit that sometimes I don't want to spend the entire day trying to decipher whatever the hell GHC is trying to tell me with its weird errors. Go makes you do a lot more manually, but at the same time you also don't need to understand the intricacies of a sort of approximation of type theory or linear logic.
IMO, I genuinely think that right now the two best languages in terms of abstraction usability are Clojure and F#. They both allow for lots of great abstract stuff, but they also do allow you to cheat when necessary, and being on the JVM and .NET Framework respectively, there's no shortage of libraries available.
It's something that you can fall into pretty easily imo, but after you dive into a few languages and realize you just weren't aware of the tool chains, librarys and platforms you begin to realize familiarity accounts for most of your enjoyment.
I like golang a lot and use it professionally and there are plenty of errors and issues that can require syntax and code that to people who aren't super proficient in it would think is over-complicated and end up fighting the compiler (channel can be awkwardly implemented and related issues and what not can be.... non-obvious..).
I've been writing a ton of rust recently and until I became proficient I felt it was really complicated and led me to fight with a compiler (borrow checker I suppose) and honestly it took me coming back to it a second time almost a year later to break through it and now i'm as productive in rust as I am in golang... but it was my familiarity, I found tools I wasn't aware of, my SQL query strings & templates are now checked statically and so I feel more productive in rust in some ways.
> <my favorite language> is used to solve REAL problems
That is really not the point being made. The point is rather "the language helps me keep focused on what I do, because many subtleties in other languages don't exist, and those have me overthinking things". Whether what you do is REAL work or a friday night game jam is irrelevant.
Also I wouldn't say that Elm lacks ergonomics or powerful expression features. It does lack browser API features, but I don't believe that's what the author enjoys.
I think there's a case to be made for simpler languages, but not the way the author portrays it here, and not the way eg Go does it.
IMO the biggest problems with almost all popular programming languages are
1) null
2) exception based error handling
such that, when you call foo() where foo is
String foo(){ blabla }
you can get a String, null OR an exception (!!!) and most compilers happily let you treat it as if it only ever returns a String.
I hope some day null is no longer a thing, and that Functional Programming types like Option, Either, Try etc in the native libraries is the new default.
incredibly, there's still no consensus as to which exceptions should be used for what and when, though these days the most common approach is to simply stick only to RuntimeExceptions, which is terrible.
Functional error handling using Option, Either, Try etc are arguably much simpler, safer and more powerful than exceptions.
Simpler because they don't rely on dedicated syntax- they're just regular objects no different to any other object.
Safer because unlike exceptions, they force callers to handle all potential outcomes, but no more. (no risk of ignoring errors and no risk of catching a higher level of error than desired, ubiquitous bugs in exception based error handling)
Powerful because they support map, flatmap, applicative etc, making it easy to eg chain multiple computations together in desired ways, which is unwieldy and bug prone when using exceptions.
It could be that, when learning Java, Python and any other language, we learn that methods return objects... and that's that. No weird dedicated syntax and magic, special treatment for returning anything other than the happy path, and the HUGE complexity that comes with it, eg the dedicated syntax itself and how it behaves, differences between checked and unchecked exceptions, hierarchies of exceptions etc etc.
In addition to that, when lists and booleans implement map, flatMap etc, you can actually reduce syntax of languages even further- there's no need for looping syntax like for, while etc, and no need for if else either. This is probably too extreme for most people, but think about it. There's literally no reason to have this syntax in the language if the types in the library give you the same functionality.
So my dream languages would be something like Kotlin ie with inferred type safety, immutability by default etc but without support for null at all, no exceptions at all, no looping or conditional syntax, and a better, smaller, simpler library.
I like exceptions. You can't get away from them, I know Haskell tried, Rust tried, they still have exceptions (perhaps they call them by a different name though). May as well make that system generally available to programers. They also allow you to have the Erlang/Elixir philosophy of happy path coding, "just let it fail", by using a high level exception handler and then just coding the happy path underneath.
The problem is that virtually all languages abuse exceptions and use them for very much expected control flow and business logic. But exceptions should be just that, exceptional. Eg when you make a db call, the db not responding is not exceptional. Nor is not finding the user you're looking for. Those are 100% within the range of expectable outcomes for a db call.
fun findUser(userId: String) : Either<Error, User>
is MUCH simpler and more powerful than
fun findUser(userId: String) //haha, I can actually throw exceptions that aren't in this signature, or return null (wat)
I hope you never program any safety critical machinery. I don't mean exceptions cannot be useful, just that there are circumstances where they are useful and circumstances where they are not.
>I like exceptions. You can't get away from them, I know Haskell tried, Rust tried, they still have exceptions (perhaps they call them by a different name though)
Rust has no exceptions in any sense of the word. Exceptions are ways to crash the program in a controlled manner with the ability for these crashes to be caught midway before the program crashes and handled correctly.
In rust you can only crash it explicitly using the panic! Macro. No handling.
The different name you are looking for is "errors". Rust has errors as do all programs. Some programs have exceptions to handle those errors. Rust does not.
If you mean "runtime crashes" then elm is the only language that is popular that uses has no runtime crashes.
> Exceptions are ways to crash the program in a controlled manner with the ability for these crashes to be caught midway before the program crashes and handled correctly.
> In rust you can only crash it explicitly using the panic! Macro. No handling.
You can catch Rust panics with `catch_unwind()` if you compiled your program with `-Cpanic=unwind` (default on most platforms). Under the hood it uses stack unwinding, just like C++ and co's exceptions. But Rust doesn't consider this idiomatic error handling, it's only used in a few special situations (test frameworks, web servers where one buggy session shouldn't take down the process...).
You absolutely can catch and handle a panic in Rust [0]. Panics are just exceptions by a different name. Although, I do agree that the Rust ecosystem tries hard to avoid using panics, which is good.
Catching a panic is _morally_ equivalent to catching a SIGSEGV. It is not meant for normal program error conditions and expected failures (e.g. "No such file").
It's not exactly cursed knowledge, you just have to have enough judgement to understand where it's appropriate.
Trying to do magic and patch something on a panic and retry is cursed indeed. If it's about cleaning up resources of a thread and logging this somewhere, then there's nothing cursed about this.
As an application gets more complex what starts to matter is general design, software architecture, performance, compile times and language agnostic features.
Obsession with language features or syntax tends to move people towards building castles in the sky and being too obsessed with code rather than the software that actually runs on a physical machine. A lot of fancy features usually come at quite high runtime or compile time costs with not really meaningful benefits in a large software system.
Unless you're writing software explicitly for the sake of staring at code, unopinionated and simple, fast and pragmatic languages that get out of the way are the tool of choice for good reason.
I definitely don't think ergonomics and features from other languages are useless! I just feel a bit overwhelmed and distracted by variety sometimes, especially when working on teams.
For example, about 50% of the JS teams I've worked with collectively agreed not to create new classes. I think I'd prefer working with JS if there were only one way to do things, but it doesn't mean that classes are bad or useless.
I like Elm but there are some things that would make life easier and not add much cognitive overhead, like updating nested records for instance. The current syntax wouldn't even need to change.
It's a valid assessment depending on the context. There's a lot of software scaffolding that exists in places that, usually for good intentions, creates complexity in the hope of simplify the problem at hand. Sometimes it works, sometimes it doesn't. When it works, it's great. When it doesn't work, it adds yet another layer of complexity to deal with whatever problem you're trying to actually tackle.
Ultimately it comes down to trying to do a cost/benefit analysis of the complexity at hand and deciding if the additional layers add enough value and how far your problem deviates from the supporting structures. The issue is, it's often quite difficult to know a priori what is helpful and what just adds complexity because goals are often moving targets and you have to try and assess the range of potential goals with the flexibility of all the middle layers you introduce.
I read this as an expression of preference not a condemnation of other languages.
The full context helps
Most languages are too powerful for my palate.
Don’t get me wrong – I love Rust and many other languages! But sometimes they’re just too much for me.
When writing Rust or JS or Haskell or Python or Lisp, I’m overwhelmed by opportunity. Should I make this generic? Should I use classes or structs? Immutable or mutable? Macros? Functional or imperative array manipulation?
I try to please compilers and coworkers and customers, but all are disappointed. Give me a woodshop and I’m lost, but give me a simple chisel and I intuitively know what to do. There’s a certain freedom in restricted toolsets.
Languages like Go and Elm spurn extravagance. They resist overcomplication. They force me to solve real problems instead of fighting compiler errors and stylistic differences.
Furthermore, consistent code makes portable mental-models. Go and Elm codebases tend to be extremely readable.
Speaking as a dude who loves legacy Lisp - I once wrote an interpreter for it in Elm. I was disappointed to find there wasn't enough parenthesis in Elm! Sometimes the bell and whistles get in the way of my creativity and flow
I hate this mindset. "<my favorite language> is used to solve REAL problems, other languages are not as good for creating VALUE. Any powerful language features, ergonomic syntactical sugar or tooling that does not exist in <my favorite language> is useless". So strange