OCaml at First Glance
batsov.com
batsov.com
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.
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.
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.
The official docs/guide/reference are actually really good, and I refer to them a lot when using some feature I'm not familiar with: https://docs.microsoft.com/en-us/dotnet/fsharp/what-is-fshar...
F# For Fun and Profit is well-known, but I can't say I use it a lot: https://fsharpforfunandprofit.com/
The same author's (Scott Wlaschin) book is very good: https://pragprog.com/titles/swdddf/domain-modeling-made-func...
As for books, I have always liked:
* 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.
https://github.com/bmitc/the-ray-tracer-challenge-fsharp
https://github.com/bmitc/nand2tetris
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.
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.
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.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".
> 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.
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 :)
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.
Until the syntax starts impeding with your understanding of the code, or is impossible to look up in documentation/google.
OCaml IMO is a bit over the line with the number of ascii art used.
I definitely agree with your other points. Though I only touched OCaml very briefly.
It gets better. Since 4.08, the preference would be `let*`.
Edit: I forgot about https://github.com/microsoft/qsharp-compiler. IDK if any server code is in F#, but at least the compiler is.
The syntax is the user interface of a language. It matters.
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.
(* let x = "*)" (* inner comment *) *)This is not the case for C, C++ or JavaScript for a start.
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)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
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.
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 ;
Now though... Who cares about either of the Re* languages?
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.
[1, 2; 3, 4]
is a list of tuples, of type (int * int) list, equivalent to [(1, 2); (3, 4)] match a, b with
| 1, 2 -> x
| _ -> yI 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.
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.
Of course, there are few situations that require any particular language.
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.
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.you can also use import language.experimental.fewerBraces with nightly builds to write code like this
List(1,2,3).map: c => ...
> You can probably use () instead
I would rather not as using braces for the final argument has been the Scala idiom for a long time now.
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: retry(Seq(1, 2, 3)) {
println("Trying")
}* 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 casesThis 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).
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.
http://a-nikolaev.github.io/fp/ - if anyone is interested in more learning resources.
I think the ReasonML/ReScript guys had the right idea tbh, but I'm not interested in writing JS with it.
What are some of the things that weird you out about OCaml's syntax?
> You can actually compile ReasonML to native code with dune.
Yeah but the community doing native ReasonML is small and the maintenance has really petered off.With OCaml 5 looming you're better off just biting the bullet and writing OCaml if you're going to do it IMO.
> What are some of the things that weird you out about OCaml's syntax?
For whatever reason, I really dislike the named-argument syntax -- and some of the operators make it hard to read compared to named alternatives IMO.Below examples showcases some of these:
let variables = (json_variables :> (string * Graphql_parser.const_value) list)
~resolve:(fun info () ->
Lwt_result.ok (Lwt_unix.sleep duration >|= fun () -> duration)
) (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 ~resolveI 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.