My thoughts on OCaml
osa1.net
osa1.net
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.
It does sound to me like you stopped reading after the first point. Maybe you didn't give the post a chance?
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.
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.
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".
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.
> You don’t write tuples without parentheses
> Same for the precedence rules of expression. You just use begin and end for blocks
This is exactly what I wrote, but in a Stockholm-syndrome-y way.
> The author is just complaining that you have to mark blocks like in every language.
He points out a real ambiguity in the syntax where the parser literally parses the same-looking and same-behaving statements completely differently.
Sure it's solved by "you just write begin-end" workaround that you internalise. Or it could be solved at the syntax level (like erlang and elixir)
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.
Is there ANY such language at all regardless of popularity?
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.
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.
Sometimes I go back to F# and sigh because I can just print any complex data structure. Sure, there’re limits, but it’s just one less pain.
Regardless, really enjoy everything else. I cloned and built the Dune repo to try to help fix something. Compiled very quickly for its size.
So ... can you ELI5 to me how that is different from how you can for instance compile a C program against different libc implementations?
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.
Pretty much the same way you'd do in Haskell or Rust: you add a `[@@deriving show]` directive to your datatype.
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.
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.
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.
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") ```
for,
``` fn foo(t: T) -> String { if(t == T::ZERO) { t.show() } else { "not here".into() } } ```
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.
[1] https://github.com/ocaml-ppx [2] https://peddie.github.io/encodings/encodings-text.html
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.
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.
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.
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?
My OCaml:
let foo (x: bar): baz =
(\* Is this quuxable? *)
match is_quuxable x with
| Yes ->
(* Then, we have to ... \*)
All the OCaml I see in the wild: qlet%bind f = 's 't 'a 'b (fun ꙮ -> ꙮ ((<_<')) ꙮ)
[@@ppx_gov_backdoor (~b~e~p~i~s)]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.
someLongAssFunctionName someQuirkyParameter
And we get a function that accepts one parameter. But in Rust you would have to do this: move |another_one: i32| {
some_long_ass_function_name(some_quirky_parameter, another_one)
}
Which is not easier to read. some_long_ass_function_name(some_quirky_parameter, ..)
which would be more readable without giving up on punctuation. func(param1, _)
Every once in a while you have to make some change to that expression. // you may want to write
func(param1, func2(_))
// but you need to write the full lambda
param2 => func(param1, func2(param2))Of course, I haven’t read every file, so maybe I got lucky with my random sampling.
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.
Related: https://willamette.edu/~fruehr/haskell/evolution.html
Now, Haskell and shudder Scala shudder, on the other hand.
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.
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.
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).
The fact Microsoft seems to pretend it doesn't exist is bewildering. Surely the .Net ecosystem can have two languages being promoted.
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# is beyond microsoft, a lot of early adopters are still there.
What are the fruits of that increased team on the VS, VS4Mac and VSCode tooling, and .NET SDKs?
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.
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)
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#.
[1] https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-10...
I get your example of ‘seq { 1; 2; 3; }’ but list, array are not even shortened.
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).
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....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.
https://en.wikipedia.org/wiki/Dangling_else (just to show that this is something people commonly consider to be a problem with Java, C, C++, and JavaScript)
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.
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.
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.
Since the author mentions Haskell as a positive example multiple times, I don't think a lack of C-like syntax is the problem for them here.
Also, the author claims:
> Since 2013 I’ve had the chance to use OCaml a few times in different jobs, and I got frustrated and disappointed every time I had to use it.
So it's safe to assume "in a few days" their dislike for OCaml syntax won't go away.
For the record, I don't think syntax is a big issue.
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].
[1] https://github.com/coalton-lang/coalton
[2] https://coalton-lang.github.io/reference/
[3] https://coalton-lang.github.io/20220906-quantum-compiler/
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?
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.
3. All Coalton functions compile to Lisp functions. There's a guide that describes what is promised about interop (https://github.com/coalton-lang/coalton/blob/main/docs/coalt...).
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. :)
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.
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.
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.
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.
Not fixing your language is a big red flag.
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).
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.
[1] https://blog.janestreet.com/testing-with-expectations/ [2] https://github.com/janestreet/sexp
brew install dotnet-sdk
dotnet new console -lang F# (in your project directory)
Start VS Code in that directory, install ionide via extensions, write code… dotnet run- JetBrains Rider (best IMHO)
- Visual Studio for Mac
- VS Code with Ionide
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.
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.
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.
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#.
What in your opinion makes .NET so well designed?
(I think learning Standard ML first was very good for my programming practices, but I wouldn't suggest it for real projects)
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.
I made this project ~10 years ago after making similar observations: https://github.com/morganstanley/hobbes
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.
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.
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."
> 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.
Everyone has to find their own style I guess.
[0]: https://jaspervdj.be/posts/2018-03-08-handle-pattern.html
I love this so much.
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.
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.
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.
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.
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.
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?
Agreed :)
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.
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.
Maybe you could bring any example of such ocaml functionality?
Had to look up to see where Java is at for sum types. First link I found. https://sfietkonstantin.github.io/2021/09/03/Java-sum-type.h...
Also, somehow I rarely have need to use something like that personally.
> And ease of use is important
the question is if insignificant syntax sugar worths switching to unpopular platform with undeveloped toolings and uncertain support.
do you have specific example?
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.
Many times compiler tooling will do just fine with good old visitor patterns, enums etc..
Sure there may be small amount of errors missed, but that's not always as critical as availability of libraries and integration.
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.
As a result of all of that, the app feels very solid to use and has comparatively few runtime errors.
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.
I personally found the MirageOS, build systems and language design episodes terrific.
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.
Why wouldn't a company use Brainfuck?
The documentation for abs[1] explains: "Warning. This may be negative if the argument is Int.min_int."
I think that's just the nature of two's complement and not necessarily a fault of OCaml. C++ is also like that[2].
> 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 am one of the developers of Flix)
algebraic data types
pattern matching
first-class functions
extensible records
parametric polymorphism
type classes
higher-kinded types
light-weight polymorphic effects
type aliases
Hindley-Milner type inference
CSP-style concurrency
buffered & unbuffered channels
first-class datalog constraints
polymorphic datalog predicates
constraints with lattice semantics
stratified negation
interoperability with Java
unboxed primitives
keyword-based syntax
redundancy checks
monadic let\* expressions
expressions holes
compilation to JVM bytecode
full tail call elimination
core standard library
parallel compiler architecture
human friendly errors
interactive mode
Visual Studio Code support
Nice!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.
The only thing one could argue is the lack of type classes, i.e., overloading. (Calling it "interfaces" is somewhat wrong.)
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).
popcount is widely used?
> 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.
[1] https://news.ycombinator.com/user?id=4ad [2] https://ocaml.org/docs/objects [3] https://v2.ocaml.org/api/Stdlib.String.html#utf_8
You missed writing what they are :P
"The regex module uses global state: string_match runs a regex and sets some global state. "
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.