Hacker News new | past | comments | ask | show | jobs | submit login
Comparing Objective Caml and Standard ML (chlipala.net)
93 points by weatherlight on Feb 15, 2023 | hide | past | favorite | 47 comments



This is an oldie but a goodie.

I remember reading this page making up my mind when choosing between the two.

OCaml has, unlike Standard ML, grown quite a lot since this page was made.

In particular, the section "Standard libraries", I'd recommend looking at:

https://dev.realworldocaml.org/

A couple of places where the comparison is outdated:

- OCaml using Base [1] allows for result-type oriented programming

- OCaml using Base uses less language magic and more module system

While there was and is truth to the distinction that SML is for scientists and OCaml is for engineers, this dichotomy is getting dated: OCaml is under active development, which means that scientists who want better tooling will choose OCaml. For example, 1ML [2] by Andreas Rossberg was built in OCaml.

[1]: https://opensource.janestreet.com/base/ [2]: https://github.com/rossberg/1ml


Remember that this is only comparing the languages. If you actually try to write code in Standard ML, you come to a rude awakening as to how poor the tooling is.

Libraries are underdocumented and unavailable. The build system and package management is arcane. SML feels like a toy language compared to working in OCaml.


PolyML has a lot more "stuff" build in like threads, multiprocess, and sockets. If I were to use SML in anger I'd use PolyML. Sadly I've never had the need.

https://www.polyml.org/documentation/Reference/Basis.html


SML has the nicer syntax though, and the millet language server is really really nice.


I remember reading this many years ago. TBH I always felt SML was a nicer language, syntactically -- more readable (mainly because of the let..in..end scoping syntax) -- and I really liked its Basis library; something about the API choices there seemed to be one of the better collections libraries I've ever used. The "object" part of OCaml didn't feel necessary to me.

But OCaml has had more momentum, and I don't know if anybody is even using SML anymore?

Interesting to look back with a bit of history now, too. Choices like having objects/classes and exceptions (instead of result types) added into OCaml were probably seen as modernizing and adding in features that people expected; but now the pendulum seems to have swung against those features. OOP hype has passed -- e.g. Rust and Go don't have 'classes' with inheritance. And exceptions are also lacking in Rust etc, in favour of Option/Result type + pattern matching, so that control flow is more explicit and easier to reason about.


If I've learned anything as a programmer, is that these things move in fashions. People will get sick of result types and exceptions will be hot again. OOP will be fashionable once again. The grass is always greener on the other side of the fence.


I think some things are fashions and some are lessons learned.

For example, English-like syntax is out. Cobol has it because it was invented for its predecessor and Cobol's still around, SQL has it because its designers copied Cobol before we'd learned better, and that's it. We still use words in our syntax but explicit block structure with punctuation is now known to be easier to read.

Speaking of block structure: Line numbers are gone. Unrestricted use of goto (as in, using goto to go from anywhere in the program to anywhere else in the program) is also gone. These things aren't fashion: We've learned better. We're more likely to invent a different kind of structure than to go back to that.

Similarly, languages with absolutely no type system are also out: You can have the types on variables, like Haskell, or values, like Python, but choosing to have neither, like BLISS and BCPL, is no longer an option unless you're actually writing in assembly language.

Other things, like column-oriented formatting, are gone because technology moved on. Even Cobol abandoned that one.


This is quite possibly true, but I feel like exceptions are one of those things... people are just finally cluing into the fact that this is a non-local GOTO, and the consequences for reasoning about program flow can be terrifying, like Djikstra already told us.

If exceptions have a "comeback" (they have not gone away in mainstream languages like Java, C#, Python etc.) I hope they come back in a way where they're bundled with static analysis features that help with the reasoning process.

When I worked in Java, 10+ years ago, checked exceptions were considered an obnoxious "no-no", and bad style. Mostly because people just wrapped and rethrew them as runtime exceptions. But, like, runtime exceptions are awful, and almost every application I worked in had buckets of garbage in the logs which consisted of uncaught or "caught & logged" exceptions. Such exceptions are particularly troublesome in highly concurrent applications.

Exceptions should be exceptional. I think Rust has made the right call here. Handle the error, or panic. Don't make it somebody else's problem.


Exceptions will come back, but in the form of algebraic effects. All of the work that goes into error handling, asynchrony, mutable state, and IO will be generalized by effects.


Maybe. Algebraic effects have been hyped for about 20 years and all the actual implementations still suck.


Tbf, I've only seen decent implementations of it over the past couple of years, with Koka and OCaml. There's still work to do to get it to interact nicely with a low-level language, but I'd argue that they're the way forward for newer GC'ed languages.


I view checked exceptions as syntactic sugar on what could be implemented as result types (as in “Either” result types) behind the scenes. It’s not how they are implemented in Java, but potentially they could. When used in that fashion, they are certainly more ergonomic than explicit result types. Being (type-)checked is crucial for that, of course.


Agreed, but back when I worked in Java and I tried to use checked exceptions in my code, team leads and fellow seniors always yelled at me.

Still, the catch syntax was very verbose for handling common conditions. Exceptions were the wrong syntactical tool for the job.


Sometimes a nonlocal goto is exactly what you want.

Consider some kind of validator/parser class/function, with a public entrypoint and a bunch of private subroutines that can fail when they hit invalid input:

    public parse() {
      try {
        this.parseX();
        this.parseY();
        this.parseFoo();
        ...
      } catch (e) {
        throw new Error(...)
      }
    }
    private parseY() {
      this.y = this.input.y.map(a => {
        if (bad data) throw new Error("corrupt data");
        ...
      })
    }
It's common in such situations to want to have one central point to collect errors in order to produce a single error type result.

It is possible to manually thread Results all the way through your logic, but that clutters the code with error handling. Rust's Try operator makes it easier but it's still awkward.

Exceptions allow us to concentrate on the two most important parts: where the exceptions are generated, and where they are handled (in this case, the root of the public interface).

I definitely agree that exceptions are not perfect. They have their own flaws, especially for public interfaces. But as control flow they are useful in many cases.


Hum, panic as in stop all work ?

I like that in Java, units of work can continue after an unhandled exception because you know, sometimes, your software is used by many people in many input variants and say, if you re doing a trading backend, it's bad that you cant fill an order because of a silly parsing bug, but it d be way worse if you had to stop for the day until a dev wakes up and fix it.

Log it, and while you fix it the thing still runs for 99% of inputs. Maybe that s what you call "handling" the error ? But it's cool to bubble up the exception because you can share the handler amongst all your downstreams: you may dislike having to do the same exact semi-complex log building everywhere and having it just capture exception at the top most unit of work dispatcher to catch if one threw something to then log and alert your support team in one place, might make sense.

Ofc return types can do all that but you contaminate your whole program with handling for bugs you cant well predict the nature off... the only certain thing is that if your program is old and big enough, you'll screw up in innovative ways a general catch will allow to recover from, because you just dismiss the whole input and move to the next.


Yes, panic, stop all work. If it's truly an exceptional circumstance, it's unlikely that anything further up can "fix it." Don't even try. Kill the process and restart. Or force the author to fix the bug.

If it's an "expected" runtime condition that you can manage and recover from, then it's not "exceptional", is it? So don't use an exception. Pass the information to the caller that needs it, and adjust state accordingly.

That's my take these days. I've seen too many systems degrade in cascading failures because of misguided attempts to "recover." Deadlocks, partial failures, explosions, etc. Real fun to diagnose.


But what if we need it for critical processes, what if we receive loosely constrained input, what if we want to change it so often that bugs must happen ? (or are you a sort of manager that think bugs can be entirely prevented?)

Why are exceptions supposed to be rare ? They re exceptional in the context of what we told the software could happen, but not in the context we're failible humans pissing code as fast as clients can pay us.

I never had problem to diagnose a corrupted state following an exception, it's pretty clear. It s much harder to tell dozens of clients that there will no trading in Hong Kong this afternoon because one of them sent an illegal character we didnt think to sanitize, or the exchange inverted two messages against their spec, or a network router dropped a packet. All these are cases I ve seen the last few years, we lost one order in each case, kept the million others trading as normal, handled the potential surprise the next release...

Recovery design can be done but you need a strict set of constraints. How do you even recover with a restart after bug fix ? Takes hours just to do, the world has moved on, your states you recovered are useless, you ll sort it the next day ?

Maybe imagine a plane software stopping all work because the human pressed two buttons at the same time and the programmer, a human too, forgot this possibility ? Or am I misunderstanding you ? Maybe you work on more one off things like data science when you re the only person interacting with the inputs and outputs ?


This is exactly the Cocoa exception model: Exceptions are treated as assertions, while errors represent things the program or user should be able to act on and recover from. Swift reifies this by not having exceptions at all, just fatal assertions like precondition() and fatalError(), while using try/throw/do/catch for propagation of errors (and making handling them non-optional).


I would argue that panicking very much makes it somebody else's immediate problem.

By this logic, it kind of seems like no function should ever return errors at all -- handle the situation or panic, right?

Sure, kicking the ball up the chain out of laziness is, well, laziness. But plenty of times, it's done out of the knowledge (or at least hope) that somebody up the chain has a better idea of how to handle the situation than you do.


90s OOP not much but I believe we ran the FP cycle and its ideas have been adopted as much as possible. I think there might be another more mathematical and practical (to cut the verbosity and improve reuse) revival of OO, context/aspect for some bit of meta maybe .. or contracts or multipledispatch.


I'm holding out for the wave of logic / declarative / relational programming. That's my nerd preference.


Oh that could make sense, usually there's the imperative > functional > logic thinking ladder.. and there's scryer, ciao, datalog and a few other logic systems popping up recently.

I'd love that too, i'm very much in love with it.


I am highly bullish on systems programming. As high performance computing becomes more important, so esoteric hardware will. I work in embedded and see some of this, as have my friends in the ML space.

We’ll see the Zigs, Nims and Rusts of the world mature along with special languages for things like tensor processing units. AI will make it much easier for humans to work using those languages.


I used to be a lisp head, sexps and paredit were so neat. Then I had a dan grossman sml mooc, it was the first time a language booted lisp outside my taste. Emacs sml/smie made indentation right. Very neat, I wish it could have a second coming.


Does make me ponder how much work it would be to get a new SML runtime built up overtop of LLVM. Assuming someone hasn't done it already.

Ah, yeah, it's been worked on before, just found a paper: https://people.cs.uchicago.edu/~jhr/papers/2020/ifl-smlnj-ll...


MLTon has an LLVM backend which you can choose by passing a CLI flag. As always though, using LLVM makes the compilation slower.


How different is haskell from sml


I'm learning SML for a college course right now. I'm pretty smittened, I gotta say. It seems strange to me, that it never took off.


The ML languages not taking off are also a mystery to me. F# hits so many sweet spots it makes it hard to find pleasure in other languages unless they offer something drastic like Elixir/Erlang or Prolog.


It's probably because I'm a chickenhead but I couldn't get the signatures/modules/functors thing working properly and found Haskell a wonderfully easy alternative.


I tried to take up Haskell some years after playing with SML/NJ and OCaml, and I just found it ... so hard to read. Not enough syntactic sugar or hints? I wanted to fall for it, but couldn't.

That and the community around it was so enamoured with Deep Intellectual Ponderings and Very Novel Arcanities. That was good, sure, but ... hard.


It's probably the point-free style that's generally used. I'm not very smart so I need to evaluate them and sometimes expand them in my head with variables. But once you start recognizing some patterns it's easier.


Some of us are still using SML for research and teaching, e.g. https://github.com/mpllang/mpl


If you are looking for an ML dialect that has a more 'familiar' syntax, check out Rescript:

https://rescript-lang.org/docs/manual/latest/overview

Although personally I prefer the syntax of F# (but I don't like the .NET focus of it).


Rescript extremely underrated imo. For self-contained projects it's strictly superior to typescript: its type system is simultaneously easier to work with and more sound, with better type inference. The tooling isn't better per se but it's easier to use.

If you have dependencies it depends. Writing bindings to all of them can take up a lot of time. Gentype from typescript types is often enough, but not always.

Hope to see this one catch on a lot more. It's an incredible alternative to typescript.


> For self-contained projects it's strictly superior to typescript: its type system is simultaneously easier to work with and more sound, with better type inference.

Exactly, typescript is a highly complex addition to a relatively messy language (JS).

Rescript feels like what Typescript could have been, a cleanup of JS and a sound typesystem and excellent type inference. (however breaking compatibility as a consequence)

IMO Rescript is easier to read than plain JS and still it's fully typed.


There aren't degrees of soundness, it's either unsound (TypeScript) or sound (Rescript).


I mean zero to one is still "more" right?

Anyway thanks idk shit about type theory and I didn't go to college I just write code for money.


If you like F# but not .NET, Fable might also be worth a look: https://fable.io/


That is exactly the kind of thing I refer to, with guest languages coming up with their alternatives instead of embracing the platform, and in this case F# isn't even a guest language.


Recently I tried to use it until I found out it doesn't support inheritance.

The only option is using the include keyword in modules but that's discouraged.


For those wanting more comparison examples between OCaml and SML, these additional web sites might be of interest.

[1] needs updating with OCaml's newest features, but is still very good.

[2] and [3] are useful to see more examples in those respective languages.

[1] https://hyperpolyglot.org/ml

[2] https://rosettacode.org/wiki/Category:OCaml

[3] https://rosettacode.org/wiki/Category:Standard_ML


Perhaps the wrong place to ask, but, does anyone have a good reference on working with the NJ CM that is more tutorial than the linked manual? Was just getting started with the NJ compiler and was getting frustrated with the REPL. Will it still be as nice as he claims coming from more modern build systems?


I've been using SML with millet language server and VScode.

you can highlight your code and run just the highlighted bits, in your REPL.

https://github.com/azdavis/millet


This looks great. Gonna try it later with the toml file. Thanks!


Interestingly in syntax OCaml is considered purer only once for its choice of not using overloaded operators.


In this comparison, "purer" more often than not means that the choice simplified the formalization of SML.

And for the context, many people from the OCaml side considers that the SML formalization has been for a good part responsible for the freezing of SML since 1997.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

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

Search: