OCaml - first impressions

Hey, I’m new here. Trying to learn some OCaml, I would like to share my first impressions, and my frustration. I hope this is okay, and that I’m not in the wrong category. (Also, my english is quite bad.)

  • The ecosystem is intimidating to me. There are many different tools, the “standard environment” implies : OCaml compiler, OPAM, Dune, Emacs, Tuareg, Merlin, and so on. I come from Racket (mainly), where things are obviously easier. I’m not saying this is a bad thing, I guess it’s the “UNIX philosophy”, just… intimidating. I would love having an IDE like DrRacket for OCaml. Learning would be much more fun. Emacs is a fine IDE, but I’m here to learn a language, not a complex text editor and sixty-four other tools.
  • OCaml is not “batteries included”, which means, I need external libraries to write programs and do concrete stuff. However, to install and use a library properly is not a straightforward thing (for me at least), and external libraries are less documented than stdlib (generally, code and interface files are the documentation itself). Because of this, I tend not to use external libraries at all, which means I don’t have http, concurrency, GUI, or whatever.
  • Because of this, OCaml feels like a language for UNIX hackers working on compilers or trading systems or automated proofs, not a language for everyone. OCaml hackers develop tools for themselves when they need to, and I guess they don’t need a fancy IDE or things like that.

On the bright side, the OCaml community seems very welcoming, and aware of these difficulties. I understand that the ecosystem is evolving and the situation was much worse fifteen years ago.

About the language itself :

  • It generally looks good and well designed (there is some eccentricity here and there, but the general ideas are sound, which is impressive for an “old” language).
  • It also looks big, and a lot of things to learn.
  • The module system, while powerful, scares me a little bit… :stuck_out_tongue: That and the fact that I have to write an interface file for each module (which means a lot of files, and remember that I don’t have any IDE…), I’m often tempted to write my programs in one big file (which is bad, since the main tool in OCaml for encapsulation and structure is the module system).
  • “Objective Caml” people sell it like a multi-paradigm language who can do OOP, but in practice, it seems people don’t bother with objects at all (me neither, to be honest). If OCaml could be redesigned without compatibility issues, I wonder if it still had objects ?
  • I miss comprehension/iteration facilities (like Racket’s for, for/list, for/fold, or even the baroque loop of Common Lisp). OCaml people seem to encourage a functional style over an imperative one (when it makes sense), but since I don’t have list comprehensions or anything, too often I have to chose between “manual” recursion or smart composition of higher order functions (which is not very readable and feels a bit “low level”).
  • List comprehensions are just an example, but generally : the type system and module system are fantastic and obviously a lot of efforts were made into it, but the rest of the language often feels rough and blunt or low-level. Other languages have a lot of practical things to make life easier (I’m thinking about Racket, Haskell, Python, for instance). In OCaml the answer is either “write it yourself” or “use X library with PPX extension”. The comparison with Haskell is canonical, because the OCaml language is more practical in essence, but Haskell folks made the language feel more modern and sometimes more practical (I know Haskell has a bigger community and it helps).
  • For a beginner like me, it’s hard to learn the “standard way” to do things in OCaml. For instance, let’s talk about error handling. It seems like some people use exceptions, some people use options in a simple way, some people use options in a monadic way, some people use everything. It’s very confusing, could you explain? (Edit : I just noticed than Stream.next raises an exception, while Stream.peek returns an option type…)

Bonus note : parts of the reference manual feel outdated. I was reading the Genlex module reference and it shows code with Camlp4. It took me a long time to understand why this [< stream >] syntax doesn’t work anymore.

Sorry for all these complains, I don’t want to sound harsh or anything. I think the language and the community are worth the effort. Feel free to help me or correct me where I’m wrong.

9 Likes

Welcome to OCaml! Since you’re already an FP programmer, I think you’ll find that your learning-curve is pretty shallow compared to others. I thought I’d respond to a few of your issues, both positively and negatively:

  • Since you’re a Racket programmer, I presume you’re somewhere in the orbit of Matthias Felleisen? Have you looked at The Little MLer? That might be a fast way bootstrap yourself a bit.

  • [order of learning] Others will have different opinions, but I think that starting with the FP core, then the (meager) imperative operations, then modules, and only then OOP, would be a good way to approach the language.

  • Are you able to use opam? If not, yeah, it’ll be a serious pain getting various libraries installed. But with opam, it’s really much, much better, to the point where I rarely-if-ever have to go in and actually build third-party stuff myself. And if you have problems with opam, you should definitely ask for help, and not try to bang your head on it: it’s supposed to make it really easy to get started with OCaml, after all.

  • [OOP] Historically, the objects got added after caml-light was pretty stable, and I would characterize them as an “add-on”, rather than central to the ML philosophy. You’re right that they’re rarely used, but (as has been attested-to when someone’s asked “what are they for?”) they do get some use: there are some cases where, quite simply, OOP is the right way to model a solution to a problem. It’s not common (for us FP folks) but it does happen. I certainly would miss objects, if they were removed, even though I rarely use them.

  • [IDE] You’re right, that the IDE situation is a little messy. But OCaml has been around for nearly thirty years in one form or another (even long if you count “Caml-Heavy” before caml-light). Combine that with the fact that it started on UNIX, and yeah, any IDE is going to be a bolt-on. And honestly, as an old-school systems-programmer, I think that that’s the right choice. That said, sure, I can imagine that more people would like better IDE support. Not being an IDE user, I can’t really comment.

  • [Emacs] What? Emacs an IDE! Non, mais non! C’est un cap, c’est une peninsule! grin

  • [Exceptions and errors] OCaml is a higher-order applicative language (like Scheme), yes? It’s always had exceptions, and monads are a bolt-on. Some people use them, others don’t. It’s a matter of style maybe; even though I myself think that monads are rarely the right tool, I -have- had occasion to use them a few times. Kind of like objects.

  • [the Stream module] I don’t know what Stream.next is for, but the Stream module was written as a support for stream-parsers/printers (those things with the syntax [< ...>] that you rightly noted are no longer supported); I don’t think anybody uses Stream except for that purpose, though I could be wrong.

  • [Option vs. exceptions] This depends a lot on the use to which a thing will be put. So for instance, if the use of a function is to be applied to an argument and return a result, that gets further-processed, then raising an exception for errors is a nice pattern. If the function is meant instead to be applied to each of a list of arguments, and the first argument for which the function succeeds is the one we wait (like List.find_map then returning an option might be a better pattern. Back in the day, I’d use a combinator try_find which took a function that either returned a result or raised an exception, and applied that function to a list, returning the first successful result.

  • [Module system] This is one of the most-powerful thing about OCaml, and merits careful approach. But really, it’s immensely powerful, and thus valuable.

8 Likes

These are all great points. Do try out the new OCaml editor tooling like VSCode + the OCaml Platform extension, it’s a great editing experience. No need to learn Emacs at all.

Anyway, you are right about a lot of the issues you raised, especially about the ecosystem. But you should know they are being actively worked on. Please see Dr Anil Madhavapeddy’s excellent recent overview of the OCaml ecosystem to learn how that’s progressing: https://youtu.be/E8T_4zqWmq8

Finally, I do want to touch on the point you raised about the ‘feeling’ of OCaml. You’re right that it often feels low-level. For example to derive a JSON codec for a type in other languages you would often use a macro or a ‘deriving’ mechanism; in OCaml you plug in a PPX like ppx_deriving with yojson and then attach an attribute to the type definition, which then does codegen of the conversion functions. Another great example is of course the lack of list comprehensions (I’ll come back to this later).

This is actually true. OCaml is a bit lower-level. I think the best way to think about it is that it’s like a balance between Haskell and Go. Or in other words, a balance between Unix hacker philosophy and PL researcher philosophy. I think this is actually a feature. This is the design space that OCaml occupies. I know that people often don’t agree.

Re: list comprehensions: OCaml almost has them now via its new let-operator syntax. I say almost because it doesn’t have guards. For example:

let ( let+ ) list f = List.map f list
let list = [1; 2; 3]

let result = let+ elem = list in elem + 1
(* val result : int list = [2; 3; 4] *)
2 Likes

Dread Cthulhu, I pray to thee, please, eat me first! Holy mackerel, that’s … disturbing.

3 Likes

On second thought, we can approximate guards using more let-operators:

let ( let+? ) list f = List.filter f list
let list = let+? elem = list in elem > 1 in
let+ elem = list in elem * 2
(* - : int list = [4; 6] *)
1 Like

I realize that this approach has the not-to-be-discarded-lightly value of sticking to a predefined grammar and all, but … wowsers, it just hurts the eyes to read it.

Are you serious guys ? A newcomer report a unfriendly syntax, and you pop out an advance way to redefine the let operator ! Ten post later I bet the discussion will present how to build her own ppx extension !

Welcome to OCaml Syssan !

12 Likes

I should have thought flatmap, returning an empty list for elements to be filtered out, would be vastly more straightforward. Sometimes (albeit maybe rarely) monads are an improvement.

let (let* ) list f = List.concat_map f list

As @Chet_Murthy mentioned the batteries are in opam which you should definitively get to grips with it to get the power (yes it’s a chore that every language out there has its own packaging system, but that’s what we have for now).

Some of the libraries are pretty-well documented but that may not be easily discovered online. Once you installed your batteries with opam you can access their docs via odig. This will generate this kind of documentation for you local install of packages. See the odig manual for more information.

5 Likes

Thanks for your interest!

I will definitely learn to use OPAM and external libraries. Thanks for the odig tip by the way, I didn’t know about that.

Ok, that was not clear at all reading the reference manual. What if I want to use streams/generators/whatever? Should I use plain closures instead? I’m inclined to do that. And what about the new Seq module?

I think the module system is the big thing to learn, for me. Maybe OO too, and some advanced stuff in the type system (GADT / Polymorphic variants / …). Basic functional and imperative features are easy to grasp since I know some other languages.

For now I use Vim, which I know the basics. I should probably look for a way to use Merlin and other stuff inside Vim.

Fair enough. I understand now that OCaml competes more with Java/C++ than with Python/Ruby. It doesn’t feel that great for scripting and rapid prototyping, but it certainly is great to develop robust applications.

1 Like

stream parsers/printers were off-and-on supported either by a preprocessor, or the compiler. Eventually support ended up in camlp4, and when that was ditched, the ocaml core stopped supporting them, period. But they’re still supported by camlp5, and work fine. I honestly think they’re the best way to write parsers, but hey, de gustibus. Streams are very different from Seq, btw. You could say that streams are just the runtime support for the language elements of stream parsers/printers.

You mentioned Python - and Haskell - in the original post as “having a lot of practical things to make life easier.” How much would it help, on that score, to just make the core types less tedious to use? For example, strings - Python a[3:] vs. OCaml String.sub a 3 ((String.length a) - 3)? It isn’t hard to implement some of that for yourself - not the syntax, but I mean in this particular example a variant of sub that defaults the last parameter - but then you have to carry all those odds and ends around for all your “rapid” prototyping.

Once you get it working the vim-merlin story is really rather nice. Additionally if you end up using the dune build system it will automatically build the definitions for other ocaml files/modules/libraries which is really nice.

(I also found https://dev.realworldocaml.org useful for learning the tooling around ocaml, though they focus on utop (a REPL) a bit too much for me personally)

2 Likes

As already mentioned by @yawaramin I would highly recommend VS Code with the OCaml Platform plugin paired with GitHub - ocaml/ocaml-lsp: OCaml Language Server Protocol implementation

If you prefer using vim then I would suggest using a plugin manager such as GitHub - junegunn/vim-plug: 🌺 Minimalist Vim Plugin Manager and installing GitHub - neoclide/coc.nvim: Nodejs extension host for vim & neovim, load extensions like VSCode and host language servers. where you can quite easily enable the above mentioned language server: Language servers · neoclide/coc.nvim Wiki · GitHub

4 Likes

Welcome @syssan!

A few more points to add to the excellent comments of @Chet_Murthy:

  • wrt “batteries included”: provided you allocate some time to setup opam (which I can only recommend), these days I think I’ve been quite happy with containers as a “standard library extension”. It is quite straightforward to use and decently documented. I would recommend to start with that, and then see if you need to add more “domain specific” libraries (for instance you mentioned http or GUI).
  • wrt the Genlex and Stream modules: unfortunately these two modules are indeed somewhat outdated and would probably benefit from being explicitly deprecated. (feel free to open a pull request if you feel extra motivated!)
  • wrt comprehension/iteration: indeed I think that people usually either write recursive functions by hand or use higher-order combinators (note that e.g. the CCList module from containers does contain more of these useful combinators than there is in the standard library).
    One gets used to these combinators, I guess :-).
  • if you are looking for iteration semantics that are closer to lazy, possibly infinite sequences, then you should use the Seq module from the standard library.
    One of its uses is to serve as a data exchange format to transfer data between data-structures (notice that there are of_seq/to_seq functions in modules that define data structures, for example Hashtbl).
    But I’ve also used it quite successfully to write complicated iteration/filtering/enumeration pipelines, by composing up the nice combinators provided by the oseq library. (I’m hoping most of the oseq combinators can be ultimately merged into the Seq module of the stdlib.) I have used this programming style when solving Project Euler exercises where one has to “enumerate all triangles such that X, then filter those such than Y, and ultimately count how many there are such that Z”. It’s then easy to define the infinite list of all triangles, then filter/count etc using the oseq combinators, whereas manually writing recursive functions for that would have been fiddly and not very compositional.
  • wrt “error handling”: my perception is that it is somewhat a matter of taste, which is why it’s hard to get a feel of the “currently dominant taste” (which has also evolved over the years). I think older APIs tend to use exceptions a bit more liberally, whereas nowadays using exceptions to indicate errors tends to be more frowned upon, and people prefer to use option instead, or result (when there’s some additional error message or data in the error case).
    Two instances of these two different styles would be for instance the Unix module from the standard library, where almost all functions can raise the Unix.error exception, and the OS module from the bos library, where all possibly-failing functions instead return a result (for instance, Bos.OS.Dir.create).
    Since OCaml 4.08, chaining many result-returning operations (i.e., working in the result monad) can be done quite easily using the let-operator syntax (just define let (let*) = Result.bind) which makes me now prefer the result-returning style rather than the exception-throwing one, as it is more explicit and less error prone.
4 Likes

Interesting, I wonder if it suggests a shift towards away from imperative? I’m thinking lately about an API wrapper similar to one I’ve already done for OCaml, where I handled C++ return status by raising an OCaml exception, and it occurred to me that maybe it should be a Result type … and then I remembered, half the time there’s no useful return value, and it seems kind of silly for a function to return (unit, 'b) result. Which wouldn’t be an issue in a more FP system.

That is, assuming there has been any real shift in what people frown on, or that it even makes sense to look at such issues in terms of whether someone frowns on it.

1 Like

The issue of “to monad or not to monad” has been discussed previously, and I sure don’t want to dredge it up: bytes on the disks of this forum’s servers are too valuable for that (grin). But I think it would be a bit of a stretch to say that the style of code with exceptions is somehow passe’. One of the shining strengths of OCaml is that it supports multiple paradigms well. So if you want to program as if you’re in C++, only with a better GC and stronger typing, you can do that. If you want to program as if you’re in an applicative Haskell, you can do that, too. And you can do something in-between, too. These aren’t weaknesses: they’re strengths of OCaml.

I rarely use monads. But I -do- use them, and not merely to use @dbuenzli’s fantastic libraries; I actually have used them b/c I wanted the particular capabilities of monads (viz. to run a type-checker across all the values in a list, and only afterwards accumulate all the errors to print them out, rather than stopping after the first error).

Likewise, the monad-enabled libraries I’ve seen (like fmt and rresult) have nice support for dealing with exceptions, and I’ve used those profitably.

1 Like

If it seems silly, don’t do it. OCaml is a multi-paradigm language, which means that much of the style is left to the programmer’s best judgement, for better and for worse.

1 Like

Obviously I’m new here - In OCaml, “monads” mean when you use Option or Result? I mean, I can see the connection, but … I’ve been seeing the word here and wondering, as I’m used to seeing it used for a more elaborate, general notion of a system of computations “within” a type, across a variety of types and computations.

For what I understand, a monad is just a thing which implements bind and return, even if these are not polymorphic (since we don’t have typeclasses).