Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
OCaml Programming: Correct and Efficient and Beautiful (cs3110.github.io)
347 points by philonoist on June 24, 2022 | hide | past | favorite | 239 comments


A quick scroll through the comments and it looks like no one is actually talking about the book itself, so I will. I came across it at a really good time in my computer science self education. It helped me learn about some essential data structures and algorithmic analysis. The video lectures interspersed in the text augment the text and vice versa. The sections without videos were tougher for me to digest, but are written clearly and bear revisiting and study. If you, like me, are looking for a good next step after Grossman’s PL MOOC, I recommend spending some time with this incredible free resource.


I'm enjoying the discussion of commutative diagrams, abstraction functions, and representation invariants in section 6.3. It is very well motivated and easy to understand.


I found my experience trying to work with a large OCaml base a nightmare — when signatures changed in an unstable dependency (e.g. function argument removed and nested inside another), the errors spat out by the typechecker were utterly incomphrehensible.

This was largely due to automatic currying in OCaml — if I have a function call "some_function arg1 arg2" and "some_function" adds a third argument, that call becomes a function that requires a single argument, arg3, but the typechecker message that tells you all of this is well-nigh unintelligible.

Switching to Rust was a blessed relief, not least because Rust has much better developer tooling and documentation (but also no automatic currying).

It made me think that OCaml is efficient and beautiful if you're the only person touching your specific codebase (which I think is true in the vast majority of cases) or if many of your colleagues are deep OCaml officionados with PhDs, but it's not a good collaborative language for the rest of us.


Do you have a more precise example in mind?

The example that you are describing should emit an error message of the form

    This expression has type type_of_arg_3 -> return_type
    but an expression of   type return_type was expected 
which seem alright to me. (But I cannot not be called an OCaml aficionado). The type error might be delayed in sufficiently polymorphism context but that is a more infrequent occurrence (outside of functors whose error messages have been improved in OCaml 4.13 partially for this reason).


yup, specific error message is here: https://news.ycombinator.com/item?id=31861450


Looks like that is exactly the error message predicted. It’s expecting a list of result values, but is getting a list of functions from some stuff to that result.

IME, it’s definitely true that OCaml doesn’t have the didactic error messages that many have come to like in Rust et al. They said, imo they generally give the information needed solve problems and I like their concision. It takes a bit of time to learn to read them however.


> It takes a bit of time to learn to read [OCaml's error messages]

Yeah, and I think you could say the same of Rust when it comes to the borrow checker's messages.

But in Rust's case the problem it's trying to describe is itself pretty complex. The complexity of OCaml's error messages are really not justified by the problem they're warning you about, which is itself very straight-forward.


The ML languages started appearing in the 70s. Rust is a very modern language.

Where do you think rust got (more than) half of its type inspiration from?

Either way no one has to use any language. Personally OCaml is not my cup of tea--Haskell is. But it is a great tool and it's solving hard problems.


I used StandardML at university in the early 2000s, and I appreciate that OCaml is an older language that heavily influenced the design of Rust.

Given that history, it’s instructive to note the sorts of things (like auto-currying) that Rust did not inherit.


Rust is a different language with a different audience.

If a functional language is presented today that does not automatically curry calls, I have no interest.

Just because the tool does not work for you does not mean there's a defect in the design.


I think that it is better to not forget that expressiveness comes at the cost of a richer world of misbehaving code. If you are used to a world where functions have more than one argument and must be applied to exactly the right number of arguments and nearly never returns another function, starting a curried language suddenly throws you in a situation where many small mistakes like `plus 1` are not caught early anymore. Moreover, those delayed mistake are here to allow room for an expressiveness that you are not using yet. This is a genuinely frustrating situation. And I do hope to improve OCaml type error messages to better highlight type difference at some point in the future.


Thanks for the work you’re doing — though it’s mostly too late for me, good to know that people are putting a lot of thought into these questions.

The sense I have got from my experience is that OCaml is a great language for people who are slightly less error-prone than I am.

My style of coding is to write a lot of slightly dodgy code in one breath, then use the typechecker to tell me all the things I got wrong. Even in a language I’m very familiar in there’s still a lot of trial and error.

OCaml currently seems great for people who are better at getting it mostly-right first time — I’ve seen a lot of OCaml code written by people with PhDs and backgrounds in type theory. Most other languages don’t have that sort of academic pedigree.


> My style of coding is to write a lot of slightly dodgy code in one breath, then use the typechecker to tell me all the things I got wrong.

That's exactly the style OCaml excels at.

> OCaml currently seems great for people who are better at getting it mostly-right first time

That's because they got it wrong a lot at first, then learned how to get it right. You've heard of people talking about how they internalized the Rust borrow checker's rules after some time, right? Well, internalizing a typechecker's inference rules works very similarly (faster if you read OP).

> I’ve seen a lot of OCaml code written by people with PhDs and backgrounds in type theory.

That's unfortunate, it's a nearly perfect language for industrial work. Tiny example of how easy it is to hack together useful tools with it: https://dev.to/yawaramin/practical-ocaml-314j#proof-of-conce...


That's weird, I really don't see the interest of 'automatic currying', optional/volontary currying? Sure that's interesting.

But automatic I don't see the point, it's like automatic conversion in C: not the good default IMHO.


To me, the previously linked error message seems pretty clear.


Looks similar to what TypeScript will offer you.


This mirrors my experience in SML/NJ, where for the longest time the most common error message the compiler would spit out was 'tycon mismatch' and a Google search would not tell you what a tycon was (it's a type constructor).

I think, unfortunately, I've observed a pattern in much functional programming (F# being a blessed exception) that relatively excellent computer language designers suffer from the utter ineptitude of the human language competency of the tool authors. These days, I am far more interested in an elegant tool chain and support infrastructure than an elegant language.


If you ever do any frontend work, you should give Elm a try. The tooling is lightweight but effective; and the compiler error messages are best-in-class.


Sadly I think SML/NJ has some of the worst messages for SML; Poly/ML and MLton both end up with something more reasonable than the dreaded

    Error: operator and operand do not agree [tycon mismatch]


FWIW, F# is very similar to OCaml, but its error message in this case is usually quite clear. E.g.

    This expression was expected to have type
        'int'    
    but here has type
        'string -> int'


yes, that would be a straightforward error message!

But I got the error message

    Error: This expression has type
         ((locl_ty * Tast.pos * ('ex, 'fb, 'en) Aast.expr_) list ->
          Result_set.t)
         list
       but an expression was expected of type Result_set.t list
       Type
         (locl_ty * Tast.pos * ('ex, 'fb, 'en) Aast.expr_) list ->
         Result_set.t
       is not compatible with type Result_set.t 
Which is not half as readable.


With OCaml type system permanently burned in my mind, I parse that error message as "there is an ` _ -> Result_set.t ` arrow type which is not compatible with type `Result_set.t`". Do you think that the issue is that the arrow `->` is too hard to spot?


Yes, I think that's the issue.

I built a reasonably popular typechecker for PHP, so I have some experience with the DX for these sorts of programs. I don't think it's good for a couple of ASCII characters and line breaks to be the difference between an acceptable and an unacceptable type annotation. It requires, as you say, for the OCaml type system to be burned into people's minds for them to immediately spot the issue.

In a similar context Rust will just say "function expected 5 arguments, you gave it 4" which has a very obvious remediation.


I think it would be great to address this as you describe, but at the same time as somebody who has dabbled in OCaml but is still very wet behind the ears, I don't understand how this error format requires the type system to be "burned into your mind".


I have no insight into what is hard and what is easy to implement here, but might it be feasible to break up the error into parts, the first just as you wrote? Along the lines of:

1. Error: This expression has type ('x -> Result_set.t) list but an expression was expected of type Result_set.t list

2. where 'x = (locl_ty * Tast.pos * ('ex, 'fb, 'en) Aast.expr_) list

The solution suggested by the parent of your post sounds desirable also, but perhaps the currying semantics of Ocaml makes that difficult or poorly-defined?


Emphasizing the arrow (or in fact the most narrow error) is definitively a good idea in that case. Counting the number of arguments is in general a bit problematic: it is possible to end with an accidental functional value due to an extra argument send to a sufficiently polymorphic function for instance.


It might help to make the difference more obvious by also showing the parts which unify properly, or introduce alias variables for complex type expressions (for instance "(int * float array) list as 'a") to move away some of the noise in the error.


For comparison, in Haskell you would get for this definition:

    myfun :: Int -> Char -> Bool
    myfun a b = undefined
if you give too many arguments:

    myfun 1 'c' 3

    • Couldn't match expected type ‘t0 -> t’ with actual type ‘Bool’
    • The function ‘myfun’ is applied to three value arguments,
        but its type ‘p10 -> Char -> Bool’ has only two
if you give too few arguments:

    putStrLn (myfun 1)

    • No instance for (Show (p20 -> Bool))
        arising from a use of ‘print’
        (maybe you haven't applied a function to enough arguments?)
I like how it says

    applied to three value arguments ... but its type ... has only two
and

    maybe you haven't applied a function to enough arguments?


Elm's developers have been working hard to make better error messages for these cases, and it really pays off even when experienced. Here are Elm's versions for comparison:

  > myfun 1 'c' 3

  -- TOO MANY ARGS ---------------------------------------------------------- REPL

  The `myfun` function expects 2 arguments, but it got 3 instead.

  5|   myfun 1 'c' 3
       ^^^^^

  Are there any missing commas? Or missing parentheses?

  > not (myfun 1)

  -- TYPE MISMATCH ---------------------------------------------------------- REPL

  The 1st argument to `not` is not what I expect:

  5|    not (myfun 1)
             ^^^^^^^

  This `myfun` call produces:

      a -> Bool

  But `not` needs the 1st argument to be:

      Bool

Besides being 'beginner friendly', this just takes away cognitive overhead when you can glance at an error message and immediately know what happened.

(Edit: Formatting. Note that in the console output the arrows actually line up with the source of the errors)


It doesn't look so bad to me. It's telling you that a list type was expected but you passed it a function that returns a list instead.

You're right though that OCaml isn't the kind of language you can pick up in an afternoon.


> you passed it a function that returns a list instead

I don't know OCaml really, but I would read that intuitively as a list of (functions that return Result_set.t), which is not compatible with a list of Result_set.t.


Tiny quibble that I wouldn’t normally post but I think illustrates the readability issues here:

Should that be “Result set type was expected”, not “list”?


No, I think this: Result_set.t list

means a list whose items are of type Result_set.t.

Also I think the notation X.t is idiomatic for a type defined by module X.


Regardless the function does not return a list.


It looks like I missed a parenthesis.


Can you explain (rather than state) what readability issue you think exists here?

I don't know if you see a real readability issue or not, but I do think it's easy to jump to "hard to read" when that's only true in a certain parochial sense: it's not what we're used to.


This seems exactly the same to me just with more complicated type names.


Indeed it is :)

Dropping an example in TryOcaml[0]:

    let f : int -> int = fun _ -> 1 ;;
    val f : int -> int = <fun>
    let ex : int list = [f] ;;
    Line 1, characters 21-22:
    Error: This expression has type int -> int but an expression was expected of type
      int

[0]: https://try.ocamlpro.com/


Stockholm syndrome arrives quite quick in this case, as you become accustomed to this form as meaning "you're missing an arg"

I agree the first 6 months of this is mind-numbingly frustrating.


This is not Stockholm syndrome. It's just learning a pattern.

That's one of the top few specialties tasks for humans. After you learn it, you immediately know its meaning, it becomes easy with no thinking at all required.

Anyway, the plain English example from Elm on the sibling comment is much better, because it's already easy to understand before you learn to recognize it.


> Stockholm syndrome

Is a lie fabricated to deflect criticism of the person who coined the term via abusive ad hominem of the critic.

Surprisingly, that tends to predict the quality of arguments invoking Stockholm syndrome pretty well, too.


What does Stockholm syndrome even have to do with this?


From what I've seen of Rust though it adds its own brand of complexity due to its basic requirement for maximal efficiency which precludes having a garbage collector.

I don't think I would want to use Rust unless I really needed a language with near optimal performance. OCaml on the other hand seems suitable for programs where correctness is important but performance isn't the driving concern. At least I'm considering using it for that type of application, though I'm not yet a serious OCaml programmer (still working my way through this course actually).


A big part of it is that currying is idiomatic, which, I think, is a design mistake that most functional languages carry as a historical baggage. But multi-argument functions can just as well be represented as functions of tuples, even in OCaml (and, if I remember correctly, it is idiomatic in SML), and then you get a proper error message about the type of argument rather than the result.


We develop a large OCaml application and find the error messages are fine. Changing signatures is an advantage when refactoring because it identifies all the places you need to make changes.

In addition I greatly prefer having a garbage collector around.


I think the issue is the compiler error messages being poor. Automatic currying is wonderful for productivity (in my experience).

Elm is not really an alternative to OCaml (or Rust) but it shows how nice compiler error messages can be.


> when signatures changed in an unstable dependency

The problem is not OCaml here.


I have a good point of contrast: I'm doing a similar task with an unstable Rust dependency, and when APIs change the error messages from the typechecker are crystal clear about why things are incompatible.


I see, you made the beginner mistake with OCaml. You should never read the OCaml error message except if all other options failed. Try to not read those messages, but just look at the line where the error occured. You can use -annot or -bin-annot when compiling and the Tuareg function caml-types-show-type in Emacs will be your best friend.


This is old advice :-). Use the LSP server and dune and the error displays instantly, no further setup required.


I don't get it. Is it sarcasm?


No. You don't need this anymore.

> You can use -annot or -bin-annot when compiling and the Tuareg function caml-types-show-type in Emacs will be your best friend.

Just open OCaml program in some editor with LSP(Vim, NeoVim, VS Code, Emacs) and it will instantly show you the error.


That's sad. I need dune to make it work.


Dune has its quirks but it's pretty good at its job. The community is standardizing on it and every time someone pops up on the forum asking for help with weird issues because they didn't use dune, I think they're just making thing hard on themselves for very little benefit.


He is just recommending the use of LSP server, which requires little to no configuration and works with any editor/IDE.


Yes because we all work in perfect world under optimal conditions.


Forgive my ignorance, I'd would like an example of the "functional" (hehe) benefits of automatic currying


Partial application and function composition are the meat-and-potatoes of functional programming.

Automatic currying is syntactic sugar that can make these two easier and clearer as it gets rid of a bunch of named arguments and lambdas. For example, I can write:

  foo a b c = a * b + c  
I can then apply this partially like this (foo2 just takes argument c):

  foo2 = foo 10 20  
and compose it with other functions, for example like this:

  composed = foo2 >> bar >> baz
Without currying, you'd have something like:

  foo (a, b, c) = a * b + c 

  foo2 (z) = foo (10, 20, z)

  composed (a) = (\b -> foo2 (b)) ((\c -> bar (c)) (baz (a)))


I find this phrase "Automatic Currying" that people are using in this thread to be very strange.

It implies that `(add 3)` is being magically converted into `(\x -> (add 3 x))` by some fancy front-end feature.

But no, `add` is just a function that returns a function.

This pattern is core to the entire paradigm of functional programming, all the way down to the lambda calculus.

All functional programming languages are essentially just fancy syntax around lambda calculus. They have different implementation strategies (strict vs lazy), different type systems, and they have been extended with different primitives (floats, operations on floats, etc).

But at their core, they are all just lambda calculus.

In the lambda calculus, there is ONLY functions, and all functions take one argument. There are no tuples. So, multiple arguments are implemented with the pattern in question. For example:

mul = (λx. λy. λz. x(yz))


Before I start, let me say that I'm on mobile and I don't know how to post code on this site. I'm assuming backticks work.

---

One benefit is that functions are more reusable. Functions with more arguments can be partially applied and used where applicable.

For instance, if you wanted to increment the values in a list you might do something like this in python:

  map(lambda x: x + 1, [1,2,3])
But in an ML like language you can do something like:

  map((+ 1), [1,2,3])
Because plus is actually a function that takes two arguments and can be curried down to one argument.

Additionally, you can think of currying as a form of dependency injection.

For example, if you wanted to inject a database client and a logger before running a query you might do something like this:

  def queryer(db, logger):
    def _q(query):
      logger.info("before query")
      results = db.fetch(query)
      logger.info("after query")
      return results

    return _q

  rows = queryer(db, logger)(query)

You could write it a bit more simply in this hypothetical ML like language:

  queryer db logger query =
      logger.info("before query")
      results = db.fetch(query)
      logger.info("after query")
      results
MLs don't require parens/commas for function calls, but you can use parens to force a particular execution order. The lines below have the same result, but some produce and execute intermediate curried functions

  rows = queryer db logger query
  rows = (queryer db) logger query
  rows = ((queryer db) logger) query
It's a bit contrived, but you have the flexibility to provide some arguments based on the call site rather than the definition site. I.e., the first example was broken up into 2 function calls at the definition site, but the second example could have 1, 2, or 3 function calls.

---

I hope this makes sense and I wish I could preview my comment to see if it was formatted reasonably!


There are many similar warts in the ocaml type system. Parametric polymorphism in ocaml sucks as well.

If you haven't tried haskell, you probably should. Its type system is much cleaner (and more powerful, if you want) than ocaml's, and many details of Rust are derived from Haskell.


Haskell is a great language but has plenty of its own warts. The value prop and trade offs between OCaml and Haskell really makes it hard to use one as a drop in for the other imo.


Strong disagree; I have thousands of hours with both and I would essentially never recommend ocaml over haskell unless your company already has an ocaml codebase/ocaml expert employees.

I'm very conscious of the existence of pareto tradeoffs; I am asserting that, in this case, there is essentially no tradeoff to be made. Haskell is equal or better (sometimes significantly so) in almost every relevant domain. For domains in which ocaml is a better choice, it is not the globally best choice (i.e. both ocaml and haskell are bad in those domains).


>Haskell is equal or better (sometimes significantly so) in almost every relevant domain. For domains in which ocaml is a better choice, it is not the globally best choice (i.e. both ocaml and haskell are bad in those domains).

I think this assertion is false, but it would take a lot of careful evaluation to prove it one way or the other. The most salient counterexample that comes to mind is modularity and namespacing.

Of course it's possible I'm wrong, and Haskell is objectively superior in every regard. But the OCaml language and ecosystem has [a decades long track record of stable and evident success stories](https://ocaml.org/industrial-users) and I find it well suited for the kinds of problems I like to tackle. I also like the prevailing vibe in the (still) small community.

This exchange reminds of a difference I've observed in the cultural tendencies in the OCaml and Haskell ecosystems: in my experience, OCamlers tend to not be very invested in arguing for the supremacy of OCaml.


> but it would take a lot of careful evaluation

This is what I was doing for a number of years.

> The most salient counterexample that comes to mind is modularity and namespacing.

Polymorphic variants and ocaml namespacing are nice, granted. These are the two ocaml features I've ever missed while using haskell. Minor details overall though. Namespacing is not as useful with typeclasses and PVs have typing problems.

> in my experience, OCamlers tend to not be very invested in arguing for the supremacy of OCaml

Yeah, this is mostly because it's not as much of a marginal improvement, so it doesn't have as many people interested in shilling it.


What about the performance challenges caused by lazy evaluation?


Fresh in both languages. But on studying the Effective Haskell book, there's a whole chapter (chp 14) on learning how to read and write efficient and faster Haskell code. Training that mental model of understanding how IO gets evaluated didn't seem too difficult. It feels akin to remembering how to write fast SQL code - you just practice a bit and measure.

In chp 7 on understanding IO, there's also another great section that explains how IO evaluation is often confused. I share an example from the book that reads, writes, and prints files.

    Memory intensive func because it attempts to read _all_ files at once because the execution of reading and writing actually only happens when the print fn is called (putStrLn files). makeAndReadFile is actually doing both reading and writing - a normal task in all other languages.

    slow =  
      let  files = mapM makeAndReadFile [1..500] ::  IO  [ String ]     
      in  files >>= (putStrLn . show)


    Efficent version. Here we force reads and writes to actually occur per file instead of waiting til the print. Seems like a pretty easy step to misunderstand.

    safe ::  IO  ()  safe =    
    foldl ( \ io id ->   io >> makeAndShow id  ) (return ()) [1..500]


People fixate on this, and all I can say is

* it has never been an issue for me

* I don't think the performance implications are nearly as hard to understand as often implied

* in the extreme you can just enable the STRICT language pragma in your project and forget about it :)


That's funny. This has been a problem for every experienced Haskell programmer I've known who works in Haskell professionally. I wonder what domain you work in where performance and space leaks haven't been a concern?


It's not that space leaks are not a concern; it's that it's actually quite easy to avoid creating them, and if you manage to do so and it becomes a problem, it's usually easy to track down. I don't know any experienced haskell programmer who would even mention this in a list of complaints about the language (which they are sure to have, but they will likely be more abstract).


Most people don't even get to see those challenges. But make wide use of the many performance opportunities created by lazy evaluation. Anyway, when any problem appears, it is obvious, so if it was a large problem, it would be fixed by now. The only reason people keep talking about it is because it nearly never appears.

Really, lazy IO is a much larger source of problems, and even there, after a week or two writing IO people just learn to write code that doesn't break due to it.


Compile times and architecture astronomy are obvious counterarguments to the 'Haskell is always superior' delusion ;-)


Parametric polymorphic is the same in OCaml and Haskell. Did you meant to say that the value restriction does not play nicely with point-free programming?


That's one aspect of it. The ergonomics are dogshit. I remember having to use explicit quantification all the time when writing polymorphic library code. Also, without typeclasses, polymorphism is super inconvenient, to the point where it's almost exclusively used in the most critical data structures like sets and maps. It's not "the same as in haskell" except maybe in the very vague sense that they have similar underlying type theories (although ocaml's is much weaker - e.g. I remember needing to use some hacks to approximate HKTs, while haskell handles them easily).


OCaml does not require more annotations than Haskell for polymorphic functions? Both language only require annotations in the case where inference would be undecidable (polymorphic recursions, higher-rank polymorphism, and GADTs).

I know few cases where OCaml require less annotations than Haskell, I would expect the reverse to be true.

And maps and sets are not the main use case for polymorphism in OCaml. Even taking in account that it sounds like you are talking about functors, maps and sets are still not the only instance of functors in OCaml.


> OCaml does not require more annotations than Haskell for polymorphic functions?

A) I am fairly confident this is not actually true in a technical sense, although it's been several years since I've thought about it

B) In any case, in the (many) instances where you (by rule or convention) need to put a type signature on your function (e.g. because it's a top-level function, one that you export, etc.), it is a pain in the ass to make it polymorphic in ocaml compared to in haskell.

> Even taking in account that it sounds like you are talking about functors

No, I'm not, although ocaml people tend to think exclusively in "functors" (badly named, in my opinion - conflicts with the more common category-theoretic definition) because the experience of using actually parametric functions is so bad that they almost never do it.


Do you have any examples? Unfortunately from my perspective, I cannot make sense of your statements.

OCaml typing is principal in all situation where type inference is decidable. The syntax for annotation for polymorphic functions is isomorphic between OCaml and Haskell.

If I take a random module in the standard library, let's say Array, 95% of the functions in this module are parametric polymorphic. I am thus genuinely puzzled by your statement that "OCaml programmers almost never write polymorphic functions".


I wrote up a bunch of grievance examples for a blog post I never got around to publishing, but I'm traveling at the moment so can't pull up my laptop. I'll see if I can find them later.

> The syntax for annotation for polymorphic functions is isomorphic between OCaml and Haskell.

Sorry, but total bullshit. I had to use these pieces of shit all the time. https://v2.ocaml.org/manual/locallyabstract.html

> If I take a random module in the standard library, let's say Array

As I specifically called out, all the core data structures are polymorphic. (Let's not talk about the float array hack; is that still around?)

It's so annoying that pretty much every other library is monomorphic or functorized.

> I am thus genuinely puzzled by your statement that "OCaml programmers almost never write polymorphic functions".

Not sure what to tell you man. I spent 4 years reading & writing ocaml and 95% of the code that would have been polymorphic in haskell (because it would be easy/free) was either monomorphic or (multiple layers of) functors. Many/most of the devs I worked with (great devs with years of OCaml experience) didn't even know what LATs were, let alone used them, which means they were almost certainly not writing polymorphic code which "did anything" with the type. The only polymorphic code that you can write in ocaml without such things are functorial (in the categorical sense, not the ocaml sense). Arrays, map values, that's about it.


> Sorry, but total bullshit.

Ok, locally abstract types are bit weird and historical quirk due to the pre-existing use of type variables as unification type variables. But first, they only matter with GADTs and local modules (a notion that doesn't exist in Haskell). And the good syntax for polymorphic function with GADTs is `type a. a monoid -> a` which requires just one explicit quantification compared to the Haskell variant. So no, Haskell and OCaml type annotations are isomorphic.

> As I specifically called out, all the core data structures are polymorphic.

I took a totally random module from the standard library! Let me another random module after rolling a dice, Atomic. Here only 66% of the functions are polymorphics. Or do you want me to go to another library? Ok, let's go for container, and let's select another random module CCPair: 100% of the functions are polymorphic. Honestly, I struggle to understand how it is possible to conclude that every OCaml library is monomorphic.

What do you mean by LATs? The Haskell wiki doesn't seem to know that term.

> The only polymorphic code that you can write in ocaml without such things are functorial

I am sorry to ask but are you confusing bounded polymorphism with parametric polymorphism?


> they only matter with GADTs and local modules

Local modules (assuming this means e.g. a module passed in as a function argument, constrained to have one of its types match the type of another argument) are how you write non-trivial polymorphic code.

> Atomic ... CCPair

More functors (in haskell terminology) - code that doesn't do anything interesting with the type parameter.

> What do you mean by LAT

Locally abstract type

> I am sorry to ask but are you confusing bounded polymorphism with parametric polymorphism?

Bounded polymorphism is an application of parametric polymorphism. Not sure what you are asking here. Possibly this will clarify my line of thought: I consider parametrically polymorphic code without any bounds to be "uninteresting"/"trivial", because either the code must treat the polymorphic type completely opaquely (leading to trivial functorial [in the haskell sense] operations like implementing fmap), or requiring you to explicitly pass in all supported operations on the instantiated type (basically requiring you to implement bounding by hand).

I think this may be a blub paradox thing, where the set of useful applications of PP is much more restricted in ocaml, to the point where ocamlers do not even consider what they are missing. A haskell or rust programmer would likely run into these semantic blocks quickly upon trying ocaml.


Ok, you are using "parametric polymorphism" to mean bounded polymorphism. This explain my confusion! Indeed, I can understand your point of view then: bounded polymorphism is indeed better done with functors in OCaml. And since functor are syntactically heavy in OCaml there are not used everywhere and only when they are useful. Which doesn't mean that they are not used at all, as the Mirage project can attest. MirageOS is essentially an Operating System build upon OCaml functors.


I don't really consider those separate things. Bounded polymorphism is parametric polymorphism, plus a type subset relationship. A quick sanity check on wikipedia is consistent with this model. BP is PP plus what's required to make it more than a toy

I remember one annoyance I had with ocaml was the effective inability to use point-free style. I can't remember what limitation was behind this; do you know off the top of your head?

Edit: it's the "value restriction", more annoying bullshit I forgot about! Huge limitation for polymorphic ocaml code.


> BP is PP plus what's required to make it more than a toy

And in fact OCaml does have BP, as was explained earlier, just with a heavier syntax.

> effective inability to use point-free style

OK but point-free style is not an end in itself, it's just a means to an end (clean code), and there's certainly no universal agreement that it's the best means...explicit style is almost always easier to understand for a wide assortment of people and doesn't require mental backflips trying to figure out the weird heiroglyphics of '(.) (.) (.)' etc. etc.

> Huge limitation for polymorphic ocaml code.

Yeah I can see how writing e.g.

    let f x = g [] x
As opposed to

    let f = g []
Is such a deal-breaker. /s


> just with a heavier syntax.

Ok, I'm glad we agree on that.

> I can see how writing

Eta reduction is one example, but a great deal of code is cleanly expressed as e.g.

    vecLen = sum . map (^2)
And ocaml's type system is not capable of handling this gracefully. Very unfortunate!


?

    let vec_len vec = vec |> map (fun x -> x ** 2.) |> sum
Or even

    let square x = x ** 2.
    let vec_len vec = vec |> map square |> sum
Honestly, if code like this is the biggest nitpick, I have a hard time taking the argument seriously.


I spent a lot of time on Standard ML in university.

OCaml was always portrayed as the engineer's alternative for real applications.

I spent some time brushing up on OCaml a few years ago using Exercism.io.

While OCaml is a personal "top tier" language, I'd always prefer Haskell, Rust or Scala.

Type classes / traits just seem to beat a higher-order module system for me.


I learned SML/NJ and OCaml around the same time and for some reason I found SML more pleasant. I particularly liked the SML "Basis" library, it felt really well designed to me.

Still, I'd pick either over Scala. Superior compilation times, cleaner syntax, less complex. Haskell also just feels excessively clever.

I had hoped Rust would be "OCaml but for systems programming" and it sort of is, -- but the borrow checker and memory safety features, as neat as they are, add a lot of mental overhead.


SML definitely has a more tastefully designed syntax also, and not only because it's a smaller grammar. OCaml syntax is the "scuffed" version of SML syntax, as the kids say!

Unfortunately to be a real-world SML user these days is a very isolating proposition, because although there are some really nice implementations (MLton, PolyML), to a first approximation there are zero libraries.


SML is the dead end because specification wasn't updated for decades.


For me, the borrow checked removes a lot of mental overhead. It finds all sorts of subtle concurrency bugs in my code.


Depends on your starting point. More overhead than GC, most of the time, so much less than C/C++


I also find it hard to spot why OCaml is the natural "step up" for creating real world applications instead of Haskell, Rust, Clojure or even Kotlin, C++, Python and Go.


let's see:

    - OCaml vs Haskell: eager vs lazy  (=> memory consumption is more predictable)
    - OCaml vs Rust: OCaml has a GC (=> comfort) OCaml has tco (could not resist this ;) )
    - OCaml vs Clojure: vastly superior typing system. (=> less bugs)
    - OCaml vs Kotlin: no JVM needed.
    - OCaml vs C++: more safety. once it compiles it will not segv.
    - OCaml vs Go: vastly superior typing system. (=> less bugs)
    - OCaml vs Python: vastly superior type system, way better performance.
The biggest risk of doing OCaml (or Haskell or Rust) for extended periods of time is that you will be unable to hide your feeling of superiority towards (fe) a python developer.


I think the languages you selected in your final remark sum it up for me. If one is truly taken by functional programming, much of their mental model starts to revolve around algebraic (inductively defined) data types and structural recursion (aided by pattern matching) over them - such that mentally reducing the set of candidate languages really does become an implicit process of questioning: "does X have ergonomic, statically-typed, discriminated sums?".

Lots of mainstream languages simply fail this test and make it feel like intellectual poverty or that there's extreme, turgid, boilerplate required for a weak imitation of the features (see the idiomatic class-hierarchy encoding of ADTs in large projects - such as LLVM - in C++, for example).

It's absolutely no surprise that more mainstream languages are picking up a match-like construct and lighter encodings of discriminated sums. So, it really comes to what else you wish to be burdened with when compiling an OCaml-like mental model to X in your head: Tagged unions a-la C? Class hierarchies for ADTs in C++? The travesties of std::variant? Monad transformers in Haskell? Lazy evaluation? No static typing at all? Caring about memory management and ownership? Box and Arc-ing recursive components of ADTs? Writing your own arena allocator? Compiling to the JVM? Spotty TCO support?

OCaml is just a nice, fairly simple (at its core, at least), language that captures the essence of the ML family, compiles to native (and bytecode and, transitively, JavaScript), has great tooling (opam, dune, ocamllex, memhir, etc.), great libraries (official LLVM bindings, for example), and a great community. Lots of OCamlers are well aware of other potential languages that somewhat suit their style of programming, they just don't want to be burdened by the other stuff.


> vastly superior typing system. (=> less bugs)

I have no horse in this race, but claiming it to be "vastly superior" and implying "less bugs" makes it sound like this is a logical consequence, when you're actually staying on one side of an endless debate that to me doesn't have clear winners. For instance, from the little I've learned about Clojure, they claim the lack of a "vastly superior type system" is a feature, not a bug, and it's a result of a fundamental difference in some beliefs about how to write correct software.

From this I wonder how misrepresentative your other comparisons are as well. But don't get me wrong, OCaml is probably my "favorite language I've never actually used" (I've never "actually used" Clojure in "real projects" either).


Take any open source python project on github (or others) look at the list of issues and count the number of 'NoneType' has no attribute ... instances. All these could have been avoided by a decent type system. I rest my case ;)


I don't think I've ever seen anyone seriously argue against the claim that strong typing systems at least prevent many types of bugs. Now people may think that they are more productive in a language with weak types but that's a different consideration.


This was on HN the other day: https://github.com/hwayne/awesome-cold-showers#static-vs-dyn.... I would probably add a caveat to the listed caveats that testing might not be considered? Like if every dynamically typed code base implements an ad-hoc typechecker with a testing framework, it's a distinction without a difference.


I’ve passed wrong type of arguments to Python functions and assumed type of the return value wrong countless of times. That has never happened in Haskell. I don’t know how to reconcile my experience with the statement that there’s no evidence that strong typing reduces bugs.


I program primarily in (untyped) Python (which, I get is technically strongly typed but people don't think in technical terms) these days, and I almost never experience type errors. I guess I could attribute this to a couple things:

- I'm very specific about when I use None

- I'm a big fan of named function arguments

- I like to think my naming of things is pretty good, as are my conventions for parameters

- I try to handle all possible cases (what I mean here is I do and if I don't I made a mistake)

I use tests very sparingly in personal projects, but yet I haven't really felt their absence. If I ever write a piece of particularly hairy code (metaprogramming comes to mind... lord) I'll write a quick script testing some cases and then delete it.

Anyway, all that is to say I think part of dynamic programming is you build an immune system for this stuff.


Python is OK for throwaway code, but I've mostly seen it used for big systems that people (for whatever reason) didn't think were important enough to write in a real language. The people with that opinion don't share your degree of rigor.

I usually avoid bugs in python code by rewriting in bash (at 1-10% the size of the original python, since bash's error handling can be set to "always do the right thing" with "set euxo pipefail")

If bash is a bad fit for the rewrite, go usually works. I don't work on linear algebra software much these days. Python seems to be a good option for that.


Yeah I love Python for quick work and prototyping, and it's great for small web services and as a DSL for data analysis. I suppose I could also envision it as a backend language for a fleet of microservices too.

I think Python gets bailed out a lot because those things turn out to be a lot of programming these days. The people who have a beef w/ Python are those who've worked with it on large, old codebases. This is a tough job for any language though like, raise your hand if you've ever worked on a large, old codebase in C++ or Java that you liked.

Usually when people push microservices I'm quick with Conway's law, saying "this is a tech solution to an organizational problem that won't actually make a difference", and I think I'm right about that. But I think I've been ignoring that a really nice thing about microservices for engineers is you can keep using languages like Python on small codebases, and at least mentally and emotionally avoid the feeling of working on a huge monolith. That counts for something.


I’ve similarly developed my own habits. But why spend our effort into developing habits, when we can address the problems across people --- junior and senior --- automatically with a type system.


Oh 100% agree. The problem I've run into is my habits aren't others' habits, so we burn time arguing about how best to $CODE, and I rarely walk away from those satisfied or more educated (I'm sure this feeling is mutual haha). I wonder if "immune system" is to "type system" as "my special formatting" is to "use a formatter" here.


I think the big win is actually immutable data and pure functions. It just so happens that strong typing tends to come along with these things (e.g. Haskell, OCaml)


Clojure is the big outlier


And Erlang / Elixir. These are also some of the most widely used FP languages in delivering real world software.


OCaml vs All: Good luck finding developers that will write quality code and not cost a fortune each.


If Jane Street can teach OCaml to traders, I can teach it to developers. That's not a concern for anyone other than a sweatshop.


If Jane Street can teach OCaml to traders, I can teach it to developers.

You can if they are motivated to learn. Unless you are offering Jane Street levels of compensation; and in return, the candidate is willing to believe, or pretend to believe that company's shtick about OCaml being so categorically superior as a general-purpose development language so as to leave all the others in the dust -- most likely they won't be.


Or if, you know, they were hired to work on a project where they get paid a salary. That seems to incentivize most devs.


Not "a salary", but a Jane Street salary and resume cred. Otherwise you just won't find that many takers.


I can assure you I will and in fact have found plenty of takers for my job postings with a niche language. You just need to word the postings appropriately and be willing to teach people what they don't know.


You just need to word the postings appropriately and be willing to teach people what they don't know.

OK, I'll grant that if you have that "magic skill" then you can in fact recruit developers for niche languages.

Most companies don't, as we know, and frankly it's amazing to me how incoherent their communications are throughout their so-called hiring process.


If having sensible hiring practices is a 'magic skill', then I guess call me Harry Houdini!


Per this person's experience (which seems sadly far from atypical), you might as well be:

https://www.benjamistan.tech/2022/06/26/wasting-time-in-tech...


Actually, what I found more impressive is that they also got them to use emacs.


Good luck finding high quality cheap developers in any language really. Very few good developers are language specific so unless your HM is an idiot and actively screens out candidates without 10 years of previous experience in the specific framework you're using it won't be a limitation. If you want quality developers you need to offer either 1) lots of money, 2) amazing benefits, 3) interesting problems to work on. For many, an interesting language can provide an edge. I chose my current company because I would be working on Scala here as opposed to Java with the other offers I had. The quality of life improvement of the Scala position was enough to take it over the prospect of writing AbstractFactoryBeanImpl for the next few years.


> Good luck finding developers that will write quality code and not cost a fortune each.

You pick one regardless of language. Not sure what's your point here.


It's not getting things done that matters. It's feeling superior to others that matters.


Hey, it's not a feeling. It's a fact.


It is certainly a fact, that you feel superior.

I would like to confirm that fact, by comparing productive output..


Define "productive output".


Producing code, that primarily solves the real world problem (not aesthetic ones).


What kind of code? People who want to solve real-world problems treat programming languages like tools in their toolbox. In the past week at work I wrote Scala, HTML, JavaScript, YAML, and Terraform. I also remove tons of code from my production systems whenever I get the chance as it solves a real-world problem for me (maintainability of the code base).

'Producing code' to measure 'superiority' is about as useful a metric as counting lines of code to measure productivity.


Not sure what you mean by "- OCaml vs Kotlin: no JVM needed." as Kotlin has support for native and JavaScript compilation, and WASM via native.


Kotlin has a crude support for native, and they had to reboot the implementation as they got clever and went with a memory model incompatible with JVM and JS GCs, thus making code portability an headache.


Maybe it's changed in the year or so since I looked at kotlin, but my impression when I did look at it was support for anything other than the JVM was definitely second class


Did you even try it? It still requires Java, Gradle and Kotlin compiler to work. Also, good luck making it work without using Intellij.


so it's either a JVM or either no libraries?


What about ocaml vs f#?


What about Swift?


On the server, less ecosystem than OCaml. Long compile times. A big part of its "market share" is taken by Rust instead.


For now, a good language if you want to develop apps for the mac ecosystem.


As a former OCaml hobbyist programmer my take on these kind of books or articles is that, yes OCaml is extremely elegant and beautiful as a programming language and it shines for simple applications. Some parts of it are actually not so elegant, for example the object oriented aspect completely spoil the elegance of the core language.

On the other side OCaml, as a pure functional programming language with immutable value by default doesn't scale well to large, complex application. Just the paradigm is no longer tenable and you need to switch at least partially to imperative programming with mutable variable. For example this is what it does the implementation of the OCaml itself.

To develop further the point about "what doesn't scale" there is also the function with unnamed arguments and currying. While extremely elegant for simple programs it gets confusing for real-world applications when function needs quite a lot of arguments and there is no longer any obvious order to give them. If you stick with that and you choose an order it becomes arbitrary, difficult to remember and currying no longer makes a lot of sense.

Functional programming with immutable values is a wrong pattern for programming languages. Many algorithms, almost all actually, are naturally expressed in imperative style with mutable arrays or variables.

What is needed is to bridge the good things from OCaml, the type system, the pattern matching with tagged types into a modern, imperative programming languages.

Rust is a sort of answer but they got it wrong because it is too low level about managing the memory, the ownership pardon, and everything else so programmers cannot just express the algorithm or the logic they want to implement but they have to spend a lot of mental energy thinking about ownership issues and unneeded accidental complexity like lifetime annotations.


There are two ways to improve performance of a given program. You either go lower level, giving more control over the execution and specifying what you want exactly, or you go higher, not constraining the execution as much, allowing for better optimizations.

For example a manual for loop will almost always be harder to optimize than a map. The latter gives explicit permission for reordering, allowing for vectorization and parallelization automatically.

Also, I would think twice before claiming FP non-ideal for PLs - modern CPUs employ just as much FPism as they are considered imperative. OOE doesn’t sound too imperative to me. And at the end of the way, imperative steps are just sequential state changes from a different perspective.


> modern CPUs employ just as much FPism

Do you have some references where I could learn more about that?


What I mean mostly are out-of-order-execution (reorder these instructions and return results as if they were executed in order - it is worthwhile because useful work can be done “in the background” while the CPU waits for memory. But this transformation is not too imperative in my opinion) and SIMD instructions (and in extension GPUs) are much more about transformations on data than a traditional Turing-machine - but there are no sharp boundaries anywhere here. It is just not smart to dismiss such a big and important part of CS, when it has plenty of applications.


> To develop further the point about "what doesn't scale" there is also the function with unnamed arguments and currying.

Not sure what you mean by unnamed arguments, but criticizing automatic currying is totally valid.

> Functional programming with immutable values is a wrong pattern for programming languages. Many algorithms, almost all actually, are naturally expressed in imperative style with mutable arrays or variables.

This is a huge jump. Any imperative solution can just be expressed as a fold of some sort and many algorithms, esp. those that use stacks, are easily expressed recursively. Which one is more "natural" is 100% subjective, but I'm in the declarative is easier to reason about than imperative camp. Moreover, the idea that "unnatural" algorithm expression implies "doesn't scale" needs much more elaboration.

> What is needed is to bridge the good things from OCaml, the type system, the pattern matching with tagged types into a modern, imperative programming languages.

> Rust is a sort of answer but they got it wrong because it is too low level about managing the memory, the ownership pardon, and everything else so programmers cannot just express the algorithm or the logic they want to implement but they have to spend a lot of mental energy thinking about ownership issues and unneeded accidental complexity like lifetime annotations.

It's not "wrong" just not what you want, and honestly the ownership model isn't that bad. The overhead amortizes somewhat as you get used to it. I of course agree there is room for more languages with ML-like type systems though!



> Functional programming with immutable values is a wrong pattern for programming languages. Many algorithms, almost all actually, are naturally expressed in imperative style with mutable arrays or variables.

The optimal implementation might be imperative but that doesn't mean we need to define our code that way. SQL is a good example here.


> The optimal implementation might be imperative

There are times when it isn't.

I'm thinking of Richard Bird's functional pearl, The Smallest Free Number, where the divide-and-conquer algorithm is faster than the imperative one.

From the conclusion:

One of the differences between a pure functional algorithm designer and a procedural one is that the former does not assume the existence of arrays with a constant-time update operation, at least not without a certain amount of plumbing. For a pure functional programmer, an update operation takes logarithmic time in the size of the array.1 That explains why there sometimes seems to be a logarithmic gap between the best functional and procedural solutions to a problem. But sometimes, as here, the gap vanishes on a closer inspection.

The ability to arrive at the divide-and-conquer algorithm is quite fascinating as it uses plain, boring old mathematics and is, for some, quite straight-forward to derive on one's own.

Yet people are more convinced by imperative implementations. I'm curious why this is. Is it because we "teach" people to believe programs are executed sequentially and are therefore somehow "inherently" imperative? Or is it easier to reason about algorithms in terms of their operational semantics?


I tend to believe that there is an issue of map/territory confusion in teaching ‘programming’ and in general thinking about programming. Programs are usually envisioned as being statements which are performed in a sequence one after the other. This is comparable to the nature intuition about what an algorithm is, i.e. a series of steps to achieve a result when given some input.

The problem then lies in the deeper explanation. It is said that the hardware is just taking instructions and executing them one after another, and that is then mapped to the execution of programming language statements. The issue of ‘imperative’ vs ‘non-imperative’ implementations is really a question of granularity. The map between machine instructions and language constructs is so large (for most every modern machine) that while the imperative algorithm seems more reflective of the underlying architecture it is just a layer of cover up to make programmers feel better.

I really think that the appearance of control over fine grained instructional behavior give people the feeling that their imperative code is truer/more-correct in relation to the machine. This feeling leads to the presumption that imperative is more efficient.

As a final caveat, the ability for a non-imperative algorithm to be equivalent to its imperative alternative tends to rely on the language’s compiler (and the restrictions inherent in the non-imperative language). This is the origin of the ‘with-a-smart-enough-compiler’ argument that people sometimes level against non-imperative programming, i.e. there is not a compiler smart enough to take your high level language and produce the same machine code I do in my low level language.

So I’m sum, I think it is both a teaching error (the continual re-enforcing of confusion between what a program says vs what a machine does) and a general human level confusion as to what an algorithm is at the level of silicon in 2022.


Show me a non-imperative, in-place, efficient implementation of textbook quicksort and we'll talk.


Here's a direct translation of a C++ implementation into Haskell with no optimization: https://koerbitz.me/posts/Efficient-Quicksort-in-Haskell.htm...

Personally I think the Haskell code is a bit nicer. Mutation is inherently unsafe and you need to be more careful when performing such computations. Haskell code makes this painful to read by planting red-flag words like, "unsafe" around.

In C++ mutation is the norm and so it's much easier skip past such code. No indication that these operations are unsafe. The experienced reader will scrutinize this carefully or gloss over it at their own peril. The code is terse on the subject as the syntax maximizes efficiency for this case.

Here's another: https://stackoverflow.com/a/5269180

If you absolutely need in-place mutation, it's available. It's just not the default.


Yes, but throwaway17_17 was making a philosophical point that imperative algorithms are somehow unnecessary, an out-of-date style of teaching, or merely a compiler detail, instead of being inherent to some algorithms.

But I don't think there's a "non-imperative" quicksort algorithm. There's definitely value in preferring functional, non-mutating implementations over imperative where they make sense, but I've seen too many people pretty much claim that functional style of programming is all you need, and that's pretty much a lie IMHO.


> Yet people are more convinced by imperative implementations

I like and use functional and immutable but saying that's best is like saying a screwdriver is better. Sometimes imperative is cleaner. In a video, the creator of Scala, M. Odersky, said Scala allows mutability because sometimes it is cleaner.

As an SQL guy, I remember one time a cursor solution was cleanest and best.

Scala's librares have some string stuff which looks immutable but isn't, under the hood.

As ever, it depends.

> Or is it easier to reason about algorithms in terms of their operational semantics?

Good question. I'd say people think in terms of actions not mathematical functions. I certainly do. Imagine teaching an 8-year old. And it does map so well onto the underlying reality of fundamentally mutable hardware.


> And it does map so well onto the underlying reality of fundamentally mutable hardware.

It maps well to the abstraction provided by the hardware. In reality we know that modern architectures are executing instructions out-of-order for efficiencies' sake and that data-accesses are not necessarily sequential either. And we haven't even considered multiple-core processors that are so common these days!

As Bird points out in his book, when one looks close enough there are cases where the logarithmic gap between the imperative vs. functional approach disappears.

And further, with register targeting and stuff it's not often true that recursive calls are less efficient than their imperative counterparts anymore... although I suppose historically this could be why, at least for small arrays, imperative programmers could assume constant-time indexing and updating.

I'm not nearly as versed in functional programming as I'd like to be. I was raised on C and algorithms were always described in terms of procedures to me where the proofs took quite a bit of work to follow. However as I learn more about algorithm design in functional programming and functional data structures I can't help but notice that the proofs are much easier to follow while sometimes the solution seems quite alien and I wonder if that's because of my heritage of thinking operationally rather than by calculation. I've often been surprised to learn that many of my assumptions about the logarithmic complexity of functional algorithms are not always there!


Ideally I would write my code in a declarative way and the compiler would figure it out. Second to that, the language can allow interior mutability so I can optimize.


Btw you can actually write loops and mutate things with the ref keyword. It’s usually much clearer than writing a loop recursively but not everyone uses it.

The impossibility to return early in a function does create really convoluted code though. I’m wondering if there’s a solution to that in FL


I generally agree, and I'm hoping that Gleam fits the bill for "Rust with garbage collector" (also, when I say this, people leap to OCaml, but OCaml introduces a bunch of problems which aren't present in Rust: cryptic syntax, lower quality build tooling, unbounded type inference, competing "standard" libraries, a fairly toxic community, etc).


> OCaml introduces a bunch of problems which aren't present in Rust: cryptic syntax ...

Hmm, I think "cryptic syntax" is probably in the eye of the beholder


I'm sure there's some element of subjectivity, but at a minimum there's something to be said for "unfamiliar to the overwhelming majority of programmers". I think it's also very likely that OCaml's incredibly terse syntax (and terse naming conventions) is objectively difficult to understand from a "how the human visual/symbolic processing pipeline works" perspective, but I don't have the data to back that up (I also don't think OCaml is alone in this regard).


Thing is, if OCaml is terse and weird syntactically (and I would agree), then so is Rust. Especially once you have to spell out signatures.


Rust’s syntax is based on C and C++. Yes, it introduces additional concepts and gives them new syntax, but function calls, parameters, struct definitions, struct initializers, function definitions, etc are all very familiar to most programmers.


How does gleam handle thread safety and asynchrony? Rust leans heavily on the borrow checker for that.

The Rust with GC proposals I've looked at all break compile time thread safety checking.


Not sure. Frankly I’ve never got any value out of compile time thread safety checking. Writing correct shared memory parallel code is rare and easily managed if you have some experience (minimize and lock mutable shared state).


Concerning the issue with function arguments, this is one of the reason why OCaml has labelled arguments. And for instance, Janestreet's idiom udes labelled arguments as often as possible.

Similarly, I am not sure what is the issue with using imperative OCaml for imperative algorithms when they are a better fit for the problem at hand?


> And for instance, Janestreet's idiom udes labelled arguments as often as possible.

So you have to give up to one of pillars of the functional programming paradigm and you get a less elegant but more practical programming language. Otherwise I agree that using labeled arguments is mostly fine and doesn't completely spoil the language.

The more serious compromise to the functional programming paradigm is the fact that you need to use mutable variables and imperative style programming. Once you do this you lose most of the elegance and attractiveness of functional programming.

> Similarly, I am not sure what is the issue with using imperative OCaml for imperative algorithms when they are a better fit for the problem at hand?

What I mean is that for any moderately complex application you need to switch to imperative style so the appeal of OCaml is mostly lost. You better choose a programming language that is designed for imperative programming since the beginning.

The arguments I am giving explains why there are practically no real world applications done in OCaml. Some people insist using OCaml because they love the elegance of the language and I understand them but reality is it doesn't scale to complex applications.

For people in Janestreet I think this is a sort of niche where they get an added value from OCaml thanks to its superior typing system and compile-time detection of many errors. I guess they care really a lot about the business logic of their applications and OCaml shines to ensure it is correct so for them the advantages out-weights the inconveniences.


I am not sure which pillar of functional programming is lost with labelled arguments? Partial applications still work, higher-order function too. One might need to use anonymous functions when labels does not match but I don't see any pillar being lost.

In the same way, it is perfectly possible to switch to the imperative style only in the specific code path where performance really matters and use abstraction to isolate this performance-sensitive part from the rest of your application. Ideally, you can then keep both the elegance of functional programming and the performance of imperative programming.


I’m with octaron on this one. No disrespect, but a bit of hand waving and big claims.

I’m kindly invoking Hitchens razor, here


> What is needed is to bridge the good things from OCaml, the type system, the pattern matching with tagged types into a modern, imperative programming languages.

Like https://rescript-lang.org/?


> Functional programming with immutable values is a wrong pattern for programming languages.

Let me refer you to the most successful programming language of all time, by far–Microsoft Excel formula language. It's:

- Functional (data transforms are purely function applications, there's even a LAMBDA function now)

- Immutable (functions in the system can't change values, only consume incoming values and output outgoing values).


> Many algorithms, almost all actually, are naturally expressed in imperative style with mutable arrays or variables.

https://www.goodreads.com/en/book/show/594288.Purely_Functio...


Have you read the book and implemented its algorithms?


Ironically, I find the OOP part of OCaml to be the most interesting, seeing how it manages to provide a self-consistent and powerful structurally typed object model that is no less powerful than what you get in C++, for example.


Cornell alum here who took CS 3110. This course made me a better programmer, made me gain a sense of respect for OCaml and functional languages in general, and made me fall in love with CS. The textbook and professor are both phenomenal.


My introduction to programming course in my university was in OCaml. It was all downhill from there when it comes to the programming languages I had to use.


Which university? Does the course have a public webpage?


Not the original commenter, but my first year course of Introduction of Programming 15 years age was also using OCaml. There were two groups of students, standard one was using Pascal, and the functional one using OCaml. I was studying on Warsaw University and the lecture notes are in Polish: https://mimuw.edu.pl/~kubica/wpf/wpf.pdf


That was exactly the course that I took at the WU :) Fond memories.


Thanks! 15 years, that's quite a long time ago.


Yup. I really loved all the functional stuff I've learned there from OCaml to SML+Extended ML, to Haskell.

Unfortunately, I haven't had a chance to professionally program functionally since the n. :(

It was all Java and C++. Though I like C++, maybe it's a Stockholm syndrome. ;)


Cambridge does this (or did when I was there); https://www.cl.cam.ac.uk/~lp15/MLbook/ is the book for the course (available online).


It still does according to the undergraduate program page https://www.undergraduate.study.cam.ac.uk/courses/computer-s...


Seems its better to start with javascript so you can have the reverse experience.


Then I would probably be writing HN posts claiming that languages with advanced type systems and functional programming are too hard and impractical, and that they are trying to be clever and cool for no reason.

But, partly thanks to that course, I'm able to pick up any paradigm, and my opinion on what's better is informed by knowledge of both things. I'm also able to structure imperative code better than my "imperative only" colleagues.


I took this class last semester. Michael Clarkson is an awesome professor. After learning OCaml I want pattern matching in JavaScript! Beyond the language itself, it absolutely made me a better programmer.


Pattern matching without ADTs though loses a lot of the power. The fact that you'd still have to handle null/undefined cases means it's still annoying to use and hardly exhaustive.


erlang/elixir programmers would disagree -- we match on ad-hoc dynamically typed tuples that serve broadly the same purpose as ADTs all the time, where the only exhaustiveness you get is the degenerate case ie `_ -> shit_the_bed()`

especially common are pattern-matching assignments (technically `=` is the “match operator”) that idiomatically behave much like matches on the left hand side of `<-` in haskell do notation, i.e. inline runtime assertions on structure

these communicate programmer intent really well! you could probably have them in static land in a non-`monadFail` context too, but you'd want dependent types or something lest you go the way of typescript and resign yourself to unsoundness

in fact it's so nice that i feel pain in any dynamic language without full-fat pattern matching, like when i have to write disgusting if-else chains in complex nix expressions


I write Elixir for work and I love most things about the language.

The pattern-matching would still be made better with static checks. It sucks to have a function blow up at runtime because of something as trivial as args being swapped or some code somewhere changing its return types. Someone brought up Gleam earlier, but god why with that syntax?

I'd still rather have dynamic checked pattern matching over the alternative.


dialyzer and a liberal sprinkling of typespecs gets a decent chunk of the way there, meanwhile the problem with gleam and friends is the fact that they're altogether new languages -- the pragmatic solution would be a restricted subset of erlang (that a likewise restricted subset of elixir could comfortably compile down to) where your module has to be fully typed, but you would need some pretty gnarly logics to handle things like "yes, when this function mashes these two iolists together in this way, it's still an iolist" etc


I'm hoping that https://github.com/josefs/Gradualizer and its Elixir counterpart get us closer to what "I" want. I find dialyzer often inscrutable compared to something like OCaml's or Haskell's type errors.

I do still use it and typespecs, because it's better than no checking.


very grateful that this course is free for all, and the youtube lectures are neat too

real world ocaml 2e is nice, but like a lot of oreilly books about $LANGUAGE lately it's a lot of thinly-veiled $COMPANY opinions on $LANGUAGE best practices, where $COMPANY is, in this case, jane street. this is great if your motivation for learning ocaml is applying for a job at jane street

if you think ocaml seems cool because wow jane street does epic hft in ocaml, then read real world ocaml 2e

if you think ocaml seems cool because wow they wrote coq/fstar/the early rust compiler in ocaml, then read cs3110


I actually find these thinly veiled company best practices fascinating.

I find I can learn a lot of life lessons by reading a condensed account of a person’s entire life in their (auto)biography.

For the same reason, I would hope to learn a lot of great real life patterns from a book called Real World OCaml… a kind of condensed (auto)biography of an institution’s experience with a technology.


sure, but if it's the reader's first introduction to the language (which rwo is positioned as), the reader is in a uniquely bad position to discern justified best practices from arbitrary ones, and truths about the language from opinions about it

learning the language first, then someone's ideas about how it should be written (in this case, as encoded in a replacement standard library with a special module dedicated to making any call to the actual standard library throw a compile error), gives you the requisite context to understand the latter


Every time I use recent "functional" languages (Rust, modern Typescript) I realise how great OCaml is


Have you tried F#?


Nope, I must say I have only used a small subset of languages from the FP-language-zoo, so my opinion on OCaml might be biased

But as an example when writing Typescript, I feel so frustrated of not having a clean way of doing pattern-matching


You will have to wait for JavaScript to add it first.

The whole point of Typescript and why it is so successful, is because they only add type system on top of JavaScript.

Pattern matching would introduce new language constructs beyond what is required for defining types.


I think as long as they maintained it as a proper superset of javascript (i.e. all js code was valid ts code), adding a new language construct wouldn't hurt them much. see kotlin vs Java for a similar case


Except that isn't the design approach from TypeScript, all TS code without type annotations should be valid JS code.

As for your Kotlin example, Dart's failure shown why it isn't a good approach.

Kotlin also shows what happens when the underlying platform decides to go in another direction (value types, loom, default methods on interfaces), and the amount of boilerplate that needs to be generated to pretend to be like the host language.

A language that only matters, thanks to the way Google is pushing it on Android to replace Java for anything besides system libraries.


Typescript gets compiled to Javascript anyway. What's stopping any pattern matching construct from being compiled to vanilla JS?

We already get stuff like type narrowing and generics.


The philosophy of not adding language constructs beyond what is required for the type system.

Typescript code without type annotations should be JavaScript compatible, minus features still in flight for standardisation.

There are other languages for that like ReasonML.


Why wait? Just use ReScript. It's OCaml but adapted to fit natively in the JavaScript world.


I love OCaml. Fast builds, native binaries, fairly expressive and elegant once you’re accustomed to it. Now that it is multi-core, I hope it gains traction.


That is a great format for an online book! The text looks like it could be read on its own for a quick review, and the hundreds of short embedded videos dig into detail.


I really want to like ML but I have some complaints about OCaml in particular:

The stdlib is very weak so everyone uses this third party library. That really rubs me the wrong way.

All structures share a namespace for their elements which is super wacky.


> The stdlib is very weak so everyone uses this third party library. That really rubs me the wrong way

So don't use the third-party library then? There's a lot of functionality in the default Stdlib, use what you can from there and find third-party libraries for the rest...

> All structures share a namespace for their elements which is super wacky.

Not sure exactly what you mean here, perhaps that all modules in a single compilation unit are globally namespaced. Simple enough to solve nowadays especially with everyone adopting a common dune-led convention of namespacing by the library name.


I learned some Haskell years ago but it didn't stick with me. Lately, I want to get back into functional programming to see whether I'm missing out on something. This book looks nice, and so does OCaml. Any suggestions on books, courses, or resources on this subject?

Also, just to get proper motivation, why should I learn functional programming as a capable Software Engineer?


You should learn multiple paradigms to broaden your horizon, so you are able to approach a problem from multiple angles and decide on the best design approach.

I don't write in a functional language in my day job, but I'm much more aware of global state and side-effects in my code than before. We mainly write in Python and C#, but I encourage my team members to dabble in SQL (for dataset manipulation) and Haskell (for functional purity). If anything, it makes them understand LINQ better.


Dan Grossman's Programming Languages is a great 3 language online course. It gives you the opportunity to compare standard ML, ruby and racket.

Before that course I was already grouping languages by inheritances from C or Lisp families, but really started to recognize and appreciate ML more.


Berkeley's Programming Languages and Compilers references this book and you get to apply OCaml to something non-trivial.

https://inst.eecs.berkeley.edu/~cs164/fa21/


It doesn't appear from your link that the course materials are available online though.


On the schedule page, there are links to further material. I don't think you can do the drills or exams, but you _can_ do the homework if you have a github account.


Another thing I recommend everyone to learn at least once is sum types aka algebraic data types. And how they are the dual to class based dispatch (what some call the "expression problem"). Sum types aren't exclusively found in functional languages, but they are certainly prominent in them.


For Ocaml in particular, one thing that is pretty cool about it is the module system. Although we unfortunately can't use this module system in other languages, it taught me some interesting lessons about abatract data types.


The OP is a great book to learn OCaml. There is also a corresponding set of lecture videos on YouTube.


You can strengthen the skill of thinking about your programs in terms of higher level abstractions, which allows you to have an overview and reason about larger systems.


IMHO the main reason to learn functional programming is to learn to untangle state and behaviour.


This is a very specific question but I thought I'd ask it here on the chance someone might have a good answer.

I've been slowly working my way through this course and I ran into an issue recently when I upgraded my OCaml installation to the latest version (4.13.1). With this version I find that the simple instructions for building an executable with dune (especially one that uses OUnit) given here https://cs3110.github.io/textbook/chapters/data/ounit.html no longer work.

It seems dune now requires you to initialize a project. Does anyone know how to translate the instructions in the course to the current version of dune ?

I realize I could install an opam switch for the previous version but I'd rather be working with the latest one.

Just hoping someone might be able to save me an hour or so figuring this out on my own.


You can add a dune-project file at the root of your project with just `(lang dune 3.2)` (or your dune version rather than 3.2). The file can also be generated by "dune init project project_name" for fresh projects.


Thanks. It looks like all I needed was the project file. I was confused because when I followed the instructions to run dune init it created an entire directory structure and then I was confused about where my code should go (I haven't gotten to the chapter on modules yet).


FYI: If you have more questions, the OCaml forum is very friendly and helpful: https://discuss.ocaml.org/


The latest version is 4.14.0. Personally I write my Makefile myself and use them to build my projects. It works perfectly.


"beautiful"

But then it has quirks like using ;; to end statements (EDIT: only in the REPL). And comments with that weird syntax

But worse of all are the optional parenthesis in function calls. Yes, I know Ruby has it. But it feel super weird and a needless flexibility (that causes more confusion than it solves).

I can't get over this stuff, sorry.


I have to interject here for the sake of those unfamiliar with OCaml and who may take the parent comment at face value.

Saying "it has quirks like using ;; to end statements" is misleading to the point of just being bogus. The double semi-colon is only ever used in the REPL. In fact, I've been programming OCaml for fun and professionally for over 15 years, and I've never used a double semi-colon in my code, nor have I ever encountered one in the "wild".


Thanks for clarifying


> worst of all are the optional parenthesis in function calls

??

    > let r = some_function x y z in ...
There are no parentheses here, and it's not optional. What you probably are missing is that if `some_function` takes 3 arguments, then `some_function x y` is a function value that takes 1 parameter (currying).

Btw, iirc, SML doesn't have this: there you only have 1 parameter but it could be a tuple


> Btw, iirc, SML doesn't have this: there you only have 1 parameter but it could be a tuple

Nah, SML does also support automatically curried function definitions, it's just that by convention that feature didn't get used as often. I tried to find out why that was the case a while ago, and stumbled on this explanation: https://www.reddit.com/r/ProgrammingLanguages/comments/jde9x...


> Note how OCaml is flexible about whether you write the parentheses or not, and whether you write whitespace or not.

from here https://cs3110.github.io/textbook/chapters/basics/toplevel.h...


isn't that the case in most programming languages ? In essence, if you have an expression exp then (exp) is also an expression. So nothing special about OCaml in that regard. [note, just checked python, php, and js and it is the case]


Ok so maybe the example is confusing, because it seemed it was talking about function calls (where they are mandatory in the languages you mentioned)


Another day, another surprisingly large discussion around OCaml whenever it comes up on hn.


I did a lot of my PhD work in OCaml. I hated pretty much every minute of it compared to C++, when comparing the exact same algorithms aha.


Oh wow, interesting. Could you elaborate why?


How do I know that some code is beautiful? And how will one know that it will always stay beautiful? Correct and efficient is one thing. Beautiful? This is off topic but topics like this grinds my gears as it’s purely subjective.


Forgive this tangential question, but does anyone know of a practically-oriented tutorial for implementing an ML-style language? Any format is fine, as are books.


There's "Modern Compiler Implementation in ML" which has a section on making the example language Tiger a pure functional language. I don't have "Compiling with Continuations" but that might be even more appropriate.


Brilliant! Much appreciated, thank you.

Also, I love your handle :P

P.S.: are you referring to the paper entitled “the essence of compiling with continuations”, or something else?


The book Compiling with continuations by Andrew Appel. Description says “Continuations can be used to compile most programming languages. The method is illustrated in a compiler for the programming language Standard ML.”


This book's setup instructions are super easy to follow than OCaml's own site!!


I took this class!


[flagged]


I'm so glad you took time out of your day to come here and tell us this.


> It has no performance or coding benefit for teams larger than a few.

That must be why Jane Street uses it then.


And pretty much only them.

Perhaps some day we'll learn how little the programming language matters for anything beyond coding.


Ever used AWS EC2? Docker Desktop? OCaml code is driving some of the lower-level performance-sensitive parts. That's a gajillion instances running globally.


Good to know, thanks.


When you look for job in OCaml, you can find several fintech and blockchain related companies.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: