OCaml Syntax Sucks

(xahlee.info)

82 points | by Qem 6 days ago ago

50 comments

  • wk_end 5 hours ago

    The title is a little inflammatory. The critique is specifically about Ocaml’s handling of let-bindings. AFAICT OP thinks the syntax sucks because:

    1. there’s no marker to indicate the end of let scopes

    2. functions are bound with the same syntax as constants

    He asserts that this is confusing. In practice - for the many issues I have with Ocaml! - neither of these are actual issues, in my experience, once code formatting is applied.

    An actual serious problem with Ocaml’s syntax is that matches don’t have a terminator, leading people to mess up nested matches frequently. Pair that with the parser’s poor error reporting/recovery and things can become unpleasant quickly.

    • pyrale 3 minutes ago

      > functions are bound with the same syntax as constants

      Apparently, the author hasn't come around to understanding that functions are just another constant.

    • nine_k 4 hours ago

      Fortunately, the OCaml compiler is very modular, and there have been efforts to make things more... reasonable.

      - Reason, a different syntactic frontend for regular OCaml: https://reasonml.github.io/

      - ReScript, a language with OCaml semantics that compiles into: JS https://rescript-lang.org/ (I suppose it's a reincarnation of js-of-ocaml).

      • JoelJacobson an hour ago

        I love the idea of Reason/ReScript. I hope they can figure out a way to work together and join forces somehow, the contributions to both repos seems to have faded over the last years, but maybe that's because the projects have stabilized, I don't know.

        I've had lots of fun playing with Reason a few years ago. I created an interactive real-time visualization tool, to see a Regexp transform into a NFA to DFA to minimal DFA graph: http://compiler.org/reason-re-nfa/src/index.html It only works for basic regexes though.

        • danielstocks an hour ago

          I can’t speak for Reason but the ReScript project and community is very alive and vibrant. There’s been some major improvements over the past year and overall it’s much more appealing and mature now compared with only a few years ago. We’ve been using it in a fairly large React app for a while and the experience has been very good.

      • jey 2 hours ago

        ReScript is better described as a descendant (and fork) of ReasonML aimed to fit into the JS ecosystem. In contrast to js_of_ocaml, ReScript prioritizes interoperability with existing JS code and APIs over interop with existing OCaml code, whereas js_of_ocaml takes the opposite approach. So people looking for an improved version of JavaScript or TypeScript should probably choose ReScript, but people who are porting an existing OCaml program might prefer js_of_ocaml.

      • BrawnyBadger53 2 hours ago

        I feel like ReasonML should have the capacity to help bridge the gap for people learning ocaml if it weren't so hidden.

    • soraminazuki 23 minutes ago

      > The title is a little inflammatory

      I think you’re being generous. The example the author gave is awful because any language can be made illegible if you cram in complicated expressions with multiple levels of nesting into a single line. I’d say it’s outright flamebait.

    • seanhunter 2 hours ago

      Totally agree. In particular when I read #2 I was really scratching my head. It's a functional language - the thing on the left of the equals sign is a pattern. Apart from the argument you pass, patterns for functions look similar to patterns for constants precisely because in a functional language everything looks like a function. And everything looks like a function because just about everything is a function.

      The match terminator end thing made me sad when I first saw this in Ocaml. So many languages (C, bourne shell, etc etc) have this exact same problem and it completely sucks in all of them. It's more debilitating in a functional language specifically because matches are more useful than say C case statements so you want to use them much more extensively.

      I frequently want to do a pattern match to unpack something and then a further pattern match to unpack further - a nested match is a very intuitive thing to want. Yes you can normally unnest such a match into more complicated matches at the parent level but this is usually much harder for humans to understand.

      ...and if you had a marker for ending match scopes you could always just reuse that to end let scopes as well if you wanted to although I've literally never a single time run into that as a practical problem (although I haven't written that much OCaml you'd think if it was a real issue I would have banged into it at least once because I found a fair few sharp edges in my time with the language).

    • remexre an hour ago

      i agree an autoformatter alleviates the let decl/expr in practice, especially for an experienced user; an autoformatter also fixes nested matches too ime.

      however, my university has a mandatory class taught in ocaml, which i've ta'd for a few times; this is the _number one_ "the undergrad ta couldn't figure out my syntax error" issue students have

  • laylomo2 4 hours ago

    The more I used ocaml the more I found beauty in the syntax. It’s very ergonomic in many ways:

    1. It’s whitespace insensitive, which means I can code something up really messy and the code formatted will automatically fix it up for me.

    2. In general there aren’t a ton of punctuation characters that are very common, which is great for typing ergonomics. Don’t get me wrong, there are still a lot of symbols, but I feel compared to some languages such as Rust, they’re used a lot less.

    Beyond the syntax, there are a couple of things I really like about the language itself:

    1. Due to the way the language is scoped, whenever you encounter a variable you don’t recognize, you simply have to search in the up direction to find its definition, unless it’s explicitly marked as “rec”. This is helpful if you’re browsing code without any IDE tooling, there’s less guessing involved in finding where things are defined. Downside: if the “open” keyword is used to put all of a module’s values in scope, you’re usually gonna have a bad time.

    2. The core language is very simple; in general there are three kinds of things that matter: values, types, and modules. All values have a type, and all values and types are defined in modules.

    3. It’s very easy to nest let bindings in order to help localize the scope of intermediate values.

    4. It has a very fast compiler with separate compilation. The dev cycle is usually very tight (oftentimes practically instantaneous).

    5. Most of the language encourages good practice through sane defaults, but accessing escape hatches to do “dirty” things is very easy to do.

    6. The compiler has some restrictions which may seem arcane, such as the value restriction and weak type variables, but they are valuable in preventing you from shooting yourself in the foot, and they enable some other useful features of the language such as local mutation.

    • bloomingkales 2 hours ago

      2. In general there aren’t a ton of punctuation characters that are very common, which is great for typing ergonomics. Don’t get me wrong, there are still a lot of symbols, but I feel compared to some languages such as Rust, they’re used a lot less.

      I never really seen someone put that into words. I always feel a certain kind of weird when I look at a language with tons of punctuation (Typescript is good example).

      • akkad33 43 minutes ago

        The lambda syntax and default lambda parameters alone make typescript very hard to parse

  • user2342 an hour ago

    Weak arguments in the article with badly chosen examples.

    If one wanted to criticize OCaml syntax, the need for .mli-files (with different syntax for function signatures) and the rather clunky module/signature syntax would be better candidates.

    • _flux 30 minutes ago

      I actually rather like the mli-files. It's a nice file to read, with the documentation and externally available symbols only. However, the fact that the syntax is so different is a bit annoying.

      Sometimes I wrote (haven't written OCaml for some time now..) functions like:

          let foo: int -> int = fun x ->
            ..
      
      just to make them more similar to the syntax

          val foo: int -> int
      
      in the module types.
  • the_clarence 5 hours ago

    Worked on an ocaml codebase for two years. My advice is: choose a different language. It's just not a great dev experience in general

    • otabdeveloper4 2 hours ago

      > Worked on an X language codebase for two years. My advice is: choose a different language. It's just not a great dev experience in general.

      I'm pretty sure it's "working on a codebase" that kills your soul, not the minutae of particular language choice.

    • eru 4 hours ago

      Depends on what you compare it with, I guess. (I worked in OCaml and Haskell and other 'weird' languages professionally in different jobs for many years.)

      • akkad33 42 minutes ago

        Fsharp is pretty good in teams f tooling at least when compared to other functional languages

  • akkad33 4 hours ago

    F# syntax improves on this I think. But why is the article so short? It ends abruptly

  • shortrounddev2 6 days ago

    I want to like OCaml, but the tooling isn't great and async operations require a library to work for some reason. I tried f# but if you want to do async operations there, you have to do them in these even weirder "computation blocks" with this annoying ! Syntax. I've found that the best way to write ML family programs is to let an imperative language handle IO and then write any more mathematically or logically complicated work in ML, but only after you've loaded all of your data

    • gabcoh 4 hours ago

      > async operations require a library to work for some reason

      Rephrased: ocaml is so flexible that async can be implemented as a library with no special support from the language.

      This is the beauty of ocaml (and strongly typed functional languages more broadly)

      • eru 4 hours ago

        > This is the beauty of ocaml (and strongly typed functional languages more broadly)

        I don't think that's anything specific to strongly typed functional languages. In eg Rust even the standard library relies on third party crates.

        Though it is still somewhat amusing to me that loops in Haskell are delivered via a third party library, if you ever actually want them. See https://hackage.haskell.org/package/monad-loops-0.4.3/docs/C...

        I do agree that it's good language design, if you can deliver what would be core functionality via a library.

        Whether you want to integrate that library into the standard library or not is an independent question of culture and convenience.

        (Eg Python does quite well with its batteries-included approach, but if they had better dependency management, using third party libraries wouldn't be so bad. It works well in eg Rust and Haskell.)

      • koito17 3 hours ago

        As the other commenter pointed out, this isn't restricted to strongly-typed functional languages.

        Clojure has core.async, which implements "goroutines" without any special support from the language. In fact, the `go` macro[1] is a compiler in disguise: transforming code into SSA form then constructing a state machine to deal with the "yield" points of async code. [2]

        core.async runs on both Clojure and ClojureScript (i.e. both JVM and JavaScript). So in some sense, ClojureScript had something like Golang's concurrency well before ES6 was published.

        [1] https://github.com/clojure/core.async/blob/master/src/main/c...

        [2] https://github.com/clojure/core.async/blob/master/src/main/c...

        • Blackthorn 3 hours ago

          > something like Golang's concurrency

          That's wildly overselling it. Closure core async was completely incapable of doing the one extremely important innovation that made goroutines powerful: blocking.

          • conjurernix 2 hours ago

            Can you elaborate? As far as I'm aware if you pull from an empty nchannel it wikl be blocking ubtik it gets a value.

    • neonsunset 6 days ago

      Some F# articles are outdated - they predate F# 6 which added task { } CE which simplifies asynchronous code that interacts with .NET's standard library.

      I found F# to be more pleasant to work with async than C# (which is already a breeze). It is true that you still have to define 'task' (or 'async' if you want to use Async CEs) but it is generally there for a reason. I don't think it's too much noise:

          let printAfter (s: float<second>) = task {
              let time = TimeSpan.FromSeconds (float s)
              do! Task.Delay time
              printfn $"Hello from F# after {s} seconds"
          }
      • shortrounddev2 6 days ago

        I dislike that there's a kind of sub-syntax specifically for async. I like how C# converts `await` into the necessary calls. In this code I think it would look better to simply have:

            let async printAfter (s: float<second>) =
                let time = TimeSpan.FromSeconds (float s)
                await Task.Delay time
                printfn $"Hello from F# after {s} seconds"
        
        and then printAfter is called with `await` as well. I'm sure there's some FP kind of philosophy which prohibits this (code with potential side effects not being properly quarantined), but to me it just results in yet more purpose-specific syntax to have to learn for F#, which is already very heavy on the number of keywords and operators
        • neonsunset 5 days ago

          It's the other way around.

          'async'-annotated methods in C# enable 'await'ing on task-shaped types. It is bespoke and async-specific. There is nothing wrong with it but it's necessary to acknowledge this limitation.

          let!, and!, return!, etc. keywords in F# are generic - you can build your own state machines/coroutines with resumable code, you can author completely custom logic with CEs. I'm not sure what led you to believe the opposite. `await Task.Delay` in C# is `do! Task.Delay` in F#. `let! response = http.SendAsync` is for asynchronous calls that return a value rather than unit.

          In the same vein, seq is another CE that is more capable than iterator methods with yield return:

              let values = seq {
                  // yield individual values
                  for i in 1..10 -> i
                  // yield a range, merged into the sequence
                  yield! [11..20] // note the exclamation mark
              }
          
          Adding support for this in C# would require explicit compiler changes. CEs are generic and very powerful at building execution blocks with fine control over the behavior, DSLs and more.

          Reference: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...

          • shakna 5 hours ago

            > It is bespoke and async-specific. There is nothing wrong with it but it's necessary to acknowledge this limitation.

            I would disagree. If you need to have a bespoke set of syntax, then something is not integrated where it should be. The language design should not be such that you are writing things differently, depending on the paradigm that you're handling. That's not something that occurs in every language, so it isn't essential that it exists.

            We can acknowledge the differences in a way that alerts the programmer, without forcing the programmer to switch syntaxes back and forth when moving between the paradigms. async/await is one method, Promises another, etc. A different syntax is a much, much higher cognitive load.

            • Vetch 2 hours ago

              F#'s computation expressions are closely related to Haskell's monads + do-notation combo, CEs are both more limited than Haskell's approach to monads (from a type expressibility perspective) and more expressive than pure monads (from a modeling perspective, can model a general class of computational structures beyond monads; CE's also share F#'s syntax, with extensible semantics). This notation can be advantageous and clarifying when used in the right places. It has advantages over C#'s async from a flexibility/extensibility perspective and also provides more options in orchestrating more complex control flow across async computations. C#'s approach is more streamlined if you only care about using async according to how Tasks are designed (which still enable a quite broad scope) and don't need the flexibility for other computational patterns.

              Simple things like the maybe and either monad are often clearer in this notation. Complex things like alternatives to async (such as CSP derived message passing concurrency), continuations, parser combinators, non-determinism, algebraic effects and dependency tracked incremental computations are naturally modeled with this same machinery, with CE notation being a kind of super helpful DSL builder that makes certain complex computations easier to express in a sequenced manner.

              If the custom syntax was only for async you'd have a point, but the general power of the framework make it the more preferable approach by far, in my opinion.

              • shakna 40 minutes ago

                However, most of the industry has moved away from DSLs. Whilst having a unique language can make certain things more expressive, having something standard makes mistakes happen less, and increases the effectiveness of a programmer. Lisp doesn't rule our day to day.

                We shoehorn things that feel like, but are structurally different, to DSLs into config files and the like, using JSON/YAML/etc in rough ways, because DSLs introduce a cognitive overhead that doesn't need to be there.

                That the shoehorn happens, does mean that DSLs are something natural to reach for. You're right there. But that we have moved away, as an industry, indicates that using any kind of DSL is a smell. That there probably is a better way to do it.

                Having a core language feature using a DSL, is a smell. It could be done better.

            • neonsunset 3 hours ago

              I cannot make sense of this reply. Different languages have different syntax.

              Support of asynchronous code and of its composition is central to C#, which is why it does it via async/await and Task<T> (and other Task-shaped types). Many other languages considered this important enough to adopt a similar structure to their own rendition of concurrency primitives, inspired by C# either directly or indirectly. Feel free to take issue with the designers of these languages if you have to.

              F#, where async originates from, just happens to be more "powerful" as befits an FP language, where resumable code and CEs enable expressing async in a more generalized fashion. I'm not sold on idea that C# needs CEs. It already has sufficient complexity and good balance of expressiveness.

              • shakna an hour ago

                Different languages have different syntax, but most do not have a separate syntax inside themselves. A function is generally a function. They do adopt various structures - but those are structures, not syntax. I'm not sure you've understood that was my point.

                • neonsunset a minute ago

                  Perhaps it's a good idea to get familiar with the details of why and how async is to be approached in C# and F# before continuing the conversation.

          • shortrounddev2 4 days ago

            I'm somewhat already aware of these considerations, it's just that when you're working in web development, a huge amount of your code is async and this means that a large part of the code is wrapped up in these computation expressions that I think are just plain ugly

            • miffy900 3 hours ago

              What's so ugly about them? I don't code in F#, but I do in C# and after reading about them I wish C# had something similar.

    • lmm 4 hours ago

      You might like Scala. It has much of the good parts of OCaml or F#, but also lets you write imperative code freely when you want. The `for`/`yield` syntax for async is very nice IMO, or you can write Javascript-like promise chaining directly if you want.

      • switchbak 2 hours ago

        Some interesting things happening in the structured concurrency / "Direct style" space. It looks like it could become a powerful and readable way to compose (asyncy type) things. Simpler code, usable stack traces, better traceability, less function colouring concerns.

        It's early days in that regard, with some folks doing some really interesting things: Odersky himself / the Ox project.

  • VeejayRampay 13 minutes ago

    flamebait tantrum of a post

  • asplake 2 hours ago

    (2016)

  • cess11 an hour ago

    My OCaml is very rusty, but just add some parentheses, no?

  • lemper 4 hours ago

    idk mate, his guide to pick prostitutes has more compelling argument compared to this article. i mean, almost all the time ocaml users don't write their stuff "nested".

  • anacrolix 2 hours ago

    I find it less abutting now that I know about let expressions in lambda calculus. it's still verbose though. do notation please.

  • KennyBlanken 3 hours ago

    As a matter of policy can HN please not accept submissions with non-https URLs?

    It's a checkbox at most web hosts, built in to many reverse proxies, etc. There's no excuse for not offering htttps, particularly since it places users at risk if at any point along the path between them and you, there's someone untrustworthy.

    • otabdeveloper4 2 hours ago

      > places users at risk

      Help me out here, what's the threat model here while reading troll programming blogs?