What are the biggest reasons newcomers give up on OCaml?

I don’t know Letters, but I have an OCaml script which uses mailx as a quick and dirty way of notifying me of events via email. That blocked on sending and it turned out the reason was that mailx does not send until the other end of its stdin pipe is closed to indicate end-of-email, and neither Unix.pipe, nor Unix.create_process on forking, sets the FD_CLOEXEC flag on its pipe handles by default. The solution was to pass Unix.pipe the ~cloexec:true option as one of its arguments. Dunno if that is anything to do with your problem.

I have just fixed my 2 issues with Ocamlnet (and conf-gnutls).

I’ve never used it for that, but Curl can also send emails:
Sending email - Everything curl :slight_smile:

2 Likes

Happy to spend some time on it. There has been 3 threads about similar topics + the survey results. Probably showcasing the results in a more web-y fashion might make the problem more visible.

1 Like

You should do as you think is best, but personally I’m not particularly looking for a more web-y presentation, the important part is to produce a summary / coherent feedback that can be tracked and turned into actionable issues. Just a markdown document posted as a gist somewhere or as a new topic on the forum itself would be fine by me. But again, if you are willing to do the work, you decide.

2 Likes

frankly, I see three tasks here, not one. Of which I would do the 1st and 3rd with curl.

Frederic, does this mean that the ocaml code you shared is out-of-date? [I haven’t fetched it yet – just finishing up some regexp PPX rewriter work]. If you could share your fixed code, that’d be useful, too.

Lovely! Thank you, Edwin! I wish there were a place where these sorts of snippets were collected. I don’t use monads much, but when I do, I’d love to refer to such a cheatsheet.

I may have missed the point, but the equivalent of try/catch for something like this is, at its simplest, terminating the monadic chain with:

function | Ok x -> ...
         | Error x -> ...

So at its simplest your case becomes:

let (let*) = Result.bind

let double_it x =
  if x < 10 then Ok (x * 2)
  else Error "argument is too large"

let () =
  (let* x = double_it 2 in
   print_int x ; print_newline () ;
   Ok ())
  |> function | Ok _ -> print_endline "all ok"
              | Error s -> print_endline s

I think you probably meant something else?

This thread is getting a bit long and hard to follow (unfortunately Discourse only supports flat discussions by design, although even one level of nesting might’ve helped to keep the discussion more on track). There is some very good feedback here, questions, and solutions to certain problems, but all mixed together (the quoting feature sometimes helps to show which reply is meant to which comment).

A markdown document summary in a github PR to some repo (e.g. even a PR in someone’s own repo from one branch to another) would allow some threaded discussions/comments/improvements to be gathered in a bit more organized manner.

2 Likes

Yes: what I meant was, of course I can write that code – I mean, I’ve known what the Error monad is since I studied PL with Prakash Panangaden from M.J.C. Gordon’s The Denotational Description of Programming Languages. I’ve programmed “in the right-hand-side of standard semantics” many times, writing monadic concurrency systems a number of times to implement things like high-concurrency web-stress-testers in OCaml. I’m very familiar with A-translation, and use it all the time. Sure, it’s all straightforward stuff.

But if monads are still at such an infantile stage of development (after only (checks notes) … 30 years, that the obvious things that people would want to do with it don’t have standard canned idioms (or better, combinators), then it really isn’t ready for prime-time, is it?

I should state my position clearly, I guess: I always prefer to program in “direct style”. Always. The only time I willingly use monads (the Error monad, typically) is when I need to use a library (like bos) that itself uses the Error monad. If bos didn’t use the Error monad, neither would I.

Here’s a more-complete version of my problem with monads in OCaml:

I’m aware that monads are derived from looking at the right-hand-side of denotational semantics equations for ML-like languages, and noticing common patterns of code. Those common patterns become monadic combinators, and then people write code using the combinators, again in the RHS of denotational semantics, and by doing so, get the effect of programming in the language on the left-hand-side. But that left-hand-side language has more than function application and let. It’d be nice if the people who push these monad things, would, y’know, explain how to do all the other things that the LHS language lets you do. Or even the most important things. B/c otherwise, people like me spend time to figure out how to do it, and when we waste that time, we wonder to ourselves why anybody bothers with monads.

Or as I once joked to Phil Wadler at DIKU in 1993: “I always program with monads: I use the SML monad – you know, the SML compiler – all the time”.

2 Likes

OCaml doesn’t have typed exceptions (yet), which is why using the Error monad is such a necessity. If you’re not careful it can make debugging more difficult though (no stacktraces, unless you’re careful to use the ‘trap_exn’ variants) , but it makes the sources of errors more explicit so most of the time I’m willing to make the tradeoff.

Here is an article written by a colleague of mine that shows how to define 2 combinators: one for success, and another for failure (//=). This was written before OCaml had support for custom let operators though: Monadic Error Handling. Explaining what a monad is has become a… | by Christian Lindig | Medium (search for //= on that page), but it shows how you can succintly combine error recovery with otherwise implicit error propagation (i.e. if has_owner fails then run chown, then check whether has_perm failed and so on):

 is_dir st >>= fun () ->
    (has_owner uid st
      //= fun _ -> chown path uid gid)  >>= fun () ->
    (has_perm perm st
      //= fun _ -> chmod path perm)     >>= fun () ->
    (has_group gid st
      //= fun _ -> chown path uid gid)
    (* improve error message, if we have an errror *)
    //= fun msg -> error "fixing existing %s failed: %s" path msg

There are other idioms though, e.g. being able to write some Lwt code to retry some network code on specific failures N times with exponential backoff is quite nice. Or throttling to have at most N concurrent executions past a certain point.
Typically it is used to make the error-free path as easy to write as possible (and make error propagation implicit). Handling errors breaks that flow and convenient syntax.

The best way that I found to handle errors is not monadic, and works with traditionally written code: it is the ‘with_’ pattern. Typically used for ensuring you close your resources, but can be used to define retries (‘with_retry_on ~condition’), that you log any internal exception (‘with_log_exceptions …’), that you put a timeout on some network code ‘with_timeout ~timeout:…’, throttling some code (‘with_limit ~semaphore…’). However most of the times these are hand-rolled, or provided in different libraries.
Most of these work even without monads, but some of them are more useful if you have one (e.g. the timeout one can rely on cancelation provided by Lwt, imperfect but works at least for networking).

A cheatsheet, or guide on how to use these sounds like a good idea. Quite often OCaml libraries give you a grab bag of functionality and it is up to everyone’s inventiveness on how they piece it together into something coherent and well designed (with some exceptions: certain libraries do take care to provide more consistent documentation), and most of the time some familiarity with the problem domain is assumed.
I’m not very good at writing blog posts and such, but I could try to put together some markdown examples somewhere as a start that others could then improve on.
Not quite beginner level topic though, although I think even beginner will stumble upon an error monad or concurrency monad in OCaml pretty quickly…

2 Likes

Downloaded in the same directory (Index of /~loyer/ocaml):check_rss_fixed.ml

However, see a poor man’s version here: Poor man's static exception analysis with alerts

That’s a weird way to look at what a monad is. Monad is just a matter of composition, the monadic bind is just a natural generalisation of the reversed application operator (|>) (which is the bind for the identity monad). When you have a functorial parametric type F : type -> type (i.e. you can map over it), then you have this diagram:

          f                 g
 A   ----------->  B   ----------->  C
 |                 |                 |
 |                 |                 |
 V      map f      V       map g     V
F(A) -----------> F(B) -----------> F(C)

On the top line you have 3 types A, B and C and 2 functions f : A -> B and g : B -> C. You can go from A to C by composing f and g. Then on the bottom line, you have the image by F (the parametric type) of the top line. You can go from F(A) to F(C) by composing map f and map g which should be the same as mapping the composition g . f since F is functorial.

And now with a monad, what you want to combine or compose, it’s the diagonals on the diagram, or in other words functions with type A -> F(B) and B -> F(C). That’s all a monad is, nothing less, nothing more: it allows you to go from A to F(C) by following the diagonals. And if F is the identity function on types, then the top and the bottom line are the same and its bind is just (|>). Hence, the let binding operators are just a natural generalisation of the usual let binding.

But, there is nothing else that different monads have in common, i.e. binding is the only common idiom between monads. Nevertheless, in the case of the result monad, you need to understand what is its monadic behavior compare to exception. So look at the definition of its bind:

let bind x f = match x with
  | Ok x -> f x
  | Error _ as e -> e

In other words, when you use the result type as a monad you never catch the error but you propagate it. So, if you want to catch it, you have to explicitly match your result value as @cvine told you; or use a combinator that does it for you (for instance one with this type catch : ('a, 'b) result -> ('b -> 'a) -> 'a). And, in principle, it’s similar to this code:

let f x = match int_of_string x with
  | exception Failure _ -> 0
  | n -> n

Edit: a full example to compare with the use of try... with

let ios i = try Ok (int_of_string i) with Failure s -> Error s

let foo i j =
  let (let*) = Result.bind in
  match
    let* i = ios i in
    let* j = ios j in
    Ok (i + j)
  with
    | Ok n -> n
    | Error _ -> 0
1 Like

So I spent a little bit of time trying to summarise this thread: Summary of "What are the biggest reasons newcomers give up on OCaml?" · GitHub (which wasn’t particularly easy and it was slightly opinionated in the sense that I left some bits out like the long discussion on whether OCaml wants to grow vs. thrive etc.). I agree it would probably be better to have it in a format that would allow threaded conversations per subject but I don’t have the capacity to do that.

It isn’t a perfect summary but hopefully a checkpoint for anybody new coming into the discussion as it appears to still be carrying on.

Having just re-read most of this 200+ comment thread, I would say the things that immediately stand out to me are:

  • It would be very convenient for the compiler to be able to ship with some deriving features to aid in printf-style debugging.
  • It would be nice to have a standardised notion of “library” or “package” (dune vs. ocamlfind etc. etc.)
  • A bunch of items for dune (which could all be issues, maybe people opened them and didn’t post to the thread that they did).
  • Windows support.
  • OCaml libraries need more tutorial-style/example-driven documentation.

Moreover, it seems that there were lots of little bugs/fixes that could and should easily be turned into issues on some project’s repository and I would encourage people going forward to open those issues :))

17 Likes

That’s a weird way to look at what a monad is.

I’ve noticed there are two camps: those who say “monad is just a …” and write another overly abstract unenlightening explanation, and those who think of a monad as a model of richer (effectful) computations, the map of a territory, an encoding of something we can’t express directly. Sure, you can’t treat the latter as a definition and you can contrive an example where it wouldn’t make much sense, but that’s what it is 99.9% of the time. My impression is that the experts (Moggi, Wadler…), having reached the post-rigorous stage, fall into the second camp. To camp 2, camp 1 may seem “weird” for being so comfortable with the model while barely saying what it really models.

3 Likes

To be honest, I don’t really see where I differ from camp 2 nor what is overly abstract in my presentation. What you want to do with a computation is to bind its result and compose it with the result of other computations, and that the only common idiom with distinct monads. After, each monad models a specific kind of computation and will have its specific idioms, and in the case of the error monad it’s the propagation of errors (or exception). When you really understand that, nobody need to explain you how to catch exception when you use this monad (which what the case of @Chet_Murthy). The parallel between all these following code are natural:

(* usual code with exception *)
let foo_exn s s' =
  try
    let i = int_of_string s in
    let j = int_of_string s' in
    Int.to_string (i + j)
  with Failure s -> s

(* variation of exception with pattern matching *)
let foo_exn_bis s s' =
  match
    let i = int_of_string s in
    let j = int_of_string s' in
    i + j
  with
    | exception Failure s -> s
    | n -> Int.to_string n
;;

(* the same but with the error monad *)
let ios s = try Ok (int_of_string s) with Failure _ -> Error s

let foo s s' =
  let (let*) = Result.bind in
  match
    let* i = ios s in
    let* j = ios s' in
    Ok (i + j)
  with
    | Ok n -> Int.to_string n
    | Error s -> Printf.sprintf "%s is not a textual int representation" s
;;
1 Like

How and where do we go from here? Usually in a large community, there’s a small committee with a lead that shoulders the work. Or perhaps a foundation to prioritize it in some fashion, assuming that the work in question is deeply wanted.

I think there are a few things to take away from the summary and the entire thread. Firstly, there’s actually a tremendous amount of excellent work happening (and that has happened) to improve the situation for newcomers coming to OCaml, which is great and should be celebrated! However, it probably hasn’t been communicated effectively with the broader community. An example for me was that I was unaware of the improvements to compiler error messages and there are RFCs in that area too. Working out better ways to communicate that sustainably might be good.

Another thing that dawned on me was a lot of the suggestions on this thread come from people who are definitely not newcomers, which is fine! But when filtering the suggestions down to those people that reference either teaching experience and the user survey (using my own Outreachy experience anecdotally too) I think it becomes clear that documentation, compiler errors and Windows support bubble to the top as very high value projects to work on. Again anecdotally, nearly every Outreachy applicant/mentee I’ve worked with has been on a Windows machine, have always struggled to understand the compiler error messages and quite rightly struggles to know how to write a dune-based project (they’re usually contributing to pre-existing projects so this isn’t so much of an issue during the internship).

Sure, and OCaml kinda has a lot of these pieces already with things like the OCaml Software Foundation, the Code of Conduct team, the individual teams managing each core tool (opam, dune etc.), the core dev team on the compiler etc. Perhaps something more centralised is necessary to push the agenda of new-comer developer experience as a higher priority (if that is desired)? I quite like how the Processing Foundation have fellowships for working on things and a diverse team overseeing and leading.

Again with my Outreachy hat on, some of these ideas seem like great follow-up Outreachy internships. These could be in the context as internships in companies that use OCaml for example. I think it is unreasonable to just assume the community/people will for free work on these issues, not everyone has the luxury and privilege (like myself) to hack on OCaml things as a hobby. This gets very company-dependent on just how that would all work, but I’m just thinking of ideas at this point to move past just pointing out reasons newcomers give up on OCaml and instead try to start to fix them.

10 Likes