> The effect of layout is specified in this section by describing how to add braces and semicolons to a laid-out program.
I wasn't going to actually read that in detail to see if it makes any sense, though, but it looks very detailed :).
Anecdotally, in my years of haskell at separate places, I had to debug/repl maybe had to repl once or twice. I wish I could say that of mycurrent java gig.
Also, Haskell isn't really ML-syntax. I love MLs but find Haskell syntax pretty ugly.
That might be true for academics. But most engineers don't consider ML syntax to be the cleanest, since most don't know any ML language.
What syntaxes do engineers find clean? I don't understand the distinction you're making.
* Python, Ruby, C#, Java, Go-style languages?
I imagine most developers operate in neither ML languages nor Lisp-style languages.The most advanced "FP" trick that I imagine most developers use is Fluent-style programming with a whole bunch of their language's equivalent of:
variable
.map do ... end
.map do ... end
.flatten
Addendum: or maybe Linq in C#
Addendum 2:And even the fluent-style trick in those languages tends to follow a similar pattern. Using Kotlin and Ruby as examples, since those are my (edit: main) languages,
variable
.map do |i| something_with(i) end
.map { |i| something_else(i) }
.flatten
shows familiar tricks. The dot operator implies that the thing previous to it has an action being done; and curly braces or the "do" operation both imply a block of some sort, and so a quick glance is easy to follow.In Kotlin, this gets a little bit more confusing (yes, really) because it's common to see:
variable
.map { somethingWith(it) }
.map { somethingElse(it) }
.flatten()
And now there's this magic "it" variable, but that's easy enough to guess from, especially with syntax highlighting.Anything more advanced than that and the cognitive load for these language starts to rise for people that aren't deep in them.
When you're starting working with a new language, that does increase difficulty and may even be so much of a barrier that developers may not want to hop over.
Having familiar constructs not only make it easier to code-switch between languages (people that work on multi-language projects know that pain pretty well), but also decreases the barrier to entry to the language in the first place.
This is like saying "most uncontacted Amazonian tribes don't like Shakespeare, because they've never read it". Sure, but why would we care about their opinion on this topic?
https://law.ubalt.edu/downloads/law_downloads/IRC_Shakespear...
The same idea is probably true with programmers who have grown used to C-like syntax or even Python-like or Ruby-like syntax. Syntax is at least in great part a cultural thing and your "cultural background" can affect your judgement in many cases:
1. Are braces good? Some programmers find them noisy and distracting and prefer end keywords or significant whitespace, but other programmers like the regularity and simplicity of marking all code blocks with braces.
2. Should the syntax strive for terseness or verbosity? Or perhaps try to keep a middle ground? At one end of the spectrum, Java is extremely verbose, but a lot of Java engineers (who have generally been exposed to at least one less-verbose language) are perfectly OK with it. The trick is that the main way that code gets typed in Java used to be copy-paste or IDE code generation (and nowadays with LLMs typing verbose code is even easier) and reading and navigating the code is done with the help of an IDE, so a lot of the effects of having a verbose language are mitigated. Diffs are harder to review, but in the Enterprise app world, which is Java's bread and butter, code reviews are more of a rubber stamp (if they are done at all).
3. Lisp-like S-expression syntax is also highly controversial. Many people who are introduced with it hate it with passion, mostly because the most important syntax feature (the parentheses) is repeated so often that it can be hard to read, but advocates extol the amazing expressive power, where the same syntax can be use to express code and data and "a DSL" is basically just normal code.
I find Python to be quite readable as well.
I've said this before but if you remove all punctuation and capitalisation from English it will make it look "cleaner", but it will also make it awful to read.
Look how "clean" text is if you get rid of all the spaces!
https://en.wikipedia.org/wiki/Scriptio_continua#/media/File%...
Clearly brilliant.
Let's look at three implementations of a doubling function in modern languages where at least two of them are considered "clean" syntaxes.
F#:
let double x =
2 * x
Python: def double(x: int) -> int:
return 2 * x
Rust: fn double(x: i32) -> i32 {
return 2 * x;
}
Tying back to my original point, are the brackets, annotations, semicolons, and return really needed? From my perspective, the answer is no. These are the simplest functions you can have as well. The differences get multiplied over a large codebase.Are people here really making the argument that F# and MLs don't have clean syntax? All three of these functions have the same IDE behavior in terms of types being showcased in the IDE and also in the type-checking processes, with F# and Rust's type-checking happening at the compiler level and Python's happening with MyPy. People might argue that I left off type annotations for F#, and that's true, but I did so because they aren't required for F# to compile and have the same IDE features like the other languages. Even if I did, it would look like:
F# with type annotations:
let double (x: int) : int =
2 * x
which is still cleaner. fn double(x: i32) -> i32 {
2 * x
}
The reason Rust requires types here is because global inference is computationally expensive and causes spooky action at a distance. The only real difference between your F# and this is the braces, which I do personally think makes code easier to read.Of course there’s more overhead in smaller functions. I’d never write this code in Rust. I’d either inline it, or use a closure, which does allow for inference, and dropping the braces given that this is a one liner:
|x| 2 * x
Different needs for different things. # Rust
let double = |x| 2 * x;
double(3);
// F#
let double = (*) 2
double 3
-- Haskell
let double = (*2)
double 3
# Python
double = lambda x: 2 * x
double(3)This also works:
let zeroCount = numbers |> Seq.filter ((=) 0) |> Seq.lengthThe answer is no for this trivial function. As your code goes beyond this it gets harder for humans to parse and so the syntax becomes more necessary.
My earlier analogy is pretty perfect here actually. Do you really need spaces in "thank you"? No clearly not. Does that mean you shouldn't use spaces at all?
> Are people here really making the argument that F# and MLs don't have clean syntax?
No. You are literally replying to a comment where I agreed that it is "clean".
and then there’s “do” notation. I has three people tell me “it’s just a simple mechanical transformation from lambdas” oh yeah? how? they couldn’t explain that either
mySubroutine = do
foo <- mkFoo
bar <- mkbar foo
mkBaz bar
mySubroutineV2 =
mkFoo >>= \foo ->
mkBar foo >>= \bar ->
mkBaz bar let x = y in F <===> (\x.F)(y)
just replacing function application with a different function (>>=).People usually check it once, see that it is correct and forget about it.
The indentation rules are definitely a mess though :/
But do notation really is just syntactic sugar for a chain of >> and >>= applications (plus "fail" for invalid pattern matches). It's not usually pretty or understandable to write it that way, but it's a very simple translation. If the people you talked to couldn't explain it to you, I think they maybe didn't understand it well themselves.
Scala's for/yield (which is pretty similar) you can literally do an IDE action in IntelliJ to desugar it.
I forget the precise Haskell syntax, but do {a <- x; b <- y; c <- z; w } is sugar for something like x >>= $ \a y >>= $ \b z >>= $ \c w . Hopefully the pattern is clear. Were there cases where you actually couldn't see how to desugar something, or are you just nitpicking about them calling it a mechanical transformation without being to explain the precise rules?
If you can use >>= and return to do it then "do" is indeed a mechanical transformation.
It is like await if you understand JS promises.
Weird. Haskell's my preferred language and I thought there was only one indentation rule - if something's further to the right it's part of the same expression, and if something's down it's a new expression.
f
x
y
g z
... With a slight addendum that you don't need to write 'let' on consecutive lines, e.g. let x = ...
y = ...I'm glad I'm not the only one. The white-space as syntax in some cases is very confusing. It took me a while to figure it out.
I also don't see the issue, when there is tooling that complains about broken indentation.
There are the actual puzzles that have been minified beyond recognition, and one has to work backwards to figure out what clearer code the author started from. I don't think anyone likes this code, but it gets written by people who are more used to writing code than reading it.
There is also the incredibly clear, straight-forward code that could have come from any other language. If you manage a sane production Haskell codebase, you'll have a lot of this.
In between those two, there is clear code but which you wouldn't find in any other language. This is code that uses well-known Haskell idioms that anyone who works with a lot of Haskell code will recognise, but which look like puzzles to someone who has not worked with a lot of Haskell. These are things like
guard (age person >= 18) $> beer
to return Just beer to someone of age, but Nothing to someone underage; Uuid.fromWords <$> getRandom <*> getRandom <*> getRandom <*> getRandom
to return a randomly generated UUID; maybe (compute_fresh seed) pure cached_value
to use a cached value if it exists, computing it from scratch if it did not; or traverse_ $ \elem -> do
error "stub"
to create a function that loops through a collection and executes a side effect for each element.These look like nonsense to people not familiar with the idioms, but they appear frequently enough that they are no longer puzzles.
Sure, the statements in the function will have side-effects on local variables, but as long as the function isn't too long, and you can comprehend all of it at a time, that's not a problem.
Your examples of "clean code" aren't any cleaner than what can be found in any imperative language.
Using C++ (with std::optional), for reasons of familiarity.
(person.getAge() >= 18) ? "beer" : {};
UUID::fromWords(getRandom(), getRandom(), getRandom(), getRandom());
cache.has_value() ? cache : (cache = computeFresh(seed));
for (auto& elem : collection) {
modify(elem);
}> UUID::fromWords(getRandom(), getRandom(), getRandom(), getRandom())
This ignores that getRandom() is a side-effecting function (it has to be because it returns something different every time). And that's not just a "local variable". UUID.fromWords val1 val2 val3 val4 works in Haskell too, the "extra syntax" is to be able to all that in IO (i.e. with side effects). If you want it in a form that is more recognisable, you could write it as
getUuid :: IO UUID
getUuid = do
val1 <- getRandom
val2 <- getRandom
val3 <- getRandom
val4 <- getRandom
return UUID.fromWords val1 val2 val3 val4
but that's unnecessarily verbose if you know the idiom.Your cache example is similarly side effecting.
I now find that using some combination of Claude, Gemini, and ChatGPT makes using Haskell more pleasant for processing compiler error and warning messages, suggesting changes to my cabal or stack configuration files, etc.
Combination of both the flaws of currying as a concept, as well as the complexity of Haskell's type system.
I have regularly run into code that can only be compiled by a rather narrow range of ghc versions; too old or too new and it just won't.
> Haskell probably has the most elegant syntax across all languages I’ve seen (maybe Idris is better because dependently typed code can become ugly in Haskell really quickly).
I have a general dislike for ML-type syntaxes, and Haskell is my least favorite of the ML-type syntaxes I have encountered. Presumably there is some underlying set of principles that the author would like maximized that I assuredly do not.
> There’s utter joy in expressing your ideas by typing as few characters as possible.
Disagree; if I did agree, then I would probably use something from the APL family or maybe Factor.