Although I read all of it, really? I'm in the syntax obsessive set.
Interesting to read about semicolons improving comprehension -it felt like it had been decades since somebody had the courage to say so. (That said I write python un functionally and so am 4space tabbed to the max)
It's also the first time I've seen "currying is bad" which seems to run against the flow of salmon.
As a non-lisper, it always kind of amused me that vi had brace-detection in the % command. I mean, what self-respecting C coder doesn't know where the other bracket is.. oh wait. ok. That's useful for me.
Then you wind up in emacs, editing a .emacsrc file, marvelling at the .. depth of germanic (((( ... things) which need to be said)) by this person) nesting.. wondering what you do...
> Interesting to read about semicolons improving comprehension -it felt like it had been decades since somebody had the courage to say so. (That said I write python un functionally and so am 4space tabbed to the max)
I think it has a lot to do with the writer not using blocks. It would solve both of their issues (with match and with if and semi-colons which is actually an issue of precedence)
Some people seem to have a lot of trouble with the BCPL/Pascal syntax using begin and end.
The author apparently doesn't know about "|>" either which would solve their sequencing issues.
At least this post was written in good faith. I think this highlights that there is a dearth of recent material promoting best practices when it comes to Ocaml.
> So when you make a mistake, you’re still giving constraints to the inference engine. Erroneous type inferences propagate in every direction. Error messages appear hundreds of lines of code away from their origin.
Rust gets this right: it has type inference, but only within the function body. Most of your code is free of annotation cruft, but interfaces are strongly specified and errors can only propagate a certain distance.
That's why it's idiomatic in OCaml to write interface files which contain type annotations for the modules. In practice many or most modules (over time) are given interface files which strongly contain the area of effect of type inference.
Probably if you need to do that it should just be enforced by the compiler. It's pretty clear to me (and the author of this post) that Rust got this right - small scale type inference is fine (in function bodies) but you really want explicit types for functions and up.
Without that, you lose out massively on readability and error message quality.
In Sail, I have IDE type inlays working but it barely helps with reachability because half the types are inferred as generic.
As a user of OCaml I have to disagree. Rust requires far too many type annotations and they are mostly unnecessary and clutter everything up (particularly with it's very heavy syntax). In OCaml my editor shows me types if I mouse over a variable, and if I need to document the code I can add annotations, but that is rarely needed.
I know this is an unpopular position in the OCaml ecosystem, but I find .mli files to be in all ways inferior to the "inline" approach Rust (amongst others) take. OCaml requires perpetual context switches between .ml and .mli files, which can be alleviated with proper IDE tools but is still annoying, but most annoyingly requires writing every type definition and module signature twice.
On the other hand the OCaml/interface files approach is superior when you want to read documentation w/o implementation, but in practice using odoc is superior because you get hyperlinks.
In 10 years of using OCaml, I think everytime I interacted with mli I'd rather the author annotated types at the definition site.
That's funny because I feel the opposite. An interface file saves me from having to wade through the implementation details and lets me focus on the exact parts that are relevant to me as a reader of the code. And yes odoc is nice too but the source of its usefulness usually comes from the fact that it's generated from a carefully curated interface file.
Would type "dehints" be the best of both worlds: you IDE could hide all the types (just like with type hints it adds them) and you could toggle them to eg hide when reading the code? But then also you could toggle them back when needed instead of having to open an interface file?
In my opinion the best of both worlds would just be to have Rust-style inline annotations and an "interface mode" that hides implementations (most modern IDEs can already do that) and non-public function.
BTW I am not sure why you would want to hide the types, I find them always useful.
I agree that the usefulness of documentation systems such as odoc comes from a carefully curated interface, but I disagree with them needing a separate file which in my experience is cause of friction.
Other languages (I am familiar with Rust and Python) also have documentation systems that only show you public methods (and functions, types, etc.), and I find them equally useful as odoc. They just take the visibility information from inline annotations (which are enforced by the compiler, in the case of Rust) instead of a separate file.
I disagree with the article here (and it’s a good article overall!) The compiler simply needs to be armed to detect this pattern and give better error messages. Vast majority of cases the user simply forgot an argument.
I think Haskell gets this close to right, actually. If always adding the type to every function is always better, I can do that. But as it turns out, occasionally during development looser error locations if I hit a relevant error is a reasonable price to pay for not having to update a signature or two while I iterate. Haskell community norms say you should annotate top level functions before you share the code, and any particular team can make that stricter. I don't think there's actually a good reason to require it on every single build regardless of the purpose of that build.
I agree with almost all of this. And I'd add that using OCaml on Windows is so painful as to be effectively not possible.
Where I slightly disagree:
1. I think expression based is fine. He's definitely right that OCaml does expressions wrong (it's very unclear where expressions end - I often end up adding and removing semicolons until the compiler stops complaining)... but I never have that problem in Rust.
2. Haskell's separation of the types of functions from the arguments is definitely not better. I don't want to have to manually count through two lists to match them up. And he correctly points out that currying is bad (overall) but at least OCaml sensibly separates arguments and return values, unlike Haskell which just checks them all in a -> separated list.
But overall this is a great list. I'm sure OCaml and Haskell programmers will feel like the author doesn't "get it" but I think if they search their feelings they'll know it to be true.
They didn't actually go that far, but yes it's still better than many languages.
I would say Rust solves almost all of the issues identified in this post. It obviously introduces the extra complexity of no GC & borrow checking, but if you're ok with that I don't see why you would pick OCaml over Rust.
The menhir parser generator is extremely good and doesn't quite have an equivalent in the Rust world.
But other than that, in no particular order:
- While it has its downsides, OCaml's module system (and functors especially) is extremely powerful and allows for all sorts of higher-kinded abstractions that are currently out of reach of Rust's type system, especially when combined with first-class modules
- No monorphisation means more reasonable compile times
- Some of the advanced type system features (GADTs, extensible types, polymorphic variants) are really quite nice
(but really, menhir is great, and I miss it whenever I need to parse things in another language)
I don't understand the downvotes. I thought the same thing. I agree with the other complaint on the error for too few arguments being unfortunate, but the one with too many arguments is on point.
I mean, it took me a beat to learn to actually read GHC error messages instead of jumping to where it's pointing in the code and assuming I know what's wrong. That's kinda the opposite of the complaint, though, except insofar as both involve learning.
I think the opposite, modules are vastly superior to typeclasses in regards to, well, modularity.
> In Haskell, the type class database is global and there is no duplication.
That's not a bug, but a feature of module system.
> This lets you naturally compose implementations: for example, you can make it so that if a type A implements the equality type class Eq, and a type B also implements Eq, then the tuple (A, B) implements Eq in the obvious way.
Unless you want a totally different ordering for A, which happens a lot in programming and even in math, where you can define many groups for the same set.
>There’s an easy way to solve this: add an end if delimiter. Again: terseness bites.
I think this is the second frontpage post in a single week with someone talking about this exact thing, going on about how OCaml doesn't have braces or block-end signifiers....somehow completely missing `begin`...`end`, which has been part of the language since like forever?
Just like with the last post, let's look at this post's example:
let foo _ =
if true then
print_endline "Hello, world!";
true
else
false
Yeah, I'm a little surprised that this doesn't work, actually, since I would assume that it would parse the if-branch until it gets an expression, but the compiler (and OCamlFormat, for what it's worth) interprets this code like this (and the auto-formatting done by ocamlformat makes it clear what's going on -- maybe consider hooking that up as an on-save action in your editor):
let foo _ =
if true then
print_endline "Hello, world!";
true
else
false
(which I discovered by just copy-pasting it directly into https://try.ocamlpro.com, which auto-formatted the code exactly like that for me).
Basically "(if true then print); true" rather than "if true then (print; true)"
Getting back to my original point, the code can easily be fixed by using `begin` and `end`:
let foo _ =
if true then begin
print_endline "Hello, world!";
true
end
else
false
OCaml _does_ have braces, like in the "more-solid-feeling" languages the author describes. They're just named "begin" and "end" instead of "{" and "}", same as in Ruby or Elixir or Pascal or even Austral (which is the author's own language).
This is, admittedly, more of a pain for dropping in a quick debug statement than what the author found:
let foo _ =
if true then
let _ = print_endline "Hello, world!" in
true
else
false
...which honestly, for the "quickly add a print-debug line" case, doesn't seem like a problem?
> Infix operators are bad. Custom infix operators are worse.
Disagree, as a regular user of `shouldBe` and other HSpec expectations, as well as DSL libraries that rhyme like English. And even when they don't rhyme, they provide flow for defining your complex types that would be tiresome to define with verbose prefixes (Servant).
> Haskell is very indentation-sensitive, more so than Python. Slight, harmless-looking cosmetic changes can break the parser.
That's not true, Python is more sensitive to indentations.
> Lazy evaluation is bad.
> Lazy data structures are bad.
> Is purity worth it? Not really.
These are just lazy crowd-opinions.
> Every file starts with declaring thirty language extensions.
This has never been true, all extensions can be declared default global to a compilation group (library/executable/project) and never mentioned in .hs files. When you see extensions being specified in every file of online tutorials, you know it's being done so for 1) didactic purposes 2) self-contained units for copy-pasting into your editor.
All in all, the author isn't very well versed in Haskell, as it becomes clear with the provided examples of Ord instances for newtype IntAsc declarations. A seasoned Haskell programmer would almost certainly write it with `deriving via`, especially in a situation where they want to demonstrate how easy it is to get around the limitation of a single typeclass instance per type.
For a discussion on combining the benefits of modules with type classes, see "Modular Type Classes"; If I were to implement a language with similar features today, I would certainly use it.
Oh, another one that applies to all functional languages: why the obsession with singly linked lists? Ok I do actually understand why, but they're objectively the worst data structure yet they're made 1st clas - maybe 0th class! - language features. Kind of mad.
I guess at least OCaml has normal vectors too, but still it always felt like a theoretical compsci feature that was almost willfully rejecting actual computer architecture.
44 comments
[ 4.0 ms ] story [ 101 ms ] threadInteresting to read about semicolons improving comprehension -it felt like it had been decades since somebody had the courage to say so. (That said I write python un functionally and so am 4space tabbed to the max)
It's also the first time I've seen "currying is bad" which seems to run against the flow of salmon.
Then you wind up in emacs, editing a .emacsrc file, marvelling at the .. depth of germanic (((( ... things) which need to be said)) by this person) nesting.. wondering what you do...
I think it has a lot to do with the writer not using blocks. It would solve both of their issues (with match and with if and semi-colons which is actually an issue of precedence) Some people seem to have a lot of trouble with the BCPL/Pascal syntax using begin and end.
The author apparently doesn't know about "|>" either which would solve their sequencing issues.
At least this post was written in good faith. I think this highlights that there is a dearth of recent material promoting best practices when it comes to Ocaml.
> So when you make a mistake, you’re still giving constraints to the inference engine. Erroneous type inferences propagate in every direction. Error messages appear hundreds of lines of code away from their origin.
Rust gets this right: it has type inference, but only within the function body. Most of your code is free of annotation cruft, but interfaces are strongly specified and errors can only propagate a certain distance.
Without that, you lose out massively on readability and error message quality.
In Sail, I have IDE type inlays working but it barely helps with reachability because half the types are inferred as generic.
On the other hand the OCaml/interface files approach is superior when you want to read documentation w/o implementation, but in practice using odoc is superior because you get hyperlinks.
In 10 years of using OCaml, I think everytime I interacted with mli I'd rather the author annotated types at the definition site.
BTW I am not sure why you would want to hide the types, I find them always useful.
Other languages (I am familiar with Rust and Python) also have documentation systems that only show you public methods (and functions, types, etc.), and I find them equally useful as odoc. They just take the visibility information from inline annotations (which are enforced by the compiler, in the case of Rust) instead of a separate file.
Agreed. The same criticism can be levelled at C and C++ with header and implementation files.
Multi core, modular implicits, build systems.
I imagine Ocaml will continue to adapt and while I don’t think it will solve all the authors problems with the language (syntax) it may solve some.
Where I slightly disagree:
1. I think expression based is fine. He's definitely right that OCaml does expressions wrong (it's very unclear where expressions end - I often end up adding and removing semicolons until the compiler stops complaining)... but I never have that problem in Rust.
2. Haskell's separation of the types of functions from the arguments is definitely not better. I don't want to have to manually count through two lists to match them up. And he correctly points out that currying is bad (overall) but at least OCaml sensibly separates arguments and return values, unlike Haskell which just checks them all in a -> separated list.
But overall this is a great list. I'm sure OCaml and Haskell programmers will feel like the author doesn't "get it" but I think if they search their feelings they'll know it to be true.
I would say Rust solves almost all of the issues identified in this post. It obviously introduces the extra complexity of no GC & borrow checking, but if you're ok with that I don't see why you would pick OCaml over Rust.
But other than that, in no particular order:
- While it has its downsides, OCaml's module system (and functors especially) is extremely powerful and allows for all sorts of higher-kinded abstractions that are currently out of reach of Rust's type system, especially when combined with first-class modules
- No monorphisation means more reasonable compile times
- Some of the advanced type system features (GADTs, extensible types, polymorphic variants) are really quite nice
(but really, menhir is great, and I miss it whenever I need to parse things in another language)
Yes, they did. This is literally from the third paragraph of the article:
> It’s actually a weirdly optimistic thing: that a language with so many glaring deficiencies stands far above everything else.
Have you considered F# instead? Native support in the dotnet CLI, VS Code, Visual Studio, and Rider. Not to mention the huge .NET standard library...
How many of the issues in this post does it solve? Or is it basically the same?
Super easy currying is great! Automatic currying is bad (overall).
Does anyone understand the complaint here? It sounds like the error message describes exactly what's wrong!
I think the opposite, modules are vastly superior to typeclasses in regards to, well, modularity.
> In Haskell, the type class database is global and there is no duplication.
That's not a bug, but a feature of module system.
> This lets you naturally compose implementations: for example, you can make it so that if a type A implements the equality type class Eq, and a type B also implements Eq, then the tuple (A, B) implements Eq in the obvious way.
Unless you want a totally different ordering for A, which happens a lot in programming and even in math, where you can define many groups for the same set.
https://existentialtype.wordpress.com/2011/04/16/modules-mat...
I think this is the second frontpage post in a single week with someone talking about this exact thing, going on about how OCaml doesn't have braces or block-end signifiers....somehow completely missing `begin`...`end`, which has been part of the language since like forever?
Just like with the last post, let's look at this post's example:
Yeah, I'm a little surprised that this doesn't work, actually, since I would assume that it would parse the if-branch until it gets an expression, but the compiler (and OCamlFormat, for what it's worth) interprets this code like this (and the auto-formatting done by ocamlformat makes it clear what's going on -- maybe consider hooking that up as an on-save action in your editor): (which I discovered by just copy-pasting it directly into https://try.ocamlpro.com, which auto-formatted the code exactly like that for me).Basically "(if true then print); true" rather than "if true then (print; true)"
Getting back to my original point, the code can easily be fixed by using `begin` and `end`:
OCaml _does_ have braces, like in the "more-solid-feeling" languages the author describes. They're just named "begin" and "end" instead of "{" and "}", same as in Ruby or Elixir or Pascal or even Austral (which is the author's own language).This is, admittedly, more of a pain for dropping in a quick debug statement than what the author found:
...which honestly, for the "quickly add a print-debug line" case, doesn't seem like a problem?> Infix operators are bad. Custom infix operators are worse.
Disagree, as a regular user of `shouldBe` and other HSpec expectations, as well as DSL libraries that rhyme like English. And even when they don't rhyme, they provide flow for defining your complex types that would be tiresome to define with verbose prefixes (Servant).
> Haskell is very indentation-sensitive, more so than Python. Slight, harmless-looking cosmetic changes can break the parser.
That's not true, Python is more sensitive to indentations.
> Lazy evaluation is bad.
> Lazy data structures are bad.
> Is purity worth it? Not really.
These are just lazy crowd-opinions.
> Every file starts with declaring thirty language extensions.
This has never been true, all extensions can be declared default global to a compilation group (library/executable/project) and never mentioned in .hs files. When you see extensions being specified in every file of online tutorials, you know it's being done so for 1) didactic purposes 2) self-contained units for copy-pasting into your editor.
All in all, the author isn't very well versed in Haskell, as it becomes clear with the provided examples of Ord instances for newtype IntAsc declarations. A seasoned Haskell programmer would almost certainly write it with `deriving via`, especially in a situation where they want to demonstrate how easy it is to get around the limitation of a single typeclass instance per type.
> This has never been true, all extensions can be declared default global to a compilation group
Yup, or nowadays you can just use "GHC2021" and be happy.
https://www.cs.cmu.edu/~rwh/papers/mtc/full.pdf
I guess at least OCaml has normal vectors too, but still it always felt like a theoretical compsci feature that was almost willfully rejecting actual computer architecture.