It makes me think that it's worth sitting down and considering what all the valid outcomes for a piece of functionality are. A user typing in a string in the wrong format is not necessarily "exceptional", whereas running out of memory while getting the input would be. I feel like programmers too often treat perfectly valid outcomes to be errors. For example, in Rust I'll see Option<Vec<Foo>> and I ask myself if we could just use the empty vector as a valid return value.
Rust shines in user-space systems-level applications (databases, cloud infrastructure, etc.) but definitely feels a bit out of place in more business-logic heavy applications.
I was really interested in lisps for a couple of years, but eventually I came to the conclusions: it's just hard to read. I know they say "the parens disappear" but even if that is the case, it simply requires you to jump around the expression holding lots of context in your head. I'm a fan of code that mostly reads top-to-bottom, left-to-right.
Lisp code is written top to bottom, left to right. Is your grievance more to do with expression-oriented—as opposed to statement-oriented—languages? "Do this" (statement) vs "represent this" (expression)? For instance, do Haskell, OCaml, etc. also irk you in similar ways?
(defun f (x)
(let ((y x))
(setf y (* y x))
(block foo
(if (minusp y)
(return-from foo y))
(loop :for i :from 1 :to 10 :do
...
This is absolutely typical bog-standard left-to-right top-to-bottom structured programming type code. It also must be executed like so:
- Define the function
- Bind the variable
- Mutate the variable
- Set up a named block
- Do a conditional return
- Run a loop
- ...
The order of execution literally matches the order it's written. But not unlike almost all other languages on the planet, expressions are evaluated inside-out.
Haskell's whole raison d'etre is to allow arbitrary nesting and substitution of terms, and all or none of these terms may or may not be evaluated depending on need. De-nesting happens with a copious number of syntax to bind names to values, sometimes before the expression (via let), sometimes after the expression (via where), and sometimes in the middle of an expression (via do).
Not speaking for all Go programmers, but I think there is a lot of merit in the idea of "making zero a meaningful value". Zero Is Initialization (ZII) is a whole philosophy that uses this idea. Also, "nil-punning" in Clojure is worth looking at. Basically, if you make "zero" a valid state for all types (the number 0, an empty array, a null pointer) then you can avoid wrapping values in Option types and design your code for the case where a block of memory is initialized to zero or zeroed out.
Only if you ignore the billion cases where it doesn't work, such that half the standard library explodes if you try to use it with zero values because they make no sense[0], special mention to reflect.Value's
> Panic: call of reflect.Value.IsZero on zero Value
And the "cool" stuff like database/sql's plethora of Null* for every single type it can support. So you're not really avoiding "wrapping values in Option types", you're instead copy/pasting ad-hoc ones all over, and have to deal with zero values in places where they have no reason to be, forced upon you by the language.
And then of course it looks even worse because... not having universal default values doesn't preclude having opt-in default values. So when that's useful and sensible your type gets a default value, and when it's not it doesn't, and that avoids having to add a check in every single method so your code doesn't go off the rail when it encounters a nonsensical zero value.
[0] or even when you might think it does, like a nil Logger or Handler
That's exactly the problem. Thanks for describing! What I find is people using linters to ensure all struct fields are initialized explicitly (e.g. https://github.com/GaijinEntertainment/go-exhaustruct), which is uhh...
I mean, yeah, you can avoid wrapping things that are optional in Option, but that doesn't make that the result is semantically meaningful. If you have a User struct with "age: 0" does that mean:
1. The user being referred to is a newborn, or:
2. Some other code, locally or on a server you're reading a response from, broke something and forgot to set the age field.
It's not just Rust that gets this (in my opinion) right, Kotlin and Swift also do this well at both a language and serialization library (kotlinx-serialization and Codable) level, but this quote from this article comparing Go and Rust is generally how I think about it:
> If you’re looking to reduce the whole discourse to “X vs Y”, let it be “serde vs crossing your fingers and hoping user input is well-formed”. It is one of the better reductions of the problem: it really is “specifying behavior that should be allowed (and rejecting everything else)” vs “manually checking that everything is fine in a thousand tiny steps”, which inevitably results in missed combinations because the human brain is not designed to hold graphs that big.
I like the goals of this language a lot and I've wished something with these goals already existed. But, I'm not sure if this syntax/approach is quite what I want. Really cool project, though!
I have always felt like Swift is the king of application development. The syntax and ergonomics really lend itself to UIs and the like. It's a shame that the Swift compiler is on the slow side.
The best way I've found to mitigate the compiler speed issue is to factor my application workspace into different sub-projects, so that the sub-projects don't get built every time I make a change. It makes a huge difference and, honestly, it's something I should have been doing anyway, just for good architectural layering.
It seems to me like parser combinators are always more trouble than they're worth. People often have the impression that parsing is difficult and should be outsourced to another library, but often it's pretty simple to hand-roll and usually it makes faster code.
I agree with the sentiment of this article but the question that fascinates me is "when do you need a language feature instead of a library in order to accomplish X, Y, or Z?"