Hacker News new | past | comments | ask | show | jobs | submit login
OCaml: a Rust developer's first impressions (pthorpe92.github.io)
173 points by qsantos 10 months ago | hide | past | favorite | 152 comments



> Where are the types?

OCaml supports type annotations practically anywhere, if you want to add them. Alternatively, you can use an editor that supports querying or showing types for bindings. VSCode and Emacs support this.

> Remember recursion? How about linked lists?

A lot of beginner material uses List to introduce algebraic data types, inductive and equational reasoning - but you don't have to use them. In fact, the standard library encourages Seq instead over List.

It's a shame the author did not get past the basics, as there are many interesting comparisons to be made. For example, OCaml 5 effect handlers are a very interesting alternative to Rust's Async.


> A lot of beginner material uses List to introduce algebraic data types, inductive and equational reasoning - but you don't have to use them. In fact, the standard library encourages Seq instead over List.

I have repeatedly found this the hardest part of learning OCaml - it seems really difficult to find a tutorial that explains how to write OCaml as a real developer would working with production OCaml. Like, I don't need another explanation of sum types or recursion, these are things I'm already using regularly. I'm more interested in the things that OCaml does uniquely that I won't have seen in other languages.

For example, which sequence type should I be using most often? And how often should I be using refs/mutability? And there's lots of alternate stdlibs - should I be dipping into those? What does unit testing typically look like for an OCaml project (and what about other forms of testing)?

It's also very difficult to search for these sorts of things, because I feel like a lot of the results I get are for students learning OCaml as an introduction to functional programming, so questions like "how do I reverse a list" are answered by showing a clever recursive function that reverses a list, as opposed to the List.rev function.

I am not the author, but I'm in a similar situation (right down the Rust background and trying out OCaml for AoC), and I'd love to get past the basics, but it's very difficult trying to find worthwhile resources to do that with.


I would personally recommend trying out F#. :) Answers below are for F#.

> For example, which sequence type should I be using most often?

You should basically default to list. If you need assignment and indexing or a 2D structure, use array. If you're writing generic processing code, use sequences if you can. Use sequence if you need lazy sequences.

> And how often should I be using refs/mutability?

Only when you need. I most often use mutability when interacting with unmanaged code, translating some Python or other imperative code before transitioning it to be more functional, and inside classes to manage internal state. Otherwise, I don't.

> And there's lots of alternate stdlibs - should I be dipping into those?

I'm not familiar enough with the OCaml ecosystem, but F# doesn't have this problem. F# has its own core library and for everything else, you just reach out to .NET.

> What does unit testing typically look like for an OCaml project (and what about other forms of testing)?

I personally use FsUnit, specifically the XUnit flavor. https://fsprojects.github.io/FsUnit/xUnit.html. FsUnit is an F# DSL over NUnit and XUnit, so it fits in well with F# and functional thinking but also plugs into the IDEs and `dotnet test`.


Cool to see I'm not the only one in this situation...

I want to like the language very much, so that is helping. That CIS310something or other course that is on Youtube is pretty cool, although a little slow for me (this is usually my issue with tutorials) but I did find the book online and that is great. I definitely hear you on the search results thing...

I started declaring all my types manually just because that's what I'm used to but switched back just because I feel like matching the 'standard'. I will definitely admit to a dodgy AOC solution (hard-coding with magic values).. but overall I'm enjoying it quite a bit.


Some of your questions might be answered in this book (free online version): https://dev.realworldocaml.org/


> In fact, the standard library encourages Seq instead over List.

Uhu? Never heard about Seq before! Seems to have been introduced in 4.07 which was released 5 years ago, which is basically yesterday in terms of stdlib api. Set and Map, sure, but Seq? Lists are ubiquitous and they are indeed a central data structure in ocaml code. For anybody also wondering, Seq is a datatype for iterators (ie thunked lists).


The Seq module has more functions than the List module. There are also more of_seq functions than of_list throughout. 5 years ago the compiler standard library was not a serious proposition, it is becoming more so now.


> the compiler standard library was not a serious proposition, it is becoming more so now.

This is actually one of my grief with OCaml. I wish I didn't have to use Core. I don't look much at the std library anymore.

My other grief is async (Lwt or Async). It forces to use monads which don't look very pretty in OCaml. That being said, I haven't looked at what OCaml 5.0 bring to the table on that front. This was my experience 5 years ago basically...


For 5.0+ you might want to look at https://github.com/ocaml-multicore/eio for how effects can make async much more pleasant


There will also be DynArrays in the stdlib from 5.2: https://github.com/ocaml/ocaml/pull/11882


Author here: Totally hear you there, the post was literally just like it says 'first impressions'. Will most definitely be posting a few updates as I progress farther and try to make some comparisons :)


Glad to hear there is more to come! I do think OCaml 5 not only keeps the language relevant but also makes it even more interesting.


Right but in practice most OCaml code does not have explicit types.

Using an editor that shows type inlays (e.g. VSCode) helps a lot but not fully because a lot of types are inferred as generics.

You're also correct that you don't have to use lists, but again most OCaml code does.


> Right but in practice most OCaml code does not have explicit types.

Most OCaml code use .mli which does have types. This is the recommended practice but I noticed a lot of beginners coming from other languages are reluctant to use them (because of duplication). It's a simple idea but I think it's a strong point of OCaml and it makes easier to program "in the large" than Haskell for instance.


But .mli files do not help with the "no types in the source code" problém. You need LSP (Merlin) to "see" the types anyway. Only adding the signature in the samé (.ml) file helps against that problém.

And I did not experience any advantage of separate signature files so far, but I also haven't written anything large in OCaml (>100 kLOC).


> But .mli files do not help with the "no types in the source code" problém

It partially helps since it forces you to have types where they matters most: exported functions

> And I did not experience any advantage of separate signature files so far,

100kLoc is already quite big! I'm starting to think I'm an outlier since a lot of people don't see the benefits :)

For me, it helps because I really don't want to see the implementation when I use an API. If I need to look at the implementation, it means the interface isn't well specified. All I need should be in the interface: types, docs, (abstract) types. And no more.

Typically, an .ml file will have more than what is exported, types won't be abstract but will have a concrete implementation, and type signatures may be missing. How would it feels like to use list if only https://github.com/ocaml/ocaml/blob/trunk/stdlib/list.ml was available, instead of https://github.com/ocaml/ocaml/blob/trunk/stdlib/list.mli? and that's not the most compelling example since it's a simple module where the main type isn't abstract. Generally speaking, giving up mli means giving up on encapsulation which is one of the primary tools to deal with large programs.

Haskell forces you to say what is exported from a module, but the module user can only see the names. To see the signatures, you need to rely on generated doc or look it up on the code.

Arguably, since OCaml has includes, it suffers from the same problem, your ".mli" may have tons of include and it becomes harder to see what's exported without an external tool


> It partially helps since it forces you to have types where they matters most: exported functions

But the problém the OP has is not knowing the types when reading the source (in the .ml file).

> How would it feels like to use list if only https://github.com/ocaml/ocaml/blob/trunk/stdlib/list.ml was available,

If the signature where in the source file (which you can do in OCaml too), there would be no problem - which is what all the other (for some definition of "other") languages except C and C++ (even Fortran) do.

No, really, I can't see a single advantage of separate .mli files at all. The real problém is that the documentation is often worse too, as the .mli is autogenerated and documented afterwards - and now changes made later in the sources need to be documented in the mli too, so anything that doesn't change the type often gets lost. The same happens in C and C++ with header files.


How do you do encapsulation without an mli? can you hide the type implementation to the module user? can you ensure the exported functions (which are all of them without an mli) maintain your type invariant?

> the real problém is that the documentation is often worse too, as the .mli is autogenerated and documented afterwards

Of course, that would defeat the purpose of having interfaces and is incredibly bad practice. People do that when they try to fix an already broken codebase. It's actually pretty ironic that people who care about type safety (which I is why they use OCaml in the first place) don't value (or understand) things like encapsulation.


I find Rust is really effective in this regard. From my understanding of the capabilities of mli files, Rust's visibility modifiers are even more powerful in terms of effectively hiding implementation details and maintaining invariants, but they're all written inline in the source code (with the `pub` keyword).

I agree that the encapsulation capabilities of mli files are great, but I don't see why they need to be written in a separate file (apart from the obvious reason: that's how OCaml does it, and you can't just change a language's design just because you don't like one bit of it!)


> How do you do encapsulation without an mli?

In OCaml you can't. If I could I wouldn't care about interface files, because I wouldn't need them.


> You're also correct that you don't have to use lists, but again most OCaml code does.

If the list is not being used in a critical section, what's the problem? The implication here is that OCaml is slow or inefficient. OCaml is often orders of magnitude faster than other high-level languages (e.g. Python). Arrays in OCaml can result in some very fast code.


> Using an editor that shows type inlays (e.g. VSCode) helps a lot but not fully because a lot of types are inferred as generics.

That is because they are generic most of the time.

This is also fun when using `#trace`in the REPL and all you see is `<poly>` instead of the "real" value


Right but being overly generic makes them hard to understand. Generics are kind of like "compile time dynamic types" and they have many of the downsides of runtime dynamic types:

* intent of the author not clear

* auto-complete etc. doesn't work as well

* type errors not caught as early (for generics this means you get super confusing type errors)

Obviously some functions are intended to be generic (e.g. operations on containers) but typical OCaml code seems to contain functions that are actually only called with one type - and the author only intended for them to be called with one type - yet they get inferred as generic.


Actually generic types can make reasoning about code much easier. This is the opposite of runtime dynamic types. Generic types tell you what the code doesn't (and cannot) do. Any generic type basically cannot be inspected or unpacked, see the paper "Theorems for free" for examples.


> Right but being overly generic makes them hard to understand.

Oh yes, I'm not arguing against that or your other points. I just wanted to express that the types being inferred as generic is not an error, but by design.


Right, I didn't mean to imply otherwise. Just that that design has big downsides.


OCaml is great, we learned it in class back in college, then we learned Rust right afterwards. Oftentimes people say OCaml is Rust without the borrow checker (or that Rust is OCaml with a borrow checker), but that's not quite true. Since it is entirely functional and recursive, it takes more time to wrap your head around. Now with OCaml 5, we also have algebraic effect support, something that Rust is looking to add in as well, in a more moderate capacity.

Also, if you don't like the OCaml syntax, check out ReasonML, which is an alternative OCaml syntax that Facebook developed several years ago. It has the Algol-style that many modern languages today use.


I think about it the other way; Rust is an ML with a borrow checker. Most of the stuff I hear people gushing about in Rust is IMO them experiencing what's it's like to write ML.


Yes and no, Rust is ML inspired but it is still way too imperative and not as functional to be called a true ML. But yes, things like Result and Option types, do-notion via "?," at least for Results and Options, algebraic data types are all part of the appeal. There sadly are still no higher kinded types though, but that is a difficult problem to solve and most won't encounter such problems anyway in day-to-day coding, so the contributors made generic associated types instead as an alternative.


I think the ‘is or is not a true ML’ argument is a bit unwinnable so I won’t comment on it. Regarding ‘functional’, Rust makes it harder to write in a traditional functional style because (a) it doesn’t offer tail-call elimination and (b) the resource-ownership tracking makes most normal functions feel side-effecty. I think it’s pretty hard to have a natural-feeling functional style without cheap allocation and some kind of deferred automatic deallocation (this is carefully phrased to allow for a garbage collector or a kind of arena allocation)

I don’t think this is that bad for rust though. For (a), it turns out that tail calls can make debugging harder, and they can be incompatible with certain type system changes you might want in an ML, and lots of OCaml (perhaps not a true ML?) in practice uses little explicit recursion and instead first-class functions with names like map and iter rather than recursion. So I don’t really think it’s very important. Rust also has these first-class iterator functions although a bit less of map for containers that can have many things (e.g. vectors). Rust iterations allow for writing code that is more functional when memory/performance are stronger constraints because the pipeline gets composed together instead of producing lots of intermediate objects. For (b) I guess the big difference is that the usual tool in functional programming for manipulating data is creating new copies of immutable data whereas in rust it is controlling ownership and then just updating it (a middle ground may be koka where you write code in the former style but the compiled code inspects recounts to potentially just mutate instead)


For sure, I agree with your points, which is why I write Rust these days instead of OCaml. Sometimes you really just want to get stuff done, while also being faster in execution speed of both the machine and the human.


> it doesn’t offer tail-call elimination

Why not? I’ve heard people say this about C too and I never understood it: LLVM and GCC both have optimization passes that perform tail-call elimination.


Typically when people say that they mean that the language doesn't have guaranteed tail-call optimization. You can certainly write Rust that has the TCO pass applied, but it can be brittle because it's just an optimisation pass that might not happen.


I don’t know the actual reason. It may have been a desire to keep flexibility in the language design by not imposing TCE rules. I think one reason is that you can’t have a tail call if you need to free things afterwards, so changing the signature from taking an object to borrowing it could make calls to the function no-longer eliminatable. Rules for which calls are tail-calls can already be confusing to people in simpler languages.

I think they also make debugging harder, aren’t used much in higher-level code, and aren’t necessary if you have good looping constructs.


True. In this case the author is speaking specifically about OCaml, which is quite comfortable to write imperatively.

I think the thing that tickles most people about Rust is the thoroughness of the type inference and type checking, which one generally gets from any ML.


Yeah but traditional ML languages are ass slow. If I want to write applications that are that slow, why not use something that targets the browser or something like Ruby or Python that has a much deeper ecosystem to leverage? Unless you’re just playing around with building a language and trying out different ideas or you think it’s better at some other axis (eg lower defect rate). But I’ve generally found that my incident rate for bugs doesn’t change with languages. What rust has done successfully and “ML” languages have not is how to engage a broader community by exploiting a weakness in some class of problems they can do better on that existing languages cannot. That’s why they shifted into systems languages. Memory safety in systems languages was critically important and ML languages are typically too slow and memory hungry so Rust adopted some of the C/C++ communities obsession with speed and memory usage philosophies while allowing for much safer code to be written. I think it’s safe to say they’ve finally dealt a “COBOL” like blow in that completely new code being written is unlikely to be C++ and C++ developer salaries will keep going up because it’s a difficult niche. will take a few decades to play out because C and C++ is so heavily entrenched. And who knows, maybe the memory safety efforts the standards body is taking will help but I think ultimately it will only be useful for hardening existing codebases but that’s a temporary patch on a bleed. Rust used that initial wedge to jam in a code repository, making testing and benchmarking easier, etc etc. some of the Rust libraries are insanely high quality and waaay easier to work with than in C++ land (even at Google and Facebook where it was a record level of easy)


I don't think ML languages tend to be that slow, depending on what you compare them with and which member of the family you are using for comparison.

If we have three stages: (1) system languages, (2) higher level languages like Dart, Java, C#, (3) very high level languages like Python and Ruby, then the those among the most popular languages in the ML family (OCaml and F#) comfortably occupy the same level as (2).

Maybe that's not what you're after. OCaml's more rare bytecode implementation (instead of native code) is about on par with Python and SML/NJ is pretty slow as well (although faster SML implementations like MLTon exist).


As someone that was introduced into ML languages via Caml Light in 1995, and has used a few of them since then, if the program is ass slow maybe the developer should have spent some time reading and practicing Algorithms and Data Structures.


The ? thing, didn't that get borrow from Swift, in fact?


IANASE (swift expert), but the rust ? operator is used for error propragation in the same way that the try operator in swift is. Swift's ?? operator is for unwrapping an option type: x ?? default, similar to x.unwrap_or(default) in Rust. (Swift's ? is the ternary operator, as in C; rust does not have a ternary operator, using expression-if instead). Applied to a type, swift's ? is the same as Option<x> in Rust.

Rust's first mechanism for error propagation was the now-deprecated try!() macro, which did basically the same thing.


No. For starters ? was introduced by C# 2.0 almost 10 years before Swift appeared so it would have been lifted from that if anything, but then Rust’s ? has completely different semantics than everyone else.


Creator of the Rust language Graydon Hoare is working on Swift, so it’s probably the other way around


He left Rust long before ? was introduced.


oh fair enough. Didn't know the timing of that, so I thought it was connected.


As far as I know, Graydon hasn’t been working on Swift for a few years now.


> still way too imperative and not as functional to be called a true ML

As a Haskell fanboy, this is how I feel about Ocaml and F#.


What are your thoughts on Idris and other Haskell derivatives? As well as the HVM and its Lisp-like Haskell-like combo of a language [0]?

[0] https://github.com/HigherOrderCO/hvm


Almost like people want some functional features without all the extra baggage.

I guess I would even say it's like... most people just want the banana, but got a gorilla holding the banana and the rest of the jungle as well.


I don't think it's the functional part of ML that tickles them. I think it's the thoroughness of the type inference and type checking.


At least in my piece of corporate software engineering, I'm looking at Rust as a "gateway drug" for FP. Management doesn't want to take the risk of investing in FP, so reliable choices like Java and C++ are usually endorsed; the safety aspects of Rust are a much stronger argument for them.


I don't think the safety aspects are a much stronger argument. Managed languages like Java and C# are safe. The garbage collector isn't going to let you ++ your way into remote code execution and most corporate software engineering isn't heavily concurrent. I think a stronger argument would be to advocate for a more functional language on the same runtime, like Scala or F#.


Java and C# will not provide any sanity check on your use of locks and shared mutable state though. But the Rust borrow checker will. It won't prevent deadlock or race conditions, but it will at least guide you there much better than those pass-by-mutable-reference OO languages will.


99.9% of all code written in these languages uses a thread per request model. Objects never go over a thread boundary. Using rust will only complicate these code bases for concerns that will never occur.

…and if you’re concerned about mutability you can use an immutable first language like Scala or F#.


Sure, and that's the right way to do it. But if you cross the thread boundary, it won't stop you. Rust will at least try.


ML with a borrow checker is a bit of an oxymoron though. Because proper closures (the kind Rust can't do) are essential for the classic functional programming that ML represents.


Jane Street just entered the room: https://blog.janestreet.com/oxidizing-ocaml-locality/

This part specially about closures: https://blog.janestreet.com/oxidizing-ocaml-ownership/

https://blog.janestreet.com/oxidizing-ocaml-parallelism/ And yes, their changes introduce quite some additional syntax.


Can you explain "proper closures"?


I would hazard that this means closures that can capture (shared) state. In Rust, a closure’s data must either be moved into the closure or outlive the closure (dangling pointers etc etc). In most other languages, GC allows closures to keep a handle to any data at all.


If you want a closure to outlive the outside bindings of its captures and thus have shared control over their lifecycle (which is the default behavior of closures in most FP languages) you can easily use Rc<> or Arc<> to that effect. Rust just forces you to be explicit about it.


Exactly - sharing is the core of FP


Nope, that is what an ML with linear types does.


ML does garbage collection, if it didn't need it it wouldn't do it.


I like both OCaml and Rust, especially OCaml, but even after 2 years of Rust I'm still way more productive in OCaml. Unless it's absolutely essentially, I really don't want to have track the life times of my objects so explicitly.

Thus, Rust for me serves mostly as a C/C++ alternative. I don't plan to ever write anything in C again, and even if it's not my goto language otherwise, for that I'm thankful.


I just clone and don't worry about it, works well.


I kinda just think of Rust as C++ with a vaguely MLish inspired syntax & some aspects of the type & module system -- which is sort of how it explicitly started TBH -- and the borrow checker just grew out of what you end up needing to do if you rip the garbage collector out of a language like that.


Rust was originally garbage collected. From Graydon Hoare's post, he didn't want explicit lifetimes originally either. He agreed because he thought they would always be inferred by the compiler.

https://graydon2.dreamwidth.org/307291.html

It seems to me that Rust evolved into a better C++, but it did not start out that way.


Early on I argued for bignums and decimals in Rust, imagining also things like automatic currying, and other FP features

But it went a completely different way, ripping out any runtime it had, making reference counting a lot more noisy, etc.

It would be interesting to build a "simpler Rust"


Always felt zig was a lot closer to ocaml than rust was.


People say that OCaml is like Rust, but unlike Rust, OCaml has Exceptions that could appear everywhere. How is that safe?


Rust has panics as well and they appear pretty much everywhere, because the Rust stdlb made a conscious decision to panic on allocation failures (later non-panicking APIs were added, but most people dont use them, and in special, most dependencies will not use this and will panic on random occasions), and also because common operations like integer division and array indexing will panic on bugs (and also integer overflow on debug builds)

In any case OCaml has memory safety, sort of (it has data races but as described in the paper "data races bounded in time and space", data races in OCaml doesn't lead to unrestricted UB like in Rust, C, C++, and most other languages actually) (unless you opt into OCaml's unsafe constructs like Obj.magic, which is like Rust's unsafe without using unsafe {} block), because it has a GC

So when you talk about safety in the context of exceptions, you probably mean exception safety rather than memory safety. Exception safety basically means you code works even if an exception is raised. Which is really hard to assure since as you said, exceptions are everywhere

But Rust suffer from this same problem! In Rust this is called panic safety and mitigating it has taken a great deal of complexity, with things like lock poisoning, which adds overhead but limit the scope of panics in multithreaded programs, and the UnwindSafe trait, which is probably a good attempt but is ignored in most of the ecosystem. Many people think such measures are inadequate and insufficient and prefer to run programs with panic=abort, which means to just terminate the program when there is any panic. (many C++ projects disable exceptions in the same way for example)

Which is kind of unfortunate because now there are many Rust programs that are only correct if you run with panic=abort, and will break if you enable stack unwinding (which is the default)


Rust currently cannot reliably panic on allocation failures because the error object is inside a Box, which itself requires allocation. This means that if memory is tight, panicking itself might fail.

For reference, this is the type that is returned from catch_unwind: https://doc.rust-lang.org/std/thread/type.Result.html

But I completely agree that Rust has exceptions. They are even used in the toolchain implementation for non-local control flow.


> Rust currently cannot reliably panic on allocation failures because the error object is inside a Box, which itself requires allocation. This means that if memory is tight, panicking itself might fail.

This seems like something that should be allocated at program startup, just like other things like the program's environment (I think it's copied to Rust's own data structures at startup to avoid using the non-threadsafe C API), and other things allocated at startup

.. but of course, not at Linux, such error allocation would be unneeded there..

.. except of you disable overcommit, which can and do happen, so in the general case you don't know if this error object can ever appear


The Box type, as used in the return type, transfers ownership to the calling function, so a check in the deallocator path would be needed to recognize this special object and avoid deallocating it. GCC has an emergency pool for its exception allocations, which is also quite ugly. And of course that pool can be too small.

It should be possible to add a third arm to that Result type, returning some &'static reference, but I'm not sure how to do it in a backwards-compatibile way.


I mean, allocate something like

static ALLOCATION_ERROR: Box<dyn Error> = (something);

and then use this variable whenever there is an allocation error. you might need unsafe { } but that's okay, the stdlib is full of unsafe


If you use it for producing the Box in the return value, you have to move out of it, and then it's gone. If you don't move it, you have a use-after-free bug after the first such panic has been caught and the box has been dropped as a result.


Safety was something added to Rust as it developed, not one of the original goals. As I recall it.

And you're working with multiple definitions of "safety" here, and Rust sorta conflates them all via borrow checker, but the one people are usually most concerned with is memory safety which is not a concern for a garbage collected language.

I do seem to recall that StandardML did not have exceptions though. And I always felt that SML was the better language.

OCaml adding OO classes and exceptions and other 90s trends that actually have ended up not aging well...


From the presentation introducing Rust to Mozilla: http://venge.net/graydon/talks/intro-talk-2.pdf

> I have been writing a compiled, concurrent, safe, systems programming language for the past four and a half years.

Safety was always part of it.


But those "Safety" definitions are all not like what Rust folks mean by safety now. He's talking about immutability and bounds checking and avoiding memory corruption but not at all about borrowing.

I guess I should have been more specific. If that's what we mean by safe, then OCaml is safe as well.

Anyways, I followed it at the time. The borrow checker came later.


I absolutely agree the borrow checker came later. I think of it as that the goals have always been the same, but the enforcement mechanism changed over time, as more and more static ways were found to achieve the goal.


I agree, Standard ML's syntax feels a lot cleaner than that of OCamls.


I think of OCaml like a "kitchen sink" language, much like Scala. The reference ML implementations like SML and Jersey seem to be much more limited in scope, and are nice for learning / getting a feel for the original intentions behind the language family.


Rust has panics that could appear anywhere.


But the flow control is easier to reason about. You don't have to go guessing about non-local catch blocks that the caller may have introduced. The code either panics, or propagates.

Exceptions look remarkably wrong headed to me in retrospect. Allowing the caller to change the error handling contract and flow control.


Rust panics can be catched too, but it's true that it's less common than in languages with exceptions


I don't like Exceptions in OCaml (or Haskell or C++ or JS/TS) either. But they aren't unsafe, except for bugs in the compiler/runtime (in OCaml, in C++ there are of course some footguns ;). Of course they add "bottom" to any function where an Exceptions can occur (and other things, see for example https://markkarpov.com/tutorial/Exceptions) but for stuff like `0/0` there are 3 possibilities:

use something like `Maybe` for the result - clumsy.

return (for example) 0, which is what most theorem provers (like Coq or Lean and dependently typed languages like Idris) do, that need their functions to be total

or throw an exception.

(C's solution of declari g it undefined behavior is missing).

Now, with OCaml's effect systém, there also is no need to use exceptions for control flow - which you should have never done anyway.


F# has exceptions. You can just pattern match on them.


In OCaml the type checker won't force you to handle exceptions (from what I remember.) See e.g. https://ocaml.org/docs/error-handling#exceptions


That's true, but at least in my experience, it is rarely a problem. Because if you're at a point in your program where you don't want to bubble up, you can just pattern match against the exceptions just as you would a Result type, which F# also has.

I don't know Rust, but after searching, it seems that it has a panic facility which seems even more escaping than an exception. Happy to be corrected there.


`panic` has nothing to do with error handling though. If you use it, you know that your callers cannot recover from it.

Throwing an exception implicitly delegates error handling back to the caller, but they are not even notified about it. (talking about OCaml)


No, they can, check the catch_unwind API.

Some frameworks catch panics automatically. For example, in the Actix web framework, if you panic in an HTTP response the panic will be catchee, so it won't bring the whole server down.

Also, by default a panic will terminate only the current thread, which is a major footgun: you now need to reason about what you program will do after some bug or unforseen circunstance happened somewhere in the code and made you program misbehave. Which leads to slightly insane things like lock poisoning.

It's more sane to compile with panic=abort, but that's not the default and it means that on panic you won't release resources (which aren't just allocated memory to be clear)


Yeah. I don't find it to be a problem either, but the parent does and I can see where they are coming from. Even Java has checked exceptions.


why do feel exceptions make a language unsafe?


Second, less-predictable execution path. Additional cognitive load in evaluating effects across both paths. Additional opportunity for bugs. Doesn't necessarily mean that exceptions make all software which uses them unsafe, but does tend to mean that exceptions significantly complicate the task of proving safety for any nontrivial program.


> proving safety for any nontrivial program.

I haven't done this, but probably the surest way to do that would be to use a proof assistant and extract the result to Standard ML or to OCaml. Standard ML has verified implementations, too.


Safe isn't the best word to describe it with. But it does mean that any expression or statement always has two possible control flows. You have the "surface flow" as well as the exceptional flow, so there's an added complexity.

I never felt this was a problem when I did Java though (despite their awkwardness - basically forcing coders to not use checked exceptions.)

Rust's control flow syntax for Results and Options are very similar to this but with an added benefit: you don't have to use the ?-operator.

panics is different, however. They are more akin to the way any Java program will happily OutOfMemoryError or NoClassDefFoundError given circumstances not (always) in your control.


I don't think panics are comparable to Java's VM errors. A lot of libraries (not just the standard library) seem to target panic-safety, avoiding unsafe behavior and resource leaks in case of panics. This means that the panic itself is not supposed to transition the process into an undefined state, like it happens with many VM errors in Java (where a stack overflow may mean that required cleanup action has not executed, for example).

With Rust, the overall situation is a bit strange: as a library author, you are expected to deal with the possibility of panics (which gives you all the headaches associated with dealing with exception safety), but as a user, you are not supposed to rely on them. (I expect that most request handler loops will have catch_unwind handlers, to avoid a faulty request taking down the entire process.)


I'm relatively new to Rust. I use panic (and cousins) in fn main only. As in: I'll expect(), unwrap() or similarly handle missing bootstrapping circumstances. Outside of main, I'll never ever use any of them. Even when I "know" that a condition is Impossible(tm).


Can't comment on the comparison to Rust, but I recently spent quite some time learning OCaml, working through the excellent and freely available cs3110 course https://cs3110.github.io/textbook/cover.html ; I really really wanted to like the language... I agree w/ the submitted article about the heavy reliance on linked lists and recursion, but what disillusioned me from it is that after many weeks of study I discovered competitive programming and on a whim started doing some easy problems.

Since I was spending most of my time w/ OCaml at that point I thought it could also be a way to practice it further. So for a particularly easy problem it would go something like this: A straight forward, easy to read, performant C++ solution took me 10 minutes; an ugly unidiomatic OCaml version took me 30 minutes; and a beautiful idiomatic OCaml version using recursion that still no non-OCaml programmer could ever read took me something like an hour...

It really dawned on me that after weeks of studying OCaml I barely knew how to write anything particularly useful in it from scratch. Dealing with user input/output still seemed cumbersome, given everything's immutability... from an intellectual perspective it feels interesting, and I had fun with it, but for now I decided I couldn't see myself becoming productive enough in it to justify further sinking time into it. There's so much to still learn for me (doing a network related project at the moment learning a ton about networking, sockets, etc), that trying to become an unproductive OCaml programmer probably shouldn't be high up my list of priorities... and that is not to say people aren't productive in it, that's to say that I don't see myself becoming productive in it any time soon, compared to being productive in C++ while being far away from being any expert in it.

Maybe I'm too stupid for it, am not suited for it, need a few years of further programming to appreciate some things about it... but for now, sadly, I feel like I've wasted significant time I should have better spent on studying other topics and actually working on projects. And addendum: Never comment on downvotes, I know, but how I immediately got one for this comment... surprising.


Not all languages are equaly suitable to all tasks. If your leetcode easy problem is to write a quick sort, you wouldn't use linked list + recursion. That being said, the OCaml idiomatic solution is to declare a mutable array and write your for-loop, just like you would do in C++.

> given everything's immutability

OCaml has a lot of mutable datastructures. It's perfectly fine to use them. Even though with experience, you realize there's often a a better/safer way to do it.

Also, I'm surprised that in 2023, functional programming still sounds like a mystery to some people. Python/Rust/C++ all have closures, immutable datastructures, their versions of the usual functional combinators, some form of variant types / pattern-matching (maybe not python?). C++23 even has monads. CppCon is full of talks of people realizing that they can write simpler code using this things. I believe all this FP tools should be in the bag of any proficient programmer.


I'm not surprised people still don't know functional programming at all. It is really such a different way of programming, and if you don't try to write an entire project using the style, it's just too easy to fall back into mutable code, depriving your self of the greater benefits.

Most of the code I used to write was just gluing libraries together, even heavily Object Oriented stuff was too complicated and becomes a spider web of dependencies, so I always hunted for better libraries, rather than try to extend existing ones.

Now, I've been writing Clojure for about 3 or 4 years, and I still struggle with certain functional and immutable algorithms. But, now when I go back to Python I occasionally get bit very hard from the surprising mutability. So my brain is definitely adjusting to the safety.

I'm still not fluent with a lot of higher order functional concepts. Though I think I'm finally understanding Monads and Transducers.

It's hard to break 10+ years of imperative habit. What really helped me was diving down into the core of how parsers and code evaluation works. I didn't read SICP, but understanding the plumbing of these languages can help ease the transition, at least for me...

Regardless, it's a lot of dedicated work that a lot of people don't want to invest.


> Not all languages are equaly suitable to all tasks.

That is true. Although, I would make the argument that F# and OCaml are two of the best most general purpose languages available. They support the imperative, OOP, and functional paradigms effectively equally. About the only thing you wouldn't use them for is embedded real-time programming. Other than that, they're statically typed with great type inference and sane scoping and syntax (I find F# to be the "cleaner" language) and are performant.


CppCon is a very niche audience in my experience. Most "C++ programmers" use a subset of C++11 with some UB sprinkled here and there. When you care about productivity I'm starting to suspect that's actually fine.


> Dealing with user input/output still seemed cumbersome, given everything's immutability...

This sentence baffled mé, why would immutability be a problém with user input or output?

But changing the way how you solve problems takes time.


You're not stupid. Functional programming requires its own way of thinking and it's different from procedural languages with mutable-state soup, like C++. It's less that OCaml is hard to learn, and more that you have to leave old habits of thinking and learn new ones. Don't give up just yet.


Right. Same goes for learning any radically unfamiliar style of programming. There are people who say once you've learned one programming language it's easy to learn any other programming language, but this just isn't true. The differences between languages aren't always skin deep.

On the plus side these are the learning curves most worth climbing, as it broadens your understanding. That might be reason enough itself, but it might also make you a better programmer in your usual wheelhouse. Haskell, Forth, and assembly, are worth taking a look at for this reason.

If you want to go further down the exotic language rabbit hole, there's Mercury [0] and Joy, [1] both of which I keep meaning to learn. (Hadn't realised until today that they're both from the University of Melbourne.)

[0] https://en.wikipedia.org/wiki/Mercury_(programming_language)

[1] https://en.wikipedia.org/wiki/Joy_(programming_language)


What about back when you started to sturdy C++. Will that not have taken you more than an hour to do an performant and elegant C++ solution?

I feel what is critical is to practice it more often and also to do a jump into how we think as a developer because OCaml is a functional language from the ML family. The way we have to design and think is not exactly the same as all the other mainstream languages.


> A straight forward, easy to read, performant C++ solution took me 10 minutes; an ugly unidiomatic OCaml version took me 30 minutes; and a beautiful idiomatic OCaml version using recursion that still no non-OCaml programmer could ever read took me something like an hour...

Replace C++ with English and OCaml with Japanese or Estonian and you'll get the idea.

OCaml is a vastly different language from what C++ is. Switching to OCaml from C++ is not at all like switching to Go or Java. If you switched to OCaml from SML or Haskell, you would write a completely different opinion.

Paradigm shift takes huge mental effort, that's something you have to deal with, unless you want to confine yourself into the comfy world of familiar languages.


After a few weeks, I am disappointed in not being an expert at $new_thing. $new_thing bad!


Downvote it all you want. I've spent the last decade writing in this style and to me it's second nature. Starting Rust I've had to basically relearn solving problems with loops and iterators. Solving problems with loops is not the way I think.

Never once thought negatively of Rust for this.


After a few weeks (and I believe it was written many weeks, so to me, that sounds like a couple of months), it's not unreasonable for experienced programmer with already a couple of languages in their toolbelt to expect to get _some_ things done in a new programming language.


After having learned French, German and Spanish, I spent three months learning Chinese and could not speak it fluently, what a shoddy language


"An NFL player found that he managed to be good at rugby after 2 months of practice, but when he tried to do gymnastics (or cycling or tennis or motorsports or ...) in the same time frame, the results were terrible."

What does that tell you the sports he tried to do? Not much. It's all about his previous experience and skills.


Many of today's popular programming languages are variations on the same theme, leading people to think that their experience is broader than it actually is.


But it isn't (just) a new programming language, it's a different way of solving problems.


yes: paradigm shifts are not easy.


I'd say one thing in your defense, it's absolutely true that the ocaml standard library is atrocious. Replace it with something like Base and you will at least stop struggling with basic stuff like simple IO.


OCaml is not a very good fit for competitive programming, you're better off with a language like Python there.

Writing code in a functional language like OCaml requires a mind switch. This can take quite a while. It took me years to really 'get it' after having programmed for 30 years in imperative languages. Now I prefer writing code in FP for a lot of things, but not for everything. E.g. for coding some algorithms you have a much easier time when you can use mutable arrays.

A nice advantage of OCaml is, IMO, that it is not so strict about pure FP. E.g. unlike in Haskell, arrays are mutable by default.


While Haskell's Array type indeed immutable, there is an incremental update function

    (//)            :: (Ix a) => Array a b -> [(a,b)] -> Array a b
that gives you a new updated array. As long as you no longer refer to the old instance, this is as efficient as a mutable array.

But Haskell also offers the mutable STArray and STUArray, for boxed and unboxed types [1]. These preserve their purity by being monadic.

[1] https://hackage.haskell.org/package/array-0.5.6.0/docs/Data-...


> As long as you no longer refer to the old instance, this is as efficient as a mutable array.

On paper this is true, in reality though it probably is not. Immutable Haskell arrays are implemented with pointers, mutable arrays are implemented as contiguous memory allocations, so they have great cache locality.


> heavy use of chained iterator methods in favor over traditional loops. This is one of the more intimidating hurdles for newcomers to [Rust], but after getting used to it, rarely will you see anyone write a for loop again.

I think this varies a lot from person to person. Rust includes syntactic sugar for imperative code (like `if let` and `let else`), and personally I prefer to use that when it's not too much trouble. I find it's pretty rare that I reach for map/and_then/etc.


Seeing his picture, I figured this guy almost looks like a thug, and sure enough he's in prison, albeit not a thug anymore. His story:

https://pthorpe92.github.io/intro/my-story/

Looks like he's got everything lined up for success when he one days is let out of prison. Great attitude and lots of experience.



i had some of the same problems; here's how i solved them, though keep in mind i am no ocaml expert, just a dabbler

at some point i got sick of trying to debug mysterious compile errors about types in ocaml and started declaring types for all of my function arguments; ocaml lets you do that. an example is http://canonical.org/~kragen/sw/dev3/mukanren.ml (an implementation of a tiny logic programming language, sort of like prolog with superpowers; cw inappropriate intimacy) where, for example, instead of writing

    let rec disj g1 g2 t = mplus (g1 t) (g2 t)
i write

    let rec
        disj (g1 : goal) (g2 : goal) (t : state) = mplus (g1 t) (g2 t)
which is more code, to be sure, but i think easier to understand because it's more explicit. and the compiler doesn't report your type errors in the wrong function anymore when you do that

if you aren't sure what the types should be (easy when parametric polymorphism gets involved) you can paste the code without type declarations into the repl and see what it says

in this case that looks like this

    # let rec disj g1 g2 t = mplus (g1 t) (g2 t) ;;
      val disj : ('a -> 'b stream) -> ('a -> 'b stream) -> 'a -> 'b stream = <fun>
and in fact my `goal` type is more specific, `state -> state stream`, thus favoring comprehensibility a bit over flexibility (i can always go back and generalize the type more later)

this makes the code a bit more verbose but most functions are more than five words long so it's not actually that bad

maybe i should declare the return type too

    let rec
        disj (g1 : goal) (g2 : goal) (t : state) : state stream = mplus (g1 t) (g2 t)
but so far parameter declarations seem like about the right balance

declaring parameter types in your own code doesn't help with code you didn't write, but you can use the repl to find out what types the compiler inferred for it

    # List.map ;;
    - : ('a -> 'b) -> 'a list -> 'b list = <fun>
presumably editors with lsp support have some magic keystroke to display this on demand in your editor or something, maybe an lsp user can tell us what it is


There is a vscode extension for the ocaml-lsp that will show type annotations above each line of code.


so, no keystrokes, but you can only fit half as much code on your screen?


The biggest issue for me with OCaml is OPAM. It's pretty awful. Simple things like "install this code" don't work reliably - often installing a different branch or just ignoring edits you've made, even after you discover the flag that says it should include them (it seems to be simply broken). It also doesn't work on Windows in any meaningful way.

It's not just me that has these issues. We introduced a tool into my company that is written in OCaml and I ended up spending so much time helping people fight OPAM that I eventually implemented a build cache system so they didn't need to actually compile the OCaml code at all. Saved a bunch of hassle.

It seems like a pretty decent language, aside from the issues the author of this article mentioned which are 100% true. But OPAM means I would never pick it.

Similar issue to Python, which isn't really a bad language (I'd say it's mediocre-to-ok), but the packaging and tooling story is abysmal.

More languages need to learn from Go, Deno and to a slightly lesser extent Rust which have all prioritised having a packaging system that doesn't make you want to tear your eyes out.


The biggest problém with Opam and OCaml on Windows is that most of the packages won't work. Officially, Opam does not support Windows at all, that is going to be included with 2.2 (which is still in alpha AFAIK) https://opam.ocaml.org/blog/opam-2-2-0-alpha/#Windows-Suppor...

I always thought of Opam as the best feature of OCaml (especially compared to Haskell with two no-working package managers - and no, Nix is a problém, not a solution either).


Yeah in fairness at least it has one package manager and it does seem well designed even if it doesn't actually work properly half the time. So it is fixable!

Which is more than you can say for Python and C++.


Yeah my semi-hot take is that type annotations in functions is a feature not a bug. It forces legible interfaces (with the exception of nasty generics I suppose).


But they can just be auto-generated in documentation. It gets pretty annoying to type annotate everything for nothing but warm fuzzies when the compiler can figure it all out for you.

Also, any decent IDE will show the types any way.

The complaint about the types is just strange and shows a failure in getting Rust out of their head. Once you get used to F# and OCaml, then going back to a language that forces type annotations everywhere gets old and seems archaic because you spend all this time telling the compiler what it already knows (in a language like F# and OCaml). However, there are times in which type annotations are needed, especially in F# when dealing with objects. I generally type annotate when I feel the name of the argument doesn't capture the type.

Writing F# often feels very Pythonic or Scheme-y, but at the end of the day, everything is being statically typechecked, so it's the best of both worlds.


> The complaint about the types is just strange and shows a failure in getting Rust out of their head.

I second this a lot. It's very strange to me that the author is complaining about powerful type inference. In my limited experience with ReScript, I've found the type inference to drastically reduce boilerplate while providing the same type safety guarantees as TypeScript. Moreover, your editor or IDE can always tell you what the inferred type is. I am really not sure what I am missing. The author headlines the paragraph with "Where are the types? [sic]" but the types are there! They're inferred and visible in your IDE!

The provided code snippet doesn't really help illustrate the point either, IMO. It will look unfamiliar to those who've never used OCaml before, and to those who have done more than a fizzbuzz in OCaml, it will look okay.


Well there's also the fact that the types flow downward from the interface. Like the source of truth comes from your function parameter types that are always explicitly written. One thing that tripped me up writing OCaml is that you could have a mistake in your function that'd show up as a bug in a completely different location due to the inference algorithm. Like dumb example but say I have `foo` instead of `foo()` that changes the function return type to be a () -> int instead of int. You'll get the error when you use the function result instead of inside the function.


> It gets pretty annoying to type annotate everything for nothing but warm fuzzies when the compiler can figure it all out for you.

This is sometimes fine for internal code, but for external interfaces you want a human to determine the interface contract you’re agreeing to, rather than the minimal set or the maximal set of types the function supports as currently-written.


Perhaps, but presumably code that is meant to have an external interface will be in a module and will have an *.mli file to it, and that is an excellent place to constrain your types explicitly if you must.


In OCaml most people are writing signatures to which their modules conform (usually in separate mli files). The problem is that checking the signature happens after all the type inference so you can’t use it to disambiguate constructors for function arguments/results, and they can’t be used much to guide users who make type errors in their functions. I think it was a good idea that rust put the types in arguments to non-anonymous function.


> because you spend all this time telling the compiler what it already knows

But a program should be optimized not for reading by a compiler but for reading by a human. This is essential for maintainability.

The question is not how hard it is for a compiler to deduce types, but how hard it is for a human (and a human that hasn't necessary written this ode, and hasn't necessary read and remembered the whole codebase) to figure it out.

It is not a simple question in general. Too much "infrastructure" clutter can hide the logic and intent of the code too. So to get the right balance in a language or in the code is non-trivial.


> But a program should be optimized not for reading by a compiler but for reading by a human.

I absolutely agree, more than you could imagine, but I think this is not a case where that's the issue. Type annotating everything will quickly make the code harder to read. There's a reason why people like Python. But the problem with Python is that you don't have a way to figure out the types. With F#, just hover over anything and get the type.

> This is essential for maintainability.

Even more to the point, over type annotating will make maintenance harder, not easier. This is because you are fixing the types statically. When you then go to update code in the future, you have thrown out the benefits of type inference and now have to go around manually updating all of these type annotations.

So you took the quote a bit out of context. If you're type annotating code, you're telling the compiler something it already knows, you're telling the human reader what it can already find out immediately for any value, you're increasing the surface area of needed updates, and making the code harder to read.


Unfortunately in the 21st century still too many people like to program the hard way without tools that improve their workflows.

Maybe they should do batch compilations as well, why bother with interactive programming.


OCaml has a separate language for module interfaces where types are required. Even better, it allows you to abstract over types and make them entirely opaque, so that users of an interface never have to look at any of the implementation details.


As you get more comfortable with functional programming, the type signatures tend to get more complex for sophisticated code. Those "nasty generics" become reliable friends.

For example, since functions are first-class objects, they are often passed around as parameters to other "higher-order" functions. Explicitly declaring the types of all these functions is tedious at best, and confusing at worst.


I'm not convinced that these type signatures have to be that bad. You could probably make them pretty tenable, especially with a good code formatter.

Also, I'm confused. We're using type driven development and writing the type signatures as part of the documentation, we should be thinking in terms of types, why would they be difficult to write?


They're not bad. They're just a pain to type out in detail every time. Here's an example signature from an F# library I like called FParsec:

    pipe2: Parser<'a,'u> -> Parser<'b,'u> -> ('a -> 'b -> 'c) -> Parser<'c,'u>
That's fine for documentation, but I'm glad it's not cluttering up the actual implementation, which starts like this:

    let pipe2 (p1: Parser<'a,'u>) (p2: Parser<'b,'u>) f =
        ...
You can see how the code does explicitly specify the types of `p1` and `p2`, but doesn't bother spelling out the type of `f` or the return type. Although we are thinking in types the whole time, this sort of flexibility in the implementation is important for real-world functional code.

(For anyone wondering, this function takes two parsers and a function as input. It sends the output of each parser to the function, and the result is itself a parser. This is equivalent to a well-known abstract function called `liftA2` in a language that supports typeclasses, like Haskell.)

[0]: https://www.quanttec.com/fparsec/reference/primitives.html#m...

[1]: https://github.com/stephan-tolksdorf/fparsec/blob/master/FPa...

[2]: https://hoogle.haskell.org/?hoogle=liftA2


This is a really good question. When you write generic code in a language with higher-order functions, type signatures can get pretty hairy with type variables and quantifiers flying around (forall a. a -> foo a). In a functional language with type inference, it's mostly managing those for you, and you can (usually) just write code in a natural way and it automatically is polymorphic, but in a safer way than dynamically-typed languages.


I think that is not a hot take at all, haha. For some reason, in TypeScript people like to have return types inferred, and while I do that just because I'm lazy in my own projects, we enforce explicit return types in our work codebases simply because you never know when something might change in the function body.


>... linked lists are slow and inefficient with modern CPU caches, and you should almost never use them

You should not use them most of the time in functional languages (like OCaml and Haskell and...) for this reasons either, it's just that (almost) all examples for beginners use them because they are "easier" than for example trees.

Oh, by the way, OCaml does not have significant whitespace (but e.g.F# and Haskell do).


You use them all the time in Haskell and OCaml. Cache locality isn't such an issue. You're not mallocing linked list nodes. You allocate by a pointer bump of the minor heap, and if the GC copies your list into the major heap, it's going to copy the list elements together.

You also use recursion all the time, and no, recursion is not generally straightforwardly optimised into iteration unless you're doing trivial tail recursion.


Yes I know and at least GHC optimizes lists better than most other data structures (I guess even isomorphic ones). But I guess your definition of "all the time" is not the samé as mine (yes, I use them in every project too). What I wanted to express is that lists are way less used than beginner tutorials may make it seem and there are other data structures available (like in other languages ;).


In Haskell you typically don’t allocate any memory when using the default list type, because of laziness. The it’s totally fine and efficient. Just don’t use it to store data for later retrieval.


Notice the ! in "!significant whitespace"

I thought I was being slick


Oh, sorry. I've read that as "non-lazy whitespace" ;)


Interestingly the original Rust compiler was written in OCaml. Or to be more precise the one used for bootstrapping rustc.


Horizontal scrolling code does make it quite impossible for me to follow the examples




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

Search: