I'm surprised people care about the syntax... that's pretty much the least important part of the language.
I've got a few issues with OCaml
1. The standard library is inconsistent. Different projects use different alternatives and it's annoying to switch between then.
2. Concurrency using Lwt/Async. Monads feel more ad-hoc in OCaml than Haskell because there's no type classes and again no consensus on how it should be done. Some projects use tons of variation of `>>=`, other use `let%bind`. Again, no clear consensus and reliance on external libraries which aren't always well documented. I find code relying too much on monads to be much less elegant and simple that plain OCaml.
3. "includes". I tend to get lost in codebase which abuse includes.
4. It's a small community. If you want to work on "real world" stuff, most likely you will stumble into tooling/libraries issues. Not all of these things are very mature or documented and they're evolving fast.
> I'm surprised people care about the syntax... that's pretty much the least important part of the language.
Eh, I don't think I can agree. There are definitely more important things about a language than syntax, but a language with a great syntax can be a lot more enjoyable to use, particularly when reading a lot of code. Prior to learning Ruby I would have agreed with you, but since Ruby and Elixir, the syntax adds much to my enjoyment of working with the language. When I think of ES7 v. ES5 I feel the same way as well. You can do everything in ES5, but in ES7 the syntactical sugar makes it so much more enjoyable.
Interesting, because I find Elixir’s syntax to be rather busy, especially coming to it from F#. I love Elixir and Erlang, but Ruby’s influence on Elixir is probably my least favorite thing about Elixir.
To be frank, any language's syntax is rather busy compared to F#. That language is setting the bar how beautifully a language can look. I don't know of any that comes close, not even Haskell or Python...
Yes, please do! Warning: F# will ruin other languages for you. I find it rather painful to work in basically anything else after using F#, with gradients of pain for different languages. Haha.
And that's a good question. I have basically every book written on F#, but I can't say I have ever used them for anything more than reference.
* Functional Programming Using F# by Hansen and Rischel (might be too simple if you are already comfortable with functional programming and is out of date every now and then with changes to F# that's happened)
* Expert F# 4.0 by Don Syme and others (contains a lot of nice things by the designer of F#
One of the latest books is Stylish F# 6: Crafting Elegant Functional Code for .NET 6 by Kit Eason. I have the first edition but haven't read it.
My personal recommendation is to take the approach of type/domain driven design. That is, I start off every F# module the same:
1. Define my types with discriminated unions, records, type aliases (such as for tuples) or single case discriminated unions. Use classes when necessary but try to prefer the more functional types.
2. Start writing functions against these.
And that's basically it. One thing to recognize with F# is that it mixes OOP rather nicely. Even discriminated unions and records, which are immutable, can have members defined on them, including operator overloading (something F# is pretty good about). They can even implement interfaces and be defined with generic types, which is also nice and powerful.
I have some projects that might of interest, since they're simple enough and illustrate the above process.
Lastly, I'd suggest just starting up some projects. You could also take the Programming Languages course on Coursera by Dan Grossman. Part A uses SML, and you could port the examples and homework solutions to F# (I did so when I took the course). I also take books written for other languages and port the code to F#, usually taking a more idiomatic functional style. .NET Interactive notebooks (https://github.com/dotnet/interactive) are a great way to get started. You just need to install the .NET 6 SDK (which gets you F#) and then install the .NET Interactive Notebook extension in VS Code. That's it. There is also the book The Little MLer which gets people comfortable with discriminated unions (sum types), and I used the book and ported the examples to F#. I need to go back and finish that annotation project (https://github.com/bmitc/the-little-fsharper). I'll probably convert the script files to .NET Interactive notebooks if I do.
I've found newbie JS devs do confuse the meanings of `=>` and `function` to mean the same thing in JS and think that it's just a sugar. I wonder if there's a nice way to 'unfold' sugars via a linter or something to explain in code the subtleties.
I think your comment is the most accurate depiction of the warts.
Yeah, the standard library is inconsistent, and they don't seem keen to merge in some features when they're readily available in other libraries. Plus there are some odd implementation details in the Unix module (for example, read/write and friends) at the C level that don't seem to get much traction on fixing despite fairly clear paths to resolution.
I personally don't care about the difference between binding something to a variable and the infix operator.
What I don't like are the multiple concurrency stories, this is similar to the problem seen with the standard library vs janestreet's. Although with the module system being what it is it's usually not too big a deal to support either concurrency library (even though some libraries force you to use one or the other).
Although I'm hopeful that with multicore finally being a real thing, the community will settle on a single concurrency library that can be pulled into the standard library in the future.
Despite the issues, it is still my favorite language.
Syntax is what you deal with every time you read or write in any language. It can't be "the least important". It's probably one of the most important part when you _read_ the code, and reading the code is an important part of programming.
Just take a look at this:
[1, 2; 3, 4]
is the same as:
[(1, 2); (3, 4)]
That's just hard to read. I don't mind `;` being a separator, but tuples without parentheses is just annoying to look at in 9/10 places.
I agree the second form is preferable to the first one. But I think, most people will use the second form anyway. In my projects, ocamlformat chooses the second option.
In the case of OCaml, we can certainly find a few oddities, but really, I never found the OCaml syntax to be such an issue or to make me unproductive in any way. I'm much more prone to syntax errors in Haskell actually.
I suspect people who complain are mostly used to C-like syntax and aren't familiar with any language of the ML tradition. I agree that the barrier to entry would be lower if the syntax was more "mainstream", but I don't find there's really anything wrong with OCaml syntax, except that it's "different".
Oh, I don't think Ocaml syntax is an issue at all, I like most of it. I'm saying that, syntax isn't the least important part of a language - look at how many languages compile to JavaScript without bringing anything new ("new" being something like types in TypeScript).
> I suspect people who complain are mostly used to C-like syntax and aren't familiar with any language of the ML tradition
Yes, that's probably it. Can't really blame them - C, C++, Java, C#, PHP been very popular for long time. When I first saw Erlang, I thought it's a language for aliens.
There was a Revised Syntax (not sure if it's still a thing) which smooths out some of the most egregious warts. It requires preprocessing, but it was part of the standard distribution last I looked.
The most egregious for me was actually ;; being so close to ; to type and you'd be staring at weird type errors for ages before realizing what had gone wrong. I realize that this may be hard to convey to people who haven't used OCaml, but: The ;; terminates a top-level definition -- which was usually type-inferred when I was using OCaml... so it would lead to all sorts of "weird" type errors, when all that was missing was a simple ;
EDIT: Just for context, last looked (seriously) at OCaml ~15 years ago, last wrote it ~20+ years ago. It's cool, but it ended up just being my gateway drug to Haskell... for which I thank it :)
1. For F# you can can use both .NET and JavaScript libraries (with tool called Fable), so isn't a issue.
2. There a lot of ways to the that, which one with pros and cons, but isn't a issue in my opinion
3. For imports there are a thing called AutoOpen that is strange at first. Just need to be carefull where to use
4. .NET documentation tend to be writed for C#. So you need to understand at least a little of C# if you want to use that libraries. But also there are a lot that can be use idiomaticaly with F#, so isn't a big issue.
As others have said, F# addresses these issues. The main complaint I have with F# is when using other dotnet libraries you end up having to deal with things like nulls again, which can make the F# feel more cumbersome than it should be. I still like it though.
My main complaint is that after all these years, Microsoft's management still seems not to have a clear picture what they want it for, althought it seems to have replaced Visual Basic in feature love for newer .NET versions.
I worked in Azure for four years in a couple different teams. I didn't see a single line of F# code while I was there. I don't think they have any plan beyond supporting it as somebody's side project.
syntax doesn't typically matter as long as you are at least as productive as in other languages. i.e. if editors can do the same things with keybindings.
The problem with the (* *) comments is that I can select a block of text, a portion of which is already commented out, and commented the whole selection. You can't solve this without lossy commenting (e.g. removing the inner comment). The portion that is already commented will screw it up. This doesn't matter in single line comments since it will just add an additional "//" in front of a commented line.
Does OCaml not have nested comments? Typically a language supporting /* */ will treat them like parentheses and require each opener to match a closer. (Not that this doesn't make documenting the behavior of files/static/* harder - hence Rust and C# preferring to document with line comments.)
OCaml has nested comments (and comments are also aware of string literals). Thus any fragments of valid OCaml code can be commented without changes. In other words, the whole line below is commented-out.
I was surprised that the article mentions dozen of languages but no word of F# which is a mainstream clone/derivate of OCaml. Especially considering exactly that point.
Those dozens of the languages are the ones that the author has personally dabbled with in the past. The focus of article is their personal experience & first impressions.
It was not intended to be an exploration of what else is available in the market and how ocaml compares to them.
> Things I Disliked
> [...]
> • using ; heavily as element separator (e.g. in lists and records):
>
> let some_list = [1; 2; 3; 4; 5]
>
> You get used to this, but it’s a pretty big departure from the norm to use commas. I wonder what’s the reasoning behind this.
It appears to be a result of the limitations of early parser technology, as Dave MacQueen explains:
"One of the kind of interesting things is that the parser was written based on Vaughan Pratt's precedence parser [...] and it had the peculiar property that it was hard to reuse symbols for multiple syntactic purposes, and hence you get things like comma used for pairing that means you have to use semicolon for list separator, that means you have to use semicolon-semicolon for statement separator, so there's some peculiarities based on the fact that we didn't really know how to parse and we accepted this quick hack from Vaughan Pratt"
(https://www.youtube.com/watch?v=ua3EYopCURo&t=179s)
The article briefly mentions ReasonML, which offers a different syntax which aims to be more familiar to JavaScript programmers but is also likely to be more familiar to most programmers of C-family languages.
There’s also ReScript, which is a very similar alternate syntax which sort of split off from the ReasonML community in a confusing and complicated sequence of events which is frustrating to try to follow for someone who was just interested in the programming language and tooling: https://rescript-lang.org/blog/bucklescript-is-rebranding
> The article briefly mentions ReasonML, which offers a different syntax which aims to be more familiar to JavaScript programmers but is also likely to be more familiar to most programmers of C-family languages.
One thing I don't like about this is that they've replaced:
> let x = 1 in
with
> let x = 1;
In plain OCaml you only use semicolons on lines that evaluate to unit, so they're a clear indicator that you've done something with side effects. ReasonML loses this useful visual distinction in order to look more like JavaScript.
Agree. ReasonML threw the baby out with the bathwater. If ever there's a language to not immatate it's Javascript.
Would be interesting to see a something that updated some ocaml quirks, but stayed true to the spirit of the syntax. Ie comments and multiply but not ;
That split was really bad. At the height if its popularity (yes, there was a brief moment in the sun) it was also influencing the OCaml community IIRC, bringing improvements like friendly error messages and syntax improvements.
Now though... Who cares about either of the Re* languages?
ReScript community is well and alive and is actively working on improving the ReScript compiler and tooling. V10 just came out with records with optional fields - https://forum.rescript-lang.org/t/ann-rescript-v10-released/.... Async/await is in the works. The language is really really good (especially say comparing to TypeScript). Make sure to check it out.
ReScript is amazing. I really don't know much about the split (it's not like I didn't try to understand it, I just can't understand the anger of non-Ocaml developers at the split).
Rescript allows me to write Javascript in a sound type system, while remaining very close to how you would write code in Javascript.
Rescript to me feels like Typescript + a very prescriptive linting system which prevents a lot of problems, and since the linting system wouldn't let you do certain things, might as well get rid of them from the Type checker, this results in a much faster compiler.
I have been using ReScript for (almost) a year as a production programming language. There were a few limitations and confusions at first, (and I don't think it will be replacing TypeScript in any near future) but I can't even think about going back to a work environment uses TypeScript, the dev experience is so good :)
I think ReScript community is pretty active right now, I have big expectations about future 10.x releases (I'm working for a company uses ReScript as a main programming language) there are less buzz recently though.
I never understood the motivation for allowing tuples without surrounding parens. There are a few very simple functions that look cuter this way, but it constantly trips me up in more complex situations.
This phenomenon is actually another example of the phenomenon he mentions. In strict precedence parsing, operators are assigned meaning rather than productions of a grammar (which would could those operators in distinct contexts), so once you've decided a comma indicates a tuple, it's easier to just allow this syntax.
Which version of that are you talking about in Python? The biggest related gotcha I've seen in Python is when you have a trailing comma `x = 1,` and accidentally create a tuple.
My biggest frustration with OCaml is having to deal with all of that cruft. I've learned many languages over the years, and I think I've just reached the point where I've lost patience with having to deal with what I would consider substandard syntactic elegance.
This sort of thing is why Rust's Editions are great and why Vittorio Romeo's Epochs (P1881) for C++ would have been the right choice.
I like Editions because of the cultural consequences, Rust's community assumes they can fix things and so they set out with that goal, even in cases where Editions won't quite do it - while the C++ community tends to accept the state of the language as a static fact and just "take it". But this sort of thing isn't about the cultural effects, it's a direct technical achievement.
If OCaml had Editions, it could say OK, that syntax was a bit rubbish, here's 2023 syntax which fixes two things everybody hated, get back to us over the next few years and we'll decide if there are further changes needed. Without losing all existing OCaml software or demanding expensive rewrites.
Even better, changes of the sort in Editions are mechanical. It can take a bit of creative work to do it nicely but the transformations can be automated, so people who have "old OCaml" and wish they had "new OCaml" can push a button and get on with their day. Having both this and the feature which keeps old code working unchanged, add up to an experience where the community can move forward, on syntax at least, and not be trapped with yesterday's mistakes.
The syntax is a little unorthodox, but just having a literal syntax _at all_ for things like lists/arrays beats having to call `new` on some random class you have to import and then doing a bunch of in place updates to add all the data.
In this respect C# has knocked it out of the park with shorthand initialisation syntax for arrays and objects. But it's newer and could look at what other languages are doing/not doing in that regard.
I don't mind the cruft. I think a bit of harmless quirks can give a language character.
It actually makes the learning easier. Now that I know the story why, I have memorized how elements are seperated in OCaml for probably the rest of my life. I just need to remember the story.
Plus not reusing symbols for multiple syntactic purposes actually fits nicely into the general Ocaml philosophy. Plus is makes my complexity-hating, minimalist heart happy.
Though I am at that point in my life where I don't really care about syntax that much to begin with. Yes syntactic complexity matters. How fast it can be parsed, how easy macros are implemented, these things can matter but the actual syntax: boring. White-space sensitive or not, curly braces or begin/end and all that stuff is so dull. I am happy with whatever.
Honestly, I'd argue that few people do. At a previous employer, we used it in situations where would have been much better off with a more mainstream language, in my opinion.
Of course, there are few situations that require any particular language.
Of course. But for example if you are writing a compiler, an interpreter, or anything that has to manipulate something that resembles a structured tree of meaningful data, OCaml is particularly well suited :).
Any language with ADTs and pattern matching is reasonably suited for that task, and unlike 20 years ago, these aren't obscure features anymore, so there are many options.
Yes but OCaml syntax is Pascal inspired. It’s always going to be a lot more elegant that anything inspired by the horror that is the B syntax. It’s just less familiar to people who grew up using C, Java or JavaScript.
One man's 'cruft' and 'substandard syntactic elegance' is another man's 'decades of backwards-compatibility preserved'.
People are of course free to use the newest languages with the shiniest syntax (and many do), it's just that they pay the price by using untested, unproven languages when they could have been using ones which have been battle-tested in production for literally decades.
What are some good alternatives? I don't find begin and end as easy to spot at a glance if the indentation is off. I'm not sure why I don't like indentation based, but I don't in practice. A good IDE helps me care less, and I don't think I could write python well without a good IDE anyway.
Scala 3 actually doesn't really eliminate braces in any meaningful way. Sure, they're optional for delimiting traits, classes, and objects. But braces are heavily used for block scoping in Scala too. E.g. a very common pattern:
retry(Seq(1.second, 2.second, 3.second)) {
// some action that can fail
}
This still requires the braces as of right now. I think the syntax changes will just confuse more people than it helps, at least for the time being.
I see your point. However you don’t have to define that thunk inline. You can write it separately and just pass it by name retry(…)(thunk). Not sure what signature of the retry method looks like.
You can define it separately but it's often not the idiom. Especially for call-by-name arguments like in this case:
def retry[A](backoffSeconds: Seq[Int])(action: => A): A
In this case you can't just define a 'val action = ...' because you don't want it to be run immediately, you want to run it inside the 'retry' method only. You could define it as a 'def' but then you're just introducing two different delayed evaluation concepts into this small piece of code. The cleanest way is to just directly pass in the action and Scala ensures it is delayed evaluation thanks to the CBN parameter type:
I feel like F# addresses quite a few items on the "dislike" list:
* It has // for single-line comments
* You can use newlines as a list separator, as in:
let some_list = [
1
2
3
]
* Similarly, multiple let-bindings can be separated by newlines and the "in" is optional in most cases
This comes from some drawbacks: for starters, when I mention "newlines", it's actually significant whitespace, so you may or may not like it (as a heavy Python user, I just don't care).
Yes, and if you use a code formatter, like Fantomas, it can automatically modify code to be in the more legible format (like Prettier.js), so you don't confuse tuples with list elements.
I've been using f# for a couple years, and after the honeymoon phase is over, I've started to really value the benefit of native interop with dotnet libraries.
Most enterprise SDKs/libraries (e.g. AWS S3, Stripe, Twilio, Rollbar, etc) have a dotnet SDK, and it just drops into an F# codebase as if it were a 1st party lib (you reference them using object-oriented dot notation instead of functional style, but still it just works).
Even when Microsoft puts work into, for example, cryptography security updates or JSON parsing performance, F# benefits from that.
This pretty much squares with my experience as well. I also want to shout out CS3110 as being an excellent (free!!) learning resource, and that having both a well-written textbook with exercises and short accompanying video lectures is a wonderful combination. I will never not be amazed at and grateful for high-quality university learning material like this being made freely available.
I had a very brief experience with OCaml debugging some seriously complicated networking code. Putting aside the quirkiness of the language, its compiler is a wonder of the world. It is practically as fast as GO which is a miracle given how much more sophisticated OCaml is.
OK but you can write code golf in any language, OCaml provides every opportunity to write readable code. The second snippet you provided is incomplete, if we were to write it in Python terms it would be something like:
(resolve=lambda info: ...)
But let's say for the sake of argument that it's an argument to a function `f`:
f ~resolve:(fun info () -> ...)
One of the simplest ways to improve readability is to use argument name punning:
let resolve info () =
let sleep =
let open Lwt.Syntax in
let+ () = Lwt_unix.sleep duration in
duration
in
Lwt_result.ok sleep
in
f ~resolve
To be fair, it’s hardly code golf. It’s just that Lwt takes a monadic approach to concurrency and that’s always going to look a bit strange to someone unfamiliar with using monads and who doesn’t know that (>|=) is fmap.
I never understood why Lwt chose this aweful operator by the way. That’s a really unfortunate choice. My guess is that they really want people to use their ppx extension. I really hope that eio will solve the current concurrency libraries mess.
It’s the same for the first exemple: if you don’t know (:>) is upcasting, you are going to wonder what you are reading. I have to confess I had to look for it. Despite using Ocaml professionally for a bit a decade ago, I never had to upcast anything.
It’s hardly beautiful code but it’s fairly readable.
Not great, not terrible; the language supports annotations which mean nothing to the compiler but which pre-processors can take advantage of, and there is a framework called ppx which you can use to write your own preprocessor. There exist many pre-processors to do things like add inline tests, generate getter/setter/pretty-printing functions, and so on. Here is an example:
I've got a few issues with OCaml
1. The standard library is inconsistent. Different projects use different alternatives and it's annoying to switch between then.
2. Concurrency using Lwt/Async. Monads feel more ad-hoc in OCaml than Haskell because there's no type classes and again no consensus on how it should be done. Some projects use tons of variation of `>>=`, other use `let%bind`. Again, no clear consensus and reliance on external libraries which aren't always well documented. I find code relying too much on monads to be much less elegant and simple that plain OCaml.
3. "includes". I tend to get lost in codebase which abuse includes.
4. It's a small community. If you want to work on "real world" stuff, most likely you will stumble into tooling/libraries issues. Not all of these things are very mature or documented and they're evolving fast.
I'm wondering if F# suffers from the same issues.