I don't the author really gave Ocaml a chance. He argues that every language needs to have interfaces. I agree somewhat with that statement, but I think the truer statement is to say that every language needs to have some way of defining composition at a structural level.
An interface allows you to pass different "structures" to the same function so long as they adhere to the same spec. In Ocaml this is accomplished through modules, functors, and module signatures. Ocaml's module signature can play the same exact role as interfaces in Haskell, Rust or F#.
The module system is arguably more expressive than what can be accomplished to interfaces. To give an example, Ocaml suffers from the same problem as rust does with having two standard implementations of a async runtime. Just like rusts: tokio and async-std, ocaml has Lwt, Async, (and newly added to the mix Eio).
Whereas in rust most libraries just implement one of these systems, and you'll have to use compiler directives to support both. Ocaml's module system means that you can describe the async runtime as a signature and make your entire library generic to the async runtime it runs on top of. Most of the well-used libraries do this, and so you don't have to worry too much about which runtime you decide to use.
Clearly, a language that can do that must have in some place a system that can replace interfaces.
Politely, I feel like this is the issue with OCaml as a community. You gave a beautiful answer about programming language design and composition. But how does it feel to write the language? How can you print a user defined data type? From reading around, it seems like your options are: explicitly pass a print function for that specific type, use a third party library for printing, or (my "favorite") don't. Or how does it feel to write a function that takes a generic that can be printed, checked for equality, and read? Or to convert one type into another type?
And we're still talking about semantics here. How does it feel to use the language server? How does it feel to read the code? To build the language? Ultimately that's what users judge a language on. Yes, Rust in some ways has a worse form of abstraction. It is global, not fully generic and doesn't allow for overloading. But it creates a user experience that is nicer. It lets a user print something and compare two variables and do type conversions without having to scratch their head, read a forum post and import a library.
> You gave a beautiful answer about programming language
You do the same thing as in Rust, Scala or Haskell and derive the printer [1]. Then at the callsite, if you know the type then you do `T.show` to print it or `T.eq`. If you don't know the type, then you pass it in at the top level as a module and then do `T.show` or `T.eq`.
> Or to convert one type into another type?
If you want to convert a type, then you have a type that you want to convert from such as foo and bar, then you do `Foo.to_bar value`.
We can keep going, but you can get the point.
You _can't_ judge a language by doing what you want to do with one language in another. If I judge Rust by writing recursive data structures and complaining about performance and verbosity that's not particularly fair correct? I can't say that Dart is terrible for desktop because I can't use chrome developer tools on its canvas output and ignore it's hot-reloading server. I can't say Common Lisp code is unreadable because I don't have type annotations and ignore the REPL for introspection.
Okay...so you have to add a third party library to print? Yes, recursive data structures are a pain in Rust (well, recursive multi-owner data structures). But that's like comparing changing your oil to opening your car door. We do one of these a lot more. And bear in mind, I had to find this answer by googling and reading a random forum post. That's some pretty poor documentation.
And that's not talking about aesthetics. `T.show foo` or `Foo.to_bar value` is a lot of syntactic overhead for a rather common task.
And again, these are the lesser concerns to the other ones I outlined. Reading the code, building the code, and understanding the compiler errors are the big ones.
Not really third-party; ocaml-ppx is something similar to `javax` in the Java space? But it is optional and doesn't come bundled with OCaml.
Recursive structures are only rare in Rust _because_ they suck to write and have terrible performance characteristics in the language. In languages like Haskell, OCaml and Scala using tagless initial encodings for eDSL's [2] are really, really common.
I don't write Rust like I write any semi-functional language with function composition because, well, it's _painful_ and not a good fit for the language.
> `T.show foo` or `Foo.to_bar value` is a lot of syntactic overhead
The `T.()` opens a scope so OCaml code generally looks like:
```
let foo = T.(if equal zero x then show x else "not here")
```
Even when talking about usage, each language community for better or worse has a predefined workflow in mind. Your questions and workflow are specific to a particular language; you don't, for example, ask about the REPL, the compile times, debugging macros, the cold-start of the compiler, whether hot-reloading is a thing, the debugger, how it interacts with perf, how to instrument an application. Presumably because the language you use or the programs that you make don't have those components as part of their main workflow.
A lot of these things that you mentioned are valid flaws in Rust. I'm not going to dismiss them as not important or non-problems because they are genuine issues. Compile times suck. We could use hot reloading and a REPL. Debugging macros, instrumentation and perf stuff is good, but I assume it could be better. That's what I expect from a language community. Not constant denial about the state of usability. And it really does feel like many of the smaller language communities fall into this hive-mind where type signatures are valid documentation and who needs to use an editor other than emacs?
They are genuine issues, within Rust, but the way you /categorize/ the severity of the issue is different within language communities. They don't impact usability /much/ because of Rust's community, focus, size and niche.
Compile times in Rust are a problem, but the RLS does a good enough job of feedback and the development loop happens enough in compile time, that users don't miss it enough, compared to, say, Java which relies heavily on runtime behaviour/reflection for application behaviour.
Debugging token macros could be better, such as with a better macro-stepper, but you don't write enough macros in Rust to care heavily about it to make it a big deal, unlike in a Lisp.
Hot reloading would be nice, but not and ~essential~ part of development and deployment like in BEAM. Better reflection utilities might be nice but I doubt Rust libraries will ever be heavily dependent on it in the same way that Go or Java are.
If you are referring to OCaml, many people in the community care a lot about writing good documentation so that it's not just the type signatures. In fact the OCaml website team have made amazing leaps and strides integrating package documentation directly in the website.
And the experience using OCaml LSP Server with VSCode nowadays is honestly pretty good--type annotations displayed, error messages, go to definition, generating interface files. The core functionality is all there.
That's good to hear! I don't mean to be too negative. I'm genuinely glad that OCaml is making these strides. I'm hopeful that OCaml can find its equivalent of Esteban Küber[1] and they can make the language a truly fun and friendly experience.
You don't need a third-party library to derive a pretty-printer for your types, it just makes it simpler. OCaml takes the approach that the core compiler doesn't do any codegen, that is left to PPXs (basically, middlewares which transform code) which run before the compiler.
You can write a pretty-printer yourself using the conventions of the `Format` module. Is it manual and annoying? Yeah. That's why the PPX exists. It's a tradeoff.
> `T.show foo` or `Foo.to_bar value` is a lot of syntactic overhead for a rather common task.
A lot of syntactic overhead sounds like a stretch. I suppose you could drop the `T` like you do in Haskell but I personally like my language to be a bit more explicit. It improves readability.
Scala is probably not a good example because if you need some quick and dirty printing you can override toString and keep the rest of the generic code intact. In OCaml, Rust and Haskell you would have to add type constraints in a bunch of places to make println! T.to_string and show work.
To be honest, anyone who used Ocaml understands immediately that the author gave it no chance and went looking for problems. It’s obvious because the post lingers a lot about things which are not actually issues like type conversion but don’t talk about the very real issues Ocaml has (opam is not great, dune is weird).
Why are these things not issues? I think it's totally valid for someone who doesn't understand OCaml to raise issues of usability or documentation. It's not just the issues of experts that are a concern. And it's very demoralizing to get a "you're holding it wrong" response to a complaint.
Because they are not issues on a day to day basis when you use the language.
I used Ocaml for a paid internship some years ago. I was very much a beginner. Do you know how many time I felt at loss or annoyed by type conversion or the precedence rules for various part of the syntax? I never did.
The truth is you never encounter the precedence rules when writing Ocaml normally using parentheses like a normal human being and you don’t convert that much between exotic types. Most of the conversion you actually do are between the same types and most of the time you have to write converter anyway because there is logic involved. I worked professionally as a Java developer for a bit. I think I had to write more conversion code between weird classes then than I ever did in Ocaml.
The issue is not telling people they are "holding it wrong". The issue is that this is not what’s going to annoy you as an Ocaml beginner.
The author of this article is not even an Ocaml beginner by the way. That’s actually someone who used to work on the Haskell compiler.
> The issue is that this is not what’s going to annoy you as an Ocaml beginner.
Sure, but tbf, the post is about what annoys the author, not what would annoy beginners. As you say, the author is not a beginner, and so their problems wouldn't be beginner problems.
That’s not the point I was making. They are not expert problems either. How often as an expert do you think you encounter precedence issues you were not encountering as a beginner?
> Because they are not issues on a day to day basis when you use the language.
They still are. You just internalize them. Progammers are the world's greatest masochists with the world's biggest Stockholm syndromes, and we don't like to admit it.
For example, his pain points about syntax ambiguity are absolutely valid, and true. However, when you "use the language every day" you learn to avoid those constructs and extract them into separate functions for example because "that's how it's always done, and it's a nice little quirk of the language".
No, the syntax parts he criticises are not this kind (they are not ambiguous by the way). What’s in the article is very unidiomatic OCaml incorrectly intended purposefully to make it look like it’s doing something else than what it does.
You don’t write tuples without parentheses because it’s weird, harder to read and no one does it. Same for the precedence rules of expression. You just use begin and end for blocks like you would use braces in C because you actually want to write a block. No one is just randomly writing nested code without them because well it doesn’t work.
It’s not a quirk. The author is just complaining that you have to mark blocks like in every language. That’s a weird thing to complain about.
I agree that the lack of as-hoc polymorphism is a downside (I don’t agree with how the article presents it but the article is disingenuous from the start anyway). Everyone would like to have modular implicit.
> This is exactly what I wrote, but in a Stockholm-syndrome-y way.
Do you complain about having to put braces around C expressions to not get unexpected behaviour from the compiler?
> He points out a real ambiguity in the syntax where the parser literally parses the same-looking and same-behaving statements completely differently.
No, he doesn’t. If that what you got from the article, you were bamboozled. He points out that match, try and (;) have different precedence which is not ambiguous. Writing begin and end is not a workaround. That’s how you mark blocks which is what he should have done because, well, he wants a block.
Erlang has exactly the same issue if you write unidiomatic code by the way.
That’s unsurprising because "don’t write code implicitly relying on precedence behaviour" is taught to virtually all beginners in most programming languages.
It’s perfectly legitimate code and working code. It’s just hard to read.
Should language prevents you from writing hard to read code if it means making trade off? I personally think that’s what linters are for but you are free to disagree.
I’m going to bed and don’t think I will revisit this comment thread. It’s probably the longest discussion I ever had about syntax points you don’t even encounter while using the language.
I have fond memory of writing OCaml. It’s nice to use. It gets out of your way. The compiler is quick. It gives nice error message. The syntax and semantics are flexible enough and elegant enough than you don’t feel like your fighting the language and your code feels nice. That’s what matters to me at the end of the day.
It's a spectrum. Most languages have some ways to write bad code, but there are definitely languages that have more or fewer ways compared to other languages. E.g. languages with simpler precedence rules, like Lisp or TCL, avoid this particular issue because you just can't write code with unintuitive precedence (although in the case of Lisp people often end up writing a macro that recapitulates the problem).
Primitive conversions are as simple as `<type>_of_<newType>(myValue)`. For example, `int_of_float(2.0)` will give you an integer. `string_of_int(123)` will return a string. Noting very surprising.
If you want to do more complex conversions, you have to write those functions and call them.
I don't much care for Ocaml syntax (StandardML is a better language in basically every way and ReasonML is Ocaml with better syntax), but this isn't a big deal.
The type system will know if you don't handle a potential conversion.
This restriction is better for pretty much every real-world use case anyway.
If you don't know what you're converting, then you are almost guaranteed to be getting garbage out the other end. If you do know what you are converting, being explicit and covering all your options isn't a big deal.
And of course, you can pack this all away in a module if you really want too. Modules are more flexible than typeclasses in a language like Haskell anyway (as you can't have multiple typeclass definitions in Haskell).
In truth, I believe module typeclasses (typeclasses only definable in modules) would be a nice addition. It would allow flexibility while creating a pattern that actively discourages the abuse you see in Haskell where typeclasses quickly devolve into unreadable garbage.
I've been using opam and dune for a few years now and they haven't annoyed me much since I got used to how to set up projects. What are the issues with them?
It's more that only really strongly disagreed with that point. Frankly, there are some cons with using Ocaml. In practice, I think the library support for ocaml isn't completely there given the small community.
> Ocaml's module system means that you can describe the async runtime as a signature and make your entire library generic to the async runtime it runs on top of
Is this the program describing the interface it expects, and the compiler matching that with the implementations structure (e.g. Go interfaces, Python Protocols), or is it something someone declares and the libraries implement?
Mostly the former. The compiler infers the type of the implementation and ensures it conforms to the interface. But, you can further restrict the interface's type to hide details from the outside world.
> Ocaml's module system means that you can describe the async runtime as a signature and make your entire library generic to the async runtime it runs on top of
So
... can you ELI5 to me how that is different from how you can for instance compile a C program against different libc implementations?
I mean…you can technically have a function that take void* and return void* be the interface. But you wouldn’t be able to represent things like iterator and chain them together without care. In C you would be tripping all over the slight details like forgetting to check errno or something.
Functional programming community likes this stuff. They go into the classical mathematicians’ trap of writing more and more general versions of something even though it’s not necessary.
When we write human languages (that use a Latin script), we use punctuation to delineate the beginning and end of terms. In a language like C or Rust, terms are surrounded by commas, parentheses, angle brackets, and so on. In OCaml and Haskell, many things that there is syntax for in C-style languages are done as ordinary function calls, which separate terms by only whitespace.
It is easier to read this (Rust):
let value = some_long_ass_function_name(some_quirky_parameter, another_one);
another_function(value)
than this (Haskell):
let value = someLongAssFunctionName someQuirkyParameter anotherOne
in anotherFunction value
or this (OCaml):
let value = some_long_ass_function_name some_quirky_parameter another_one in
another_function value
Individual terms in camel case are difficult to read if they consist of more than a couple words. Consecutive terms in snake case are difficult to read because underscores and whitespace look alike visually.
Haskell and OCaml's idiomatic solution is to use extremely terse names for arguments, type variables, and local variables, out of what seems to be syntactic necessity.
It is true that currying looks comparatively awful in Rust. But that is not because Rust uses parentheses and commas. A hypothetical C-style language could use dedicated syntax for currying, like
I don't think this is correlated to functional programming. Programmers in general tend to over-architect because we're often learning as we're going and sometimes it's more fun to use some cool concepts. Since design patterns started becoming en vogue back in the day, OOP codebases got filled with those patterns. Now we are left picking up the pieces with things like `AbstractProxyFactoryBean` or whatever. Same thing with JavaScript where that ecosystem is infamous for using tons and tons of npm packages to recreate simple functionality, e.g. 'left-pad a string'. Arcane mathematics-heavy FP code is just how that expresses in FP languages. And in fact, OCaml is actually an antidote to that (for the most part), because it deliberately offers a lower level of abstraction than Haskell. I frequently say that OCaml is like a cross between Haskell and Go.
I don’t know OCaml, or really any language that would help me fully understand the code, but my exposure to OCaml is this stuff, and it looks pretty clean to me. https://github.com/janestreet/base
Of course, I haven’t read every file, so maybe I got lucky with my random sampling.
It's a pretty practical language, so much so that its creators think it's actually a great language to teach Unix programming, and even wrote a book about it: https://ocaml.github.io/ocamlunix/ocamlunix.html
Excerpt:
> Tradition dictates that Unix system programming must be done in C. For this course we found it more interesting to use a higher-level language, namely OCaml, to explain the fundamentals of Unix system programming.
> The OCaml interface to Unix system calls is more abstract. Instead of encoding everything in terms of integers and bit fields as in C, OCaml uses the whole power of the ML type system to clearly represent the arguments and return values of system calls. Hence, it becomes easier to explain the semantics of the calls instead of losing oneself explaining how the arguments and the results have to be en/decoded. (See, for example, the presentation of the system call wait, page ??.)
> Furthermore, due to the static type system and the clarity of its primitives, it is safer to program in OCaml than in C. The experienced C programmer may see these benefits as useless luxury, however they are crucial for the inexperienced audience of this course.
> A second goal of this exposition of system programming is to show OCaml performing in a domain out of its usual applications in theorem proving, compilation and symbolic computation. The outcome of the experiment is rather positive, thanks to OCaml’s solid imperative kernel and its other novel aspects like parametric polymorphism, higher-order functions and exceptions. It also shows that instead of applicative and imperative programming being mutually exclusive, their combination makes it possible to integrate in the same program complex symbolic computations and a good interface with the operating system.
Hmm. I haven't seen the latter in OCaml as much. More the opposite; when I first used it, things like list length in the standard library weren't tail recursive. 8-|
Now, Haskell and shudder Scala shudder, on the other hand.
Compared to Haskell, OCaml developers are generally not to fond of custom operators which shield you from some of the worst cases unless you are reading code an Haskeller wrote in OCaml which happens.
For a long time, most of the users were either writing compilers or static analysers for low level languages and a fair share was very knowledgeable in C and system programming. It had a huge impact on what was seen as idiomatic. You can still find trace of it in for exemple the system library which looks more like the C stdlib than a modern standard library or how the compiler finds libraries.
Generally the community used to have a very different "flavour" than the Haskell one (it was also very French to be fair). I think I t’s less true nowadays. Still I would fall from my chair if I ever encounter someone working on the OCaml compiler writing a disparaging disingenuous post on Haskell. Meanwhile, we are here which reflects exactly my general experience with the Haskell community.
Ppx are used more and more often however and it did make code harder to read.
> Still I would fall from my chair if I ever encounter someone working on the OCaml compiler writing a disparaging disingenuous post on Haskell.
This exactly. OCaml devs uniformly respect Haskell and give it its due props for pushing innovation and commercialization in FP. We just think OCaml is pragmatic especially if you ever need to scale up your codebase and team.
Oh, I like Haskell a lot. I'm a bit leery of using it because performance and memory use are still a bit of a dark art, though much less than they used to be.
I just like mocking Haskellers (and Scala) for the symbols. :-)
1. No standard and easy way of implementing interfaces
No problem in F#, you have interfaces, abstract classed, ...
2. Bad standard library
In F# you have access to the full .NET standard library and ecosystem. There are also quite a lot of libraries that are especially designed to take advantage of F# (SQL libs for example).
3. Syntax problems
3.1 OCaml doesn’t have a single-line comment syntax.
It's `//` for single line and `(* comment *)` for n line comments in F#.
3.2 It has for and while, but no break and continue. So you use exceptions with a try inside the loop for continue, and outside for break.
Same in F#, use recursion.
Generally F# solves some of the issues but definitely not all of them. Some are just in
the nature of the Standard ML syntax I guess.
4. Rest of the package is also not that good
- NuGet is a decent/good package manager.
- Easy to install a complete dev env.
- sane build system (MSBuild)
- Great out of the box support in JetBrains Rider/ Visual Studio (for Mac/ Windows) or via a VS Code plugin (Ionide).
I use F# at work. Really love the language but the connection to the dotnet ecosystem is a curse as much as a blessing. Having to operate in a world of nulls and exceptions can reduce/nullify the amazing benefits of discriminated unions. Often you are back to relying on the due dilligence of other developers as a result.
I use a lot of rust in my spare time. The runtime guarantees it offers make other languages feel like a house of cards in comparison, including F#. I coincidentally picked up an ocaml book yesterday seeking a more 'robust' alternative to F#. Unfortunately it seems as though ocaml is even more niche than F# (I doubt I'll be working for jane street any time soon)
> Having to operate in a world of nulls and exceptions can reduce/nullify the amazing benefits of discriminated unions.
I've been saying this about Kotlin on the JVM for ages. Using a Java dependency is an absolute last resort for me for those same reasons you mention about .NET from F#.
The C# compiler got a ton of new smarts around reference type nullability and almost all of the BCL (.NET standard library) got annotated for it. I don't know when F# will finally pick up all the new compile-time smarts for Nullable Reference Types, but it has been proposed and prototyped, at least [1].
Before F# came into VS 2010, we already had C#, VB, C++/CLI (nee Managed C++), IronPython, IronRuby, and all the third parties targeting the CLR.
It is really not having a clue what purpose they want for F#, I bet they have repented to ship it on VS2010.
First it was for libraries only, then during the VS Express days it was for Web development, now they are trying to pivot it into data analysis and ML, while DevDiv manages to bring Guido out of retirement and finally manages to pursuade CPython core team about improving its performance.
F# was originally just kind of a passion project that MS insisted get productized to continue development. It's still very much an internal favorite within Microsoft Research and the Bing team, from my understanding.
Don is still working on F#. The F# team at MS now is bigger than is was years ago. People from different companies and the community are collaborating on compiler and language features.
F# is beyond microsoft, a lot of early adopters are still there.
I very much feel this, I found it productive, but it feels like a complete ghost town. I'm considering swapping to Go over using .NET for tooling/scripting.
Wanted to add my experience here: after writing C# code most of my professional life, F# is a breath of fresh air. The type system makes so many issues and annoyances I've lived with in C# just disappear. Much like Rust, the compiler is capable of providing so much more value when the type system is so expressive. You can bang out a lot of code quite quickly and be pretty darn sure it will work just because it compiles.
I really wish to like F# but the line noise kills me every time. I would kill for a "visual basic" f# where the absurd terseness is replaced with a little less noise. E.g. instead of seq use sequence, instead of abstract one letter symbols use words.
Perhaps you mean mix a little more noise into the dense signal? There's very little noise in F#, it's almost entirely dense signal.
One good thing is the ability to bind new operators, so if you don't care to learn to read <> you could make it something else. Or you could have a local binding for Sequence so you don't have to use just Seq.
It's also possible to avoid almost all operators if you don't like them. There's nothing wrong with using a limited pool as long as you can get done the job!
I also prefer to name variables, functions, and parameters with nice clear names, and I've not found F# to get in the way of that.
I remember F# designed its own package manager (paket) because of NuGet's deficiencies. I haven't been on that ecosystem for a while now. Have things changed? (My memory is that paket added friction and I eventually dropped it for nuget on my personal projects).
F# has largely moved back to PackageReference, since package versions can now be managed centrally across projects, and F# scripts can now reference NuGet dependencies directly.
> Another example is the sequencing syntax <expr> ; <expr> and productions with <expr> as the right-most symbol:
let test1 b =
if b then
print_string "1"
else
print_string "2"; print_string "3"
> Here print_string "3" is not a part of the if expression, so this function always prints “3”.
Well, yeah. Here's the equivalent Java code:
void test1(bool b) {
if (b) print_string("1");
else print_string("2");
print_string("3");
}
You've chosen to use a single-statement "else" form _without_ "bracketing" it with `begin` and `end`:
let test1 b =
if b then
print_string "1"
else begin
print_string "2";
print_string "3"
end
...which is equivalent to this Java code, where you _also_ would have to "bracket" the else-block:
void test1(bool b) {
if (b) print_string("1");
else {
print_string("2");
print_string("3");
}
}
You could argue that Java (and C, and C++, and Javascript, and...) has the same "ambiguous parsing problem", then -- except that it doesn't, and the author is just thrown off by `begin`/`end` as block delimiters instead of curly braces, which might be a sign that they've also never seen Ruby before, or Elixir, or Pascal, or....
It's a non-issue IMHO. Just use parenthesis if you're unsure, then auto-format. You'll catch any issue that wan't caught by the typechecker by reading the diff.
Agreed except for the part at the end: Java, C, C++, and JavaScript definitely do have the "dangling else" problem! It's a manifestation of the same basic issue. Who is saying they don't? You just showed how they do! It is a real problem and they all have it. The takeaway here isn't that those languages don't have the issue. They do. It's just really common in language design, that's all; certainly not something to single out OCaml about. Apple's infamous "goto vulnerability" was directly caused by this language design issue in good old C.
>Java, C, C++, and JavaScript definitely do have the "dangling else" problem! It's a manifestation of the same basic issue. Who is saying they don't? You just showed how they do! It is a real problem and they all have it.
Yeah, that's actually exactly my point: it's not some new weird novel horrible broken parsing problem that only OCaml has; rather, it's a common parsing rule that many languages (including Java, C, C++, and JavaScript -- and yes, OCaml) have.
> You could argue that Java (and C, and C++, and Javascript, and...) has the same "ambiguous parsing problem", then -- except that it doesn't
The author didn’t claim there’s any ambiguity in parsing, only ambiguity in reading as a human. And I would agree that all the languages you mentioned have the same problem. And this is a real problem as long as people use this form of if (which they do). New languages avoid this pitfall by mandating the use of braces, in return dropping the parens, and I think that’s great.
Now keep reading on to literally the next sentence.
The article's point is that the `if-else` behavior is inconsistent with `match`. And based on the part about double quotes in comments, the parser seems to be fucked up something fierce in other ways too.
1. To print values, nowadays you just derive pretty printers automatically.
2. Syntax? yes, it's not C-like... you'll get used to it in a few days.
3. Lack of interface? would be helpful to see a concrete example of what the author finds limiting. You have modules, functors, includes, objects, first-order modules... Personally, I find that plain modules are simple and go a long way.
It's not perfect, but I find that nowadays Core/Dune/Opam/Merlin gives you a very decent developer experience.
Incidentally, I find Haskell syntax more confusing, specifically the semantic indentation. But maybe I'm like the author, I don't use Haskell regularly enough so that the syntax sinks in.
Been using Haskell full-time for a year or so now, and using it part-time a few years ago for a side project. The reliance on indentation is nice and light but definitely bites you every now and then.
Coalton [1] is an OCaml-like dialect of ML: it's strictly evaluated, has imperative features, and doesn't force a purity-based development philosophy. However, Coalton has S-expression syntax and integrates into Common Lisp. It works, and is used "in production" by its developers, but it's still being developed into a 1.0 product.
Coalton made a lot of design decisions to work away from issues that this post describes.
- Coalton dispensed with ML's module system of structures, signatures, and functors. No doubt a beautiful concept, and sometimes satisfying to write, but almost always unsatisfying to use due to how explicit one tends to need to be. Like the author suggests, the "interface-style" of type classes or traits has always felt more intuitive, despite losing the ability to have multiple implementations of an interface for a single type. (However, I've come to appreciate that different interfaces should require different types—as opposed to one type having many interfaces.) Coalton uses type classes.
- Coalton has S-expression syntax. No indent rules. No precedence rules. No expression delimiters. All of the "tradition" of ML syntax is dispensed with into something that is a lot easier to read and write for complex programs... yes, provided you use ParEdit or similar
- It should be no surprise that with S-expressions, you get Lisp-style macros. Macros in Coalton are just Common Lisp macros. No separate pre-processors. No additional language semantics to deal with. Arbitrary compile-time computation and/or codegen, completely integrated.
- Because it's built on Common Lisp, Coalton gets something few (if any?) other ML-derivatives get: Truly incremental and interactive development. You can re-compile types, functions, etc. and try them out immediately. There's no separate "interpreter mode"; just a Lisp REPL.
Coalton's language features are still settling (e.g., records are being implemented) and the standard library [2] is still evolving. However, it's been used for "serious" applications, like implementing a compiler module for quantum programs [3].
> Coalton has S-expression syntax. No indent rules. No precedence rules. No expression delimiters.
This is a great selling point for a language in the ML family. Programmers can't abuse infix operators when there are none. :-)
Besides this, Coalton looks appealing. A highly interactive statically typed language is something I have thought of. I am sure I am not the only one. It is also a good sign that you are using it as it is being developed.
This looks excellent! I have a soft-spot for the elegance of Lisp but can't live without a robust type system. A few questions I didn't see immediate answers to on the GH page (sorry if I missed something/could've found those answers if I digged a little deeper):
1. Beyond the typeclasses you mentioned, how powerful is the type system beyond standard HM stuff? Are there GADTs/existential types? Any other interesting features?
2. The lack of records sounds like it might be a deal-breaker for current use. Is that as bad as it sounds? Is it something that is expected to be addressed soon? Are there any other large omissions?
3. How does it integrate into the existing Common Lisp ecosystem, if at all? Is there any friction there?
4. On that note: in the REPL example it looks like you need to wrap your expressions with (coalton ...), which feels like it'd get frustrating. Is there any way around that? I guess even a reader macro would help...but still wouldn't be ideal.
5. Is Coalton backed by any kind of corporate funding? And (assuming you're involved in Coalton in a professional capacity) ...you guys hiring?
1. There are functional dependencies and multi-parameter type classes. No GADTs (yet?) or existential types (yet?).
2. Records are being implemented but it's far from being a dealbreaker, in the sense that thousands of production lines of Coalton have been built without needing them. It's just not ergonomic to shuffle around and pattern match against record-like data. With pattern matching in function arguments, things are at least easier.
4. Since Coalton functions are Lisp functions, you can call (unconstrained) functions directly. However there's a lot of room for improvement. There's an open issue to make a dedicated Coalton REPL that can show types and not require COALTON to be typed.
5. Coalton is developed as an open source project to build tools at HRL Laboratories, so it does receive sponsorship. Yes, HRL is hiring. Feel free to send me an email to the address in my profile.
As seems to be the case with any "serious" programming language, there's always another thing to do—seemingly perpetually—to make said language useful enough for "serious" use. :)
> Coalton [1] is an OCaml-like dialect of ML: it's strictly evaluated, has imperative features, and doesn't force a purity-based development philosophy.
OCaml is also strictly evaluated, has imperative features (e.g. ref), and doesn't force a purity-based development philosophy.
What makes you call Coalton a "dialect of ML"? ML's module system is its defining feature, beyond that there's really not much unique about it (by design). Would you call any language with algebraic data types a "dialect of ML"?
EDIT: I'm picking on this one sentence, but the rest of your comment is a good overview of the language, thanks.
That's why I said it's "OCaml-like" followed by a colon: to highlight the similarities.
As for why it's an ML dialect, maybe (maybe not) it's more fair to call it "ML-inspired". Nonetheless, it's similar enough to the ML family to be understood by ML programmers. But yes, one of the major pillars of ML—the module system—is conspicuously absent in favor of Haskell-like type classes.
The last paragraph show this article for what is is. That's an Haskell fan trying to discredit Ocaml and doing it very poorly.
It takes a special kind of writer to write an incorrect sentence about the expression syntax while quoting the actual definition from the specification which contradicts what's written just after.
I generally disagree about the complaint regarding the syntax. If you indent the code properly and use parentheses as you should and the language invites you to do, none of this is ambiguous. That's bad code not bad language design.
That's a best practice. Like always using braces in C or Java or not using ternary operators in hard to read way.
The syntax is unambiguous but non obvious to read if you don't know precedence rules as it is for most languages. You can make it explicit using parentheses which you should.
I personally agree that parentheses around tuples should be mandatory but it's fairly easy to just put them.
I’m not sure I understand your point then because the code in the article is definitely very bad OCaml which is what I have been trying to tell you from the start.
It's very bad if a language community KNOWS something is very bad and they still allow that to be valid syntax. Like `if (x) break;` in Java/C/C++. It's VERY BAD.
I agree, but the languages that actually do this are in a hyper-minority, I feel. Nearly every language allows you to do the wrong thing and then only lets you discover this at runtime via exception OR sometimes even just by careful examination of subtly incorrect output.
As such, this doesn't really feel like the best argument against ocaml (and I believe arguments against ocaml do exist and on a much more accessible level).
I used OCaml as my daily driver in grad school and my postdoc, and recommend my students use it. I think the main benefits of OCaml are:
Functional
Has mutable references
Not lazy
Haskell's type system is just better. But for large, complex systems that require performant code, it can be quite difficult to track the laziness, and sometimes life is just easier if I can use a mutable references.
The author is completely correct that there's a big failure in the lack of a type-class-like system. This is why people ask so much for modular implicits. Core is also a decent standard library (though I don't know why they require sexp_of_t and t_of_sexp on like all their data structures).
So I'd gladly use a different functional language with mutable references and strict evaluation. But there isn't any besides F#, and I have a Mac and don't personally want to figure out .NET on Mac.
They require `sexp_of_t` and `t_of_sexp` because of their workflow being based on them; such as expectation testing [1] and tools such as [2]. It's a less noisy alternative to json for many people.
I think looking at it as a 'failure' or 'deficiency' is the wrong mindset. OCaml makes a different tradeoff than Haskell. In exchange for not having ad-hoc polymorphism, OCaml's functor system allows much quicker compilation and easier-to-understand type errors. Like everything in tech, it's a tradeoff.
ReasonML and ReScript are a (more or less the same) new syntax on top of OCaml. ReScript only targets JS, while ReasonML targets both JS and the native archs OCaml supports. Facebook and Bloomberg are using ReScript internally, afaik. Messenger.com is written in it. Facebook also maintains React bindings to ReScript.
Rescript is completely excellent imo. I have used ocaml for years and while I like it more than most languages it's also weird and frustrating sometimes. Rescript uses the best parts of ocaml (type system & exhaustive pattern match, first class modules) to patch over the worst parts of js. It's almost alarming how effective it is to combine these two gnarly hostile languages like this.
I also work in typescript a lot and it's the obvious comparison/competitor there. While it has objectively "won" it's so so much worse than rescript. Its type system is much more complex, untrustworthy inference, and ultimately is still unsound lol. Ocaml's type system is truly a wonder. Extremely practically helpful, complex enough to express anything without becoming a full blown language in its own right like eg typescript and haskell.
Rescript compiles to very reasonable js and writing bindings is not particularly exhausting once you get the hang of it. I've been using it for a number of projects including completely irresponsible cases like deno fresh and it's been wonderful. What typescript could and should have been.
How does that work with Fresh? Just not using its built-in "Islands" machinery at all? Or smuggling stuff in that as far as Fresh is concerned are more orthodox React components?
Rescript has good react integration and compiles to normal js, so a little care to structure your rescript files so the compiled js ends up where fresh expects it is mostly all you need.
The more worrying thing is that fresh uses preact while rescript explicitly uses react and so far doesn't have a way to change it. I just overrode react in the import map to link preact instead. This is frightening and fragile, since the interfaces aren't guaranteed to be identical. But it's been working fine on a personal project for a few months now.
The rescript-reason split killed both IMO. ReasonML was a very tiny language to begin with. Splitting it into two split the already tiny language and community into two minuscule ones.
And for no good reason (pun not intended).
And they were doing quite well, and influencing OCaml in good and meaningful ways (like improving error reporting).
- I really want to do a project in an almost "pure" functional language. I tried with Elixir and Phoenix, and while they are certainly great, and I wouldn't mind using it again, Elixir, and subsequently Erlang didn't feel like FP a lot of times, it felt like the warty Elixir/Erlang way to do FP, so it didn't scratch that itch for me (but again, still a great experience overall)
- is there a way to do this openly? I.E. with dotnetcore, running on linux containers, no windows what so ever, etc? Guessing yes but wanted to make sure from people who have actually done it
- What exactly about the .NET libraries that has everyone so hyped up about? I remember from my C# .NET days, they always felt to be lacking.
Today you can install the .NET runtime, as well as the .NET development tools, on both Windows and Linux. They handle F# as well as C# out of the box.
You can `dotnet publish MyApp.fsproj` on either Windows or Linux, copy the resulting `publish/` folder to another machine with a different OS, and use `dotnet MyApp.dll` to run it. There are also ways to create containers, though I have never tried them for lack of an actual use case.
As for libraries, .NET comes with a huge standard library, much larger than what you would find on a different language. Off the top of my head, here are a few things that come with .NET that would have been a third party library in other languages: text encodings, protocols (HTTP client, TLS, QUIC, SMTP), mainstream hashes (MD5/SHA1/SHA256), mainstream crypto (AES/DES/RSA/EC/ChaCha), serialization (JSON/ XML), weird formats (X509/ASN.1/tar/MIME), mainstream compression (deflate/brotli/zip), binary data manipulation (e.g. endianness conversion, AVX intrinsics, memory-mapping), runtime code generation, in-assembly resources, interoperability (COM, JavaScript, Objective C, SQL), concurrent collections, async...
Besides, since C# is popular, many tools have .NET-compatible clients or libraries which can then be accessed from F#.
Scala might be a good option - the JVM library ecosystem is second to none, Linux is very much first class and ~everything is open source. Although you may have the same problem of not being forced into a pure style.
(I think learning Standard ML first was very good for my programming practices, but I wouldn't suggest it for real projects)
You asked me to clarify what I felt they were lacking. I was pretty explicit about the fact that this was my opinion based upon my previous experience working at .NET shops and using C#. So no, I am not kidding.
This piece focuses on complaints about ocaml, and the last half seems to just be syntax complaints (which I mostly ignore just because every language seems to have awkwardness in its syntax -- certainly Haskell, a language otherwise held up as an example, can be shown to accept some really weird text).
Good things about ocaml that I think should be mentioned are Hindley-Milner type inference (some might argue this, but it's a very effective system for inferring types), and also straightforward semantics (it's usually pretty easy to look at some code and make a reasonably accurate guess about what it will do on real hardware -- compare with Haskell).
I'm sympathetic toward the initial complaints here, which I also think are the most substantive. Type classes and qualified types, as in Haskell, would be very useful in ocaml/ML. The examples offered (show_of_int, show_of_double, ...) are valid, and also the related complaint that dishonestly broad types like structural equality (where runtime errors are raised for cases where the types are actually not supported) also make a compelling case for qualified types in ML.
Things like structural equality and ordering are user functions here rather than magic internal definitions (e.g. equality is defined as a type class, and type class instances can deconstruct algebraic types at compile time to decide how to implement instances). But evaluation is eager by default, so it's pretty easy to reason about performance. And data structures can be persisted to files and/or shared transparently in memory with C/C++ programs without translation, also very convenient for the places where this was used. Actually we built some very big and complicated time series databases with it, used in both pre and post trade settings where ~15% of daily US equity trades happen. So I think these observations are useful and have passed through some pretty significant real tests.
The biggest problem I have with Ocaml is not the language, but the tooling and the libraries. The library ecosystem is not even remotely comparable to go or even rust. Jane street core is mentioned as a good option, but that library is the most popular undocumented library I have ever seen. There is not even some normal default structure every library follows (e.g go io.Reader).
Compare this to languages with supposedly "worse" type systems like Java, which has a library for everything and high quality well documented libraries like guava and apache commons. I find it fine to use ocaml for a random weekend whim project, but wouldn't touch it with a 10 feet pole for professional work.
> like Java, which has a library for everything and high quality well documented libraries
Unless it's a library that has been around for a while and is probably hosted at Apache.
There was a marked shift in 2010s in expectations, and now we expect libraries and code to be somewhat well-documented. For older libraries... well, it's often "here's a dump of autogenerated reference files with maybe a blob of prefacing text at the top. Good luck."
> No standard and easy way of implementing interfaces.
> In Haskell, this is done with typeclasses ...
> In OCaml there’s no way to do this. I have to explicitly pass functions along with my values, maybe in a product type, or with a functor, or as an argument.
Haskell is a mixed community / paradigm language too.
I routinely program in "Braindead Haskell" (keep stuff simple and direct as much as possible). Funnily, in that programming style, using typeclasses for _all_ interface types is bad style.
Instead, the Handle pattern [0] can be used. Which really is just shoveling functions in a product type. And imagine - it works worderfully.
The author says: "If you have a reason to think that OCaml is the best choice for a new project please let me know your use case, I’m genuinely curious." I think it's overwhelmingly the best language for writing an interpreter or a compiler. I'm sure Haskell is also a good language for writing compilers, but whenever I tried writing interpreters in Haskell, I found myself fighting the laziness. Most of the criticisms listed strike me as things that bother people starting out in the language (syntax etc.) but which quickly become non-issues with more experience. And my experience with opam/dune has been _much_ more pleasant than with Cabal in Haskell (though ghcup has certainly improved matters). I think the infrastructure in OCaml, which admittedly used to be quite poor, is now on par with any other language, if not better. However, if your application domain is one in which few OCaml libraries exist (e.g. web programming) you probably should look elsewhere.
God I hate this term because it's not even a problem. For a human reader, it's pretty obvious that an "else" should belong to the closest "if"; and it's quite difficult to write a mechanical parser that would match "else" with the furthest "if" instead. So what is, exactly, the problem? That the grammar is technically ambiguous? Who cares? The authors of LR-generators? They don't, they have been resolving shift-reduce conflicts by default in favour of shifting for half a century already.
I’ve worked in software now for most of my life, from small to the biggest companies. I’ve watched teams with senior developers use non mainstream languages to build services and just about every time those devs get board again, leave the team, leaving them stuck with this system that’s hard to develop and different from the rest of the orgs, eventually resulting in a complete rewrite.
At companies I try to be boring, not weird, and efficient. Which usually means for me, writing in a very mainstream language that is easy to hire for.
A language like ocaml excels when the input and output are program-like, and the problem can't be number-crunched by hardware. The typical example would be a compiler. Other examples would be model checkers, code generators for number crunching libraries, proof assistants, and so on. Complex problems, where you need to explore different avenues of approach quickly, and where symbolic manipulation is at the heart of the problem.
Thus, ocaml is best used as a "kernel" or "module". The contextual part of your system uses the part written in ocaml as a service.
But ocaml doesn't excel at problems which are common to a typical company. The people who are in the intersection of wanting to solve those problems, and ocaml developers are few. The same intersection with a more mainstream language is far larger.
However, if you find yourself with a problem at which ocaml excels, chances are the mainstream languages ends up falling short very quickly, because they tend to lack the abstraction features necessary for correctly handling the complexity.
The question is if modern versions of popular languages (java, c#) caught up on ocaml's syntax sugar, or maybe this syntax sugar is trivially replaceable by some utility functions.
Maybe you could bring any example of such ocaml functionality?
Syntax and sugar is subjective, right? Instead, easier to discuss the layer beneath syntax, and that’s to compare the type systems. Java now has some version of sum types aka algebraic data types, but because of its beginnings, it may never feel as easy to use. And ease of use is important, as is evident from discussions here.
Sum types save more than a few lines of code, they can save you from nasty bugs thanks to exhaustiveness checking in pattern matches. Once you get used to this you will be hard pressed to go back to prehistoric times.
Initially you might think generating HTML tags from data structures in code should be a simple matter. But there are complexities--some tags are defined as having no child tags, others do. Some tags are purely character data (unstructured text), not structured data. Some are just comments. We need a way to compose multiple tags together into a single 'virtual' tag for flexible HTML generation. All these conditions can be pretty hard to keep track of--unless your compiler does exhaustiveness checking. Then the compiler will tell you if you missed any cases.
In the example above I didn't make any manual effort to cover all the cases, I simple listed out the cases I wanted to handle in order. The compiler made sure that I didn't miss any.
Back when I tried it, there seemed to be a few verticals where Ocaml abstractions made Ocaml very productive to work with.
Once a colleague and I did a programming race between two languages, to see how long it would take to implement a small cryptanalysis program using Ocaml vs C++. I wrote it in a day, and it took my desk neighbor who was doing the same in C++ a couple of weeks.
What made the difference:
- There were a lot of things I didn't have to worry about. For example, I was working with nontrivial structures but didn't have to write any boilerplate functions for deep equality or lex ordering.
- Immutable structures with GC made it much more direct to translate the math into a working program. Ditto with guaranteed tail recursion optimization.
- The match syntax caught many issues at compile time by forcing me to reason about every case. As a result I had many less runtime bugs to deal with, while my colleague needed to troubleshoot memory and correctness issues half the time.
Ymmv. This was for research and didn't end up in production use. The C++ implementation was also much "closer to the metal" so was able to wring out more performance. Still the difference in initial velocity was striking and there could've been a case that this would've been a better choice based on dev velocity in that vertical.
.. that's a complaint for all new languages. You don't know which ones will stick until you try.
Code in a strongly-typed functional language is easier to maintain because of the features of the language. The functional part gives great abstraction, and the strongly-typed part gives enough structure to make the whole thing tractable.
Absolutely agreed. I'm lucky to work on a ReasonML front-end codebase and the type system and immutability are a real pleasure to work with. The strong typing lets you code against the compiler, and there are many instances of "if it compiles it works" that crop up throughout development. There's no fear of refactoring code that you didn't write, because the compiler will warn you about consequences on the far end of the codebase that you didn't even know existed.
As a result of all of that, the app feels very solid to use and has comparatively few runtime errors.
Very cool to hear. I've been using rescript some recently and was hoping for exactly that sort of benefit.
I have a mixed opinion of ocaml itself having used it professionally a moderate amount. But one thing I can't deny is that ocaml programs seem to "age" better than any other languages, even other strongly typed languages.
PG claims using Lisp was the "secret sauce" to his early success. Whether this is true or not, and whether this is still true for companies today, remains to be seen. But it's an argument for things like OCaml.
It won’t really answer your question, but I found the Signals & Threads podcast fantastic, and it made me appreciate OCaml much more (it is produced by someone working at Jane Street).
I personally found the MirageOS, build systems and language design episodes terrific.
OCaml is pretty much the most "boring", "mainstream" language that isn't awful. It's been around for decades. It's got an established library ecosystem, development tools and all that. And pretty much every bigger/more popular language is missing one or more basic feature that's been common-sense since the late '70s (e.g. sum types or pattern matching).
I'm sure some companies out there might find a competitive advantage for using exotic languages and tech stacks.
But I get the impression the more common scenario is that the engineers were bored and given the chance, wanted to play with something new and shiny.
It also depends on the company's prestige. A company like Jane Street using OCaml? Considering both their prestige and the amount they pay, I'm sure they will have no problems hiring. Nor will their alumni have problems getting hired elsewhere.
Some no name startup or even some "innovation lab" inside a boring cludgy Fortune500? Yeah, no thanks.
Jane Street wasn't all that prestigious when they started using OCaml. The way they put it is "it was easier to find great programmers in the empty set of people who know OCaml than in the huge set of other programmers" (roughly). So, on the contrary, I think it might help boring cludgy company find quality talent.
I think there is a fallacy being repeated in this thread, namely that the choice is between mainstream and weird, or between old-and-tested and new-and-shiny. No, that's not the choice. The choice is between tools and abstractions that are helpful and productive, and those that aren't. Some of the helpful and productive things are old, some are new, some are mainstream, some are niche.
I don't fully understand how someone wants to spend their career in programming and not actively seek out what is helpful and productive, rather than what currently gets the most questions on Stack Overflow or has a big pool of people who put it on their CV. It's not like all languages are equally good. It's a wasteful approach.
There's another aspect, which myself and a few others in this thread have mentioned - hireability. Both from the company's and employee's perspective.
I'm pretty sure Jane Street would have paid well even when they were an obscure name.
Will boring cludgy Fortune500 company or noname startup pay well for someone to come use an exotic tech stack that is used very rarely at any other companies? Likely not.
Likewise, would an engineer well versed in some exotic language, and can land offers at top companies, come work for a boring cludgy Fortune500 company that is only willing to pay a fraction of what they can get elsewhere?
Maybe if you've already made your fortune elsewhere and well on the road to FIRE, and are really just looking for interesting work regardless of pay.
So are you saying that Jane Street more or less bribed people who didn't really care for OCaml to come work in a weird language, by offering a good salary? I find that far-fetched, and there are many other examples of companies attractive highly-competent people because they choose -- no, not exotic, weird, or niche tech, but good tech, that aid people thinking more clearly and abstracting better.
As in everything else, there is a trade-off involved here of course. And what is truly good and helpful is by no means obvious, and might take time to assess.
"Bribed" is a crude way to describe it, but in a manner of speaking, yes?
I think you might be a tad idealistic, and you might think I'm being a tad cynical. Maybe the truth is in the middle somewhere.
But throwing enough money at someone can talk. And not throwing enough money at someone can also talk. Short of being asked to do something unethical or illegal, what's wrong with that?
Can you honestly say all of the talented top engineers out there working for top paying companies are doing it for the pursuit of technological perfection, and aren't doing it at least partially for the good money?
I would say the same thing about doctors - people all love to say you should not get into medicine for money, but can we honestly say money isn't at least a factor in whether someone decides to pursue a MD?
"Helpful and productive" is highly context dependent. There are many tasks for which Ocaml is great at, and also many tasks for which there are much better choices available.
I worked on a team using Haskell at Target for a few years and we had a massively easier time hiring compared to the other data science/ML teams. I figure it came down to two things:
1. People are actively interested in using Haskell and working with others interested in Haskell/PL/FP/etc. More people want to do Haskell than there are Haskell jobs and Haskell openings get massive word-of-mouth advertising.
2. It's a way to show that we're willing to do something different and interesting. Everybody says they are, but how do you actually demonstrate it? I remember one of the first great hires we had was an OR professor who didn't even know about Haskell—but joined in part because what we were doing was different and innovative.
If I ever end up starting my own company, I'm going to use Haskell because it's so much easier to hire for it—completely opposite to the superficial "common knowledge" that guides executive decisions at large companies.
In C and C++, abs(INT_MIN) is undefined behavior. (C17 §7.22.6.1: “The abs, labs, and llabs functions compute the absolute value of an integer j. If the result cannot be represented, the behavior is undefined.” Footnote: “The absolute value of the most negative number cannot be represented in two’s complement.”)
Honestly the way OP just throws out a scary-sounding 'well this is an obvious bug that abs can return a negative number', without giving the context that it's for the minimum value of a machine int type, really says it all. He doesn't have an interest in evaluating it fairly, he's just doing a hit piece.
An alternative to OCaml is Flix (https://flix.dev/) which attempts to address some of the critiques in the post. For example:
> Bad standard library
A particular goal of Flix is to have a consistent standard library based on type classes.
> Standard types are sometimes persistent, sometimes mutable. List, Map, and Set are persistent. Stack and Hashtbl are mutable.
In Flix immutable types are named List, Set, Map, etc. and mutable collections are prefixed with Mut, e.g. MutSet, MutMap, etc. Mutable data types also carry a region.
> Also, OCaml has no tracking of side-effects (like in Haskell)
Flix has an effect system that tracks purity/impurity.
I have been keeping an eye on Flix. Thanks to you and rest of the Flix team for working on a bold but practical language. I wish you success. I think Flix is one of the most promising languages in the ML family. (Do you consider it an ML?) I love that it has embedded Datalog.
I know it is pre-1.0, but I would still like to ask about adoption. Are there notable third-party open source projects in Flix yet? By "third-party" I mean created outside of Aarhus University and the University of Waterloo.
Yes, I would definitely consider it a language in the ML-family. Its core is based on Hindley-Milner (like StandardML, OCaml, and Haskell) which I think is the hallmark of ML-family languages. Flix sits neatly between Ocaml and Haskell in that it allows side-effects (like Ocaml, unlike Haskell) but they are tracked by the effect system. Hence one can program in the spectrum between those two languages, but get strong guarantees from the compiler.
In terms of adoption, we see a lot of people trying it out, and there are some packages out there already, but nothing too big yet. We recently added support for package management, so hopefully that will help the community grow.
Sorry, I wasn't clear. I meant to ask whether you considered Flix "an ML" as opposed to just being in the ML family. It's an ontological question, and not important, so don't sweat the answer. I am just curious how Flix developers see their project.
Knowing the virtually dead Standard ML is a curse when it comes to appreciating the aesthetics of OCaml. The two languages have similar syntax, but in just about every way they differ, OCaml syntax took the ugly road :(
Ocaml5 is multithreaded. MLdonkey would run much better.
Compared to the classical Amule, a monstruosity writen in C++, at least it doesn't leak memory like some youngster with a constipation.
I learned OCaml two decades ago. It was great to learning algorithmics but I already had the feeling the language was old and awkward. Today there is Scala, Haskell, Rust, F# etc.
Everyone I’ve met, including on the steering committee, has been funded by high-frequency trading.
Heck, I almost took a job on a handshake in manhattan on this premise.
But it just seems, even now, archaic for the task. As the author mentions, the community is small, the integrations are weak. You will be working on legacy systems if anyone pays you well.
Same with certain parts of national defense, but we’ll make fun of PDP-11’s and FORTRAN another day.
Proactive thought: why don’t we fund a B-team to say, try using Julia? Even ERLANG has some superior qualities for these real-time audited tasks. (And hey, if you want employees for some reason, just use python).
> No standard and easy way of implementing interfaces
Firstly another commenter has mentioned [1] modules as interfaces. There is also the object system [2] which I've seen used to great effect in similar scenarios.
Typeclasses and modular implicits are fantastic - being a heavy user of Scala and Rust; but I've always found that they tend to be best when used as an alternative to macros, for derivation purposes.
The interface and code split in OCaml is also really nice for value types and comes with no limitations; for years, we had issues in Scala because of the host platform, and in Rust exposing functions for your newtype can be oddly boilerplate heavy.
> Bad standard library
The OCaml standard library is (infamously) anemic and obviously is not as fully-featured as something like Java or Python, but since alternative libraries tend to be compatible with the bundled StdLib (both use the same `option` and `list` type), it means that having libraries dependent on different ones is not a huge issue. Learning resources, can be confusing though.
I'm not sure why it's a problem certain data structures are mutable and some are persistent; the documentation is usually fairly clear and you usually have a performance characteristic in mind when selecting your data structure.
Universal equality and hashes are indeed a little annoying (here an `Eq` typeclass would be great!) but I do understand the original decision to have them inside. At the very least, it's not pointer based equality and since records and most types don't have subtypes, you don't run into as many issues. You can of course use the equality methods on the modules themselves and since `Hashtbl` and co have functors, you aren't penalized for it.
Strings are a problem-ish (what older language doesn't have string issues ha!), but there is utf-8 support now with [3].
> but just two years ago it was common to use Makefiles to build OCaml projects
It is true that OCaml has a unix-y flavour, but honestly I don't really understand the snipe at make. Sure there are alternatives, but make is bundled on most distros, has a quick startup time and a lot of people are familiar with it.
More of the community is moving towards dune as well, which is pretty good; the docs could do with better indexing but there are some features such as promotion which are really nice.
> was no standard way of doing compile-time metaprogramming
I haven't used ppx much so I can't really comment here.
Finally they've missed the biggest reasons to /use/ OCaml.
My friends. Algebraic effects in ocaml 5! I did protohackers and AOC with effects and it was a blast! Im not a daily ocaml driver so my code can be a bit clunky, but i love the patterns effects unlock, especially around module coupling.
OCaml is one of those languages (like Raku and Nim) that I really want to learn and do something in at some point but usually end up not bothering because it's easy for me to just start up new personal projects in Python like I'm used to.
Yeah, I'm not sure I understand the author's complaint. OCaml has modules, associated types, anonymous modules, GADTs, polymorphic variants, and row polymorphism. All of which are kind of like interfaces.
As far as I can tell, the complaint is maybe that ocaml doesn't have something exactly like C#/Java interfaces (which I think is a fair assessment). So maybe the author is lamenting that they can't just rewrite their java code in every language?
Although, they also mention rust traits. Which at first blush look very similar to c#/java interfaces (at least closer than anything ocaml has). However, at least in my experience, rust traits don't quite fulfill the same niche as c#/java interfaces because rust cares a lot about when you can figure out the size of your objects. So every time I tried to use them like I would a C# interface, I wound up regretting the decision.
Ultimately, this article feels like a superficial "I don't like ocaml" blurb. There is a lot to be warry of with respect to ocaml. Although, the fact that ocaml still exists as a niche language with a lot of very significant maintenance and active development (effects + multithread recently landed), shows that the author did not pay sufficient attention.
The author is plenty aware of them, I can assure you (I used to work with him on the Glasgow Haskell Compiler; most Haskell programmers are plenty familiar with the ML module system, it's a highly coveted feature by many.)
Anyway, what they're referring to has been rehashed a billion times already in the relevant communities. ML functors being good has little to do with it; even with functors and modules, you're required to actually pass them around and construct them explicitly and use it at call sites. If you want a Set for Strings, you make an instance of the Set functor with the proper implementation of the signature which specifies the Set ordering (or whatever, assuming that's the signature needed), but then you still have to use the functor explicitly and pass it around. You can have more than one, for good reason, so making this explicit usage implicit is tricky. In contrast, in Haskell, there is one ordering for each (nominal) type, so there is no need to refer to specific instance of `Set String` or whatever to define the ordering. `Set` uses `Ord`, which has an unambiguous implementation. It is resolved implicitly for you.
A lot of people just call this an "interface." Hell, even I do that. It's just a generalized term for "ad hoc polymorphism" where you don't need to guide instance resolution, I guess.
The author even quite literally (in like, the next sentence) says that modular implicits would help this issue -- it would allow many uses of an instantiated module to become more implicit (e.g. uses of something like "turn this thing into a string for debugging" without having a menagerie of individual string_to_ functions). But they are not yet upstream. Even then, implicits have their own tradeoffs, and the design spectrum here has no clear winner. Even Haskell has adopted Backpack, which takes a different spin on the whole thing using mixin-modules (no functors), but tries to keep the characteristic power of functors available in a languages where typeclasses reign.
Much of this is also relevant in the design of theorem proving communities, e.g. Lean4, which heavily relies on implicit instance resolution in a similar form to modular implicits; yet it doesn't have modules in the ML sense. They really want this approach though, because in math you really do have lots of instances you might refer to by name. But in programming it's not always the same; implicit resolution of ad hoc instances is often very useful.
It's not really a weird or unusual complaint, it's been rehashed to death, but maybe it could be less ambiguous.
When writing about a language which has a well-known feature called 'interfaces', it behooves the writer to not use 'interfaces' to mean 'ad-hoc polymorphism' to avoid confusion. Not doing so led to the predictable confusion.
I can kind of understand the author's complaint. A problem with OCaml is there are several different ways to achieve the same thing, and they're not compatible.
For example, you can do OOP-like interfaces/abstract classes with `class virtual`, but classes and modules don't mix together nicely. Your virtual class cannot abstract over modules, nor do functors abstract over classes.
So you end up either using modules/functors pervasively, or using objects pervasively. It's like there's two separate languages competing for use.
Using modules seems to be more common, but it overshadows some features of OCaml's object system which are pretty unique to it and useful: row polymorphism and functional objects.
Obviously, functors have a clear performance advantage because they're expanded at compile time, and objects have the vtable overhead.
I think it would be interesting to see a variant of OCaml which goes all-in on the object side of things. Strip away the modules and functors and a provide standard library based around functional objects.
Someone mind explaining how these help? I'm unfamiliar with OCaml, but looking at the documentation of modules, I don't understand what they have to do with interfaces in the OOP sense(EDIT: or, rather, ad-hoc polymorphism), which I'm pretty sure is what the author meant.
The point as I understand is that interfaces in the OOP sense try to mimic what modules do. Therefore OCaml not having them is not a serious accusation since everything you try to accomplish with interfaces / design patters you can and should be able to do with modules. Here is the article explaining the concept: https://www.pathsensitive.com/2023/03/modules-matter-most-fo... but I have to note that I never worked in OCaml so this is just my theoretical understanding.
To be fair, ocaml doesn't have something exactly like c#/java interfaces. Given some problem that you might want to solve with a c#/java interface, you can also solve it with ocaml modules by flipping the coding structure on it's head. I'm not going to try to explain exactly what this looks like because I actually don't really like that structure of coding, so I don't feel like I'm going to do it justice.
BUT ocaml also has row polymorphism and polymorphic invariants, which you can use to get something much directly closer to c#/java interfaces. The major difference is that with ocaml it's structural typing and with c#/java it's nominal typing. However, at a high level you can write the code in a very similar structural pattern as with what you get in c#/java.
Clearly not considering he mentions typeclass in the article.
Module signatures are similar to interface and parametrized modules allow you to do what you would do with abstract class: you define a signature and then write a module using these functions. Parametrized modules are more or less a generalization of functions which goes from module to module.
Sorry, I initially wrote 'interfaces as in OOP' to distinguish them from module interfaces, but had 'ad-hoc polymorphism' in mind. 'interfaces' is much too overloaded of a term.
Ocaml has no ad-hoc polymorphism but you can use parametrised modules to write generic code which you will have to specialise at some point. That brings you ninety percents of the way in term of what ad-hoc polymorphism is used for. It's more cumbersome but I prefer it to the complexity involved by what Scala does for exemple.
The OCaml object system is still parametric polymorphism (trying to pass itself as subtyping polymorphism through row variables) rather than ad-hoc polymorphism: there are still no functions that only work on a finite subset of types. In other words, a function may work on any objects that has a method `m`, but you cannot have the same method that has different implementation for `int` and `float`.
It uses structural typing not ad-hoc polymorphism. Plus it’s rarely used and it used to give hard to parse error messages when you made type mistakes. I don’t really see it as an adequate replacement for ad-hoc polymorphism.
The author brings up modules and functors. A more charitable interpretation is that they want ad hoc polymorphism but are handed parametric polymorphism instead.
That's a big "if". The author themselves mentions modular implicits and points out that they haven't been updated in something like five years. I don't think modular implicits are going to happen in Ocaml in our lifetimes.
Almost all modern statically typed languages have closures, higher-order functions/methods, lazy streams, and combinators that run efficiently. Persistent/immutable data structures can be implemented even in C.
Also, OCaml has no tracking of side-effects (like in Haskell), and the language and the standard library have lots of features and functions with mutation, such as the array update syntax, mutable record fields, Hashtbl, and the regex module.
The only thing that makes OCaml more “functional” than e.g. Dart, Java, or Rust is that it supports tail calls. While having tail calls is important for functional programming, I would happily give up on tail calls if that means not having the problems listed above.
When you mix imperative and functional styles tail calls become less important. For example, I don’t have to implement a stream map function in Dart with a tail call to map the rest of the stream, I can just use a while or for loop.
In my opinion there is no reason to use OCaml in a new project in 2023.
You might want to make it clearer that this is a literal quote from the last paragraph of the article. Are you posting this because you agree/disagree?
An interface allows you to pass different "structures" to the same function so long as they adhere to the same spec. In Ocaml this is accomplished through modules, functors, and module signatures. Ocaml's module signature can play the same exact role as interfaces in Haskell, Rust or F#.
The module system is arguably more expressive than what can be accomplished to interfaces. To give an example, Ocaml suffers from the same problem as rust does with having two standard implementations of a async runtime. Just like rusts: tokio and async-std, ocaml has Lwt, Async, (and newly added to the mix Eio).
Whereas in rust most libraries just implement one of these systems, and you'll have to use compiler directives to support both. Ocaml's module system means that you can describe the async runtime as a signature and make your entire library generic to the async runtime it runs on top of. Most of the well-used libraries do this, and so you don't have to worry too much about which runtime you decide to use.
Clearly, a language that can do that must have in some place a system that can replace interfaces.