Reason - general function syntax discussion

I don’t care much about Reason (as I’m not going to use it anyway) - their arguments against currying is more like justifying Baby Duck Syndrome of JS programmers.
Some of their arguments is just not true - especially one about performance - you either create a closure using currying, or you create the same closure using explicit function definition. There is no performance gains in the second case, you just lose readability for no reason.
But I’m surprised that even experienced OCaml developers (@lpw25, for example) believes that currying should be avoided. Having non-curried data constructors is one of my biggest pain points when using OCaml, i.e. it forces you to write

fold (fun x a -> x :: a)
map (fun x -> Some x) l

instead of just

fold (::)
map Some l
1 Like

For what it’s worth, I’m in full agreement with Leo here: starting from scratch, I’d avoid partial application as the default way of building multi-argument functions. But, given the language as it is, making the syntax try to hide that fact seems awkward. I can see how it would help beginners; indeed, when I explain how OCaml works to new people, I don’t explain the partial application story at first.

In the above, don’t confused currying with partial application. Even a language without currying could (and I think should) support a concise syntax for partial application.

By the way, I’m surprised to see Fred Bour’s (@let-def) suggestion that partial application is inefficient to compile as it stands. I thought that was once true, but has basically been fixed. That said, Fred knows the innards of the compiler better than I do, so he’s probably right.

y

1 Like

if we design language from scratch, there should be a syntax to tell full application from partial application. FYI BuckleScript added uncurried calling convention support but it’s not perfect. @Alex, there is perf cost for sure

1 Like

Please elaborate. I’m unaware of any difference in compilation scheme between currying and partial application.

Naive compilation of unary-argument functions had a non-negligible cost at runtime (it took time to discover efficient compilations scheme for ML).

The solution retained in OCaml is to handle efficiently multi-argument functions and run static analyses to optimize direct calls – that is breaking abstraction for the sake of performance, which is quite exceptional in the OCaml compiler.

http://pauillac.inria.fr/~xleroy/talks/zam-kazam05.pdf gives a more detailed overview.

In practice, the costs are:

  • in dynamic calls, handling under, exact and over applications,
  • in closure representation, having one trampoline function address (caml_apply, tuple*, curry) and one more word for direct call address.

So it has slight cost over having direct support for multi-argument functions, which can be optimized in first order code at the cost of some memory (one word per closure) & complexity in the runtime…

*: the nice side effect of this analysis is that application of tuples which are immediately deconstructed is handled more efficiently.

3 Likes

Isn’t all that mandatory as soon as you have high order functions (which can return functions) and type abstraction (where you can hide functions behind abstract types), regardless of easy access to partial application and/or currified functions?

In any cases, the performance cost is not that big (and doesn’t come up in regular OCaml programming) and it’s a bit late to worry about the implementation complexity cost now that everyone uses those features. The discussion is interesting for new languages, but that’s not the case here. Implying those costs are a good argument for syntax changes is a bit misleading.

My personal worry is that it makes some classes of libraries very painful to use. Notably combinator-based libraries, which are quite common in the OCaml ecosystem (cmdliner, re, fmt, angstrom, …). We already have that problem a little bit in OCaml, where high-flying module manipulation is horribly verbose, and the consequence is that this is never used.

1 Like

Exactly. Currying is just convenient method to create new functions from existing. You can’t have your cake (first-class functions) and eat it (don’t deal with closures and under-exact-over dispatching) too.

Sorry, I don’t see how it breaks abstraction (which one?). Curried functions works the same way whether you optimize it or not. And static analysis of saturated calls is very simple and well studied.

Note also that most of the time simple partially-applied functions will be inlined so there is no overhead at all.

Isn’t all that mandatory as soon as you have high order functions (which can return functions) and type abstraction (where you can hide functions behind abstract types), regardless of easy access to partial application and/or currified functions?

The question is about unary vs n-ary functions. If you have n-ary functions, this information is given by the type signature, you don’t have to break abstraction access it, so it works as well in higher-order functions and is not affected by abstract types (in this context, a binary function is not an unary function).

In any cases, the performance cost is not that big (and doesn’t come up in regular OCaml programming) and it’s a bit late to worry about the implementation complexity cost now that everyone uses those features. The discussion is interesting for new languages, but that’s not the case here. Implying those costs are a good argument for syntax changes is a bit misleading.

OCaml is in local optimum: it has to deal with that, and it does it quite well. This it not going to change.

But the language we are talking about is not OCaml. In Reason thread, I started the confusion by giving some possible semantics arguments to the syntax changes, even though I noted that these arguments don’t apply in Reason case because we cannot change the underlying semantics.

The point was just to show that there is more than syntax in this application story and that OCaml veterans opinions are not clearly in favor of unary functions, for reasons which are not immediately obvious.

My personal worry is that it makes some classes of libraries very painful to use. Notably combinator-based libraries, which are quite common in the OCaml ecosystem (cmdliner, fmt, angstrom, …).

That claim is not supported. For average code (not combinator heavy), feedbacks have been largely positive, and we didn’t try anything with combinator heavy code. This is something that should be done but I would refrain from drawing conclusions too early.

Sorry, I don’t see how it breaks abstraction (which one?). Curried functions works the same way whether you optimize it or not. And static analysis of saturated calls is very simple and well studied.

The compiler has too look inside values (function bodies) to determine what to do to generate efficient code. The information locally available (path and type) are not enough, so OCaml compiler artifacts (cmx files) contain extra information.

This is not surprising and is not a bad thing, most compiler optimizations are about breaking abstraction boundaries. However the overall design of the OCaml language gives performance characteristics that are quite obvious by looking at the source code, but the case of function application is a bit more complex.

This way you are just forcing programmers to deal with closures by itself for the sake of compiler simplicity.

1 Like

Please stop spreading FUD. Claiming that the absense of such a useful and fundamental concept like a closures is not painful is just like Go developers saying that generics is not needed.
Having non-curried constructors in OCaml is painful. Having non-curried functions will be even more painful. But of course, if you never seen anything better than JS, your feedback will be “largely positive”.

1 Like

That is true (although you could replace “compiler simplicity” but “predictability of generated code”, the same argument that justifies having to write List.map (fun x -> Some x) instead of List.map Some).

However, I expect occurrences to be quite rare:

  1. in this hypothetical framework, one designs with n-ary functions in mind, not unary functions
  2. syntax doesn’t have to be as heavy as (fun x -> Some x), Some(_) can be enough. And with this small overhead, you gain support for partial applications in arbitrary positions, your problem with Jane Street ordering of arguments goes away.
  3. the code that would be affected the most are the ones that use partial applications because they really rely on f x being different from fun y -> f x y… And that would be a good thing.

Can you stop behaving like an asshole? You realize your behavior is unnecessarily aggressive? So tone down a bit.

5 Likes

@Alex second warning on this thread about your use of language in multiple posts since my original comment. You are welcome to post comments and technical questions, but find somewhere else on the Internet for ad hominem attacks against other language communities. I’d appreciate it if you and @let-def could go back and edit your posts so that the technical points remain for other viewers of this thread.

11 Likes

That’s actually my point. I did not mean to draw any conclusions. The github thread mostly centers around how convenient and easy to understand the new syntax is to non-OCaml programmers, but the question of whether some of the (useful!) OCaml idioms are still easy to express seems equally important to me, and I’m curious of the results.

2 Likes

Maybe irrelevant to say this two weeks after the discussion ended, but one contributing factor to wanting to start using OCaml in addition to Clojure (recent primary language) was the simplicity of partial application in OCaml. In Clojure partial application uses an additional function partial, e.g.

(def f (partial nth [1 2 3 4]))

which is approximately equivalent to

let f = at [1;2;3;4]

using Batteries.List.at.

As a JS dev, I really don’t like the idea of trying to make it look more like Javascript. I thought the favorite thing Javascript devs like to do is complain about javascript. And most of those complaints are about syntax. And now we are carrying it over to another language…

I believe that python is increasingly being used as a language to teach new students because it is easy to read. Maybe we should be taking pointers from languages like python and ruby and not Javascript.

To add, I look to Reason or another language because I don’t want to use Javascript anymore. The js-like syntax makes me want to keep looking.

3 Likes

I’m not a JS dev, but I dislike JS syntax as well.

I also find that making languages more similar to each other makes it harder for me to separate them and remember their differences. I wouldn’t be surprised if, after programming in Reason for a while, it’d be hard to switch back to JS.

Never fear! If you don’t like Reason’s syntax, the ordinary OCaml syntax is always at your disposal.

y

9 Likes

Indeed, the interop story is very well done, you can use anything written in Reason in OCaml syntax… but it comes with a price in UX (or DX?).

I have had the chance of exploring Bucklescript for the last few weeks, and I tried to stay with the OCaml syntax as it lets me familiarize myself to do OCaml backend stuff too.

From what I’ve seen, the whole Bucklescript community (which is most active on Discord) seems to lean more to Reason. There are many libraries and bindings written in or migrated to Reason. Everyone keeps saying that it is essentially OCaml, just with a different syntax—I’d like to argue that the different syntax bit is definitely not a just. That is, I think it causes problems. There are two reasons:

Context switching. You’re a beginner writing in OCaml. You got the basics worked out and doing well with your Bucklescript project. Then you need a library to do something, and when you explore Github you found that it’s written in Reason… It’s great if the lib author provides examples for both flavor, but if it’s only in Reason then mostly you’re on your own. Those little things like swapped type positions ('a list vs. list 'a, Js.Json.t Js.Dict.t Js.Promise.t vs. the backwards version), different notations altogether (< .. > vs {..}, < foo : string; .. > vs {.. foo : string}, etc.). Those little damages might slow you down from actually getting things done. You end up copying and pasting stuff into the reason-tools extension every now and then.

Codebase inconsistencies. Other than syntax difference, Reason also has different conventions, one of them being identifiers using camelCase instead of the usual OCaml’s snake_case. In my experience, consistency across codebase is one of the major point of a great DX when navigating a project. When you interop, you’re intermingling both conventions in one place. In JS we even have linter rules for making sure these kinds of consistencies are enforced… Also, when I write a lib, should the API expose camel case functions? If I use snake case function, would Reason devs feel excluded? A bit exaggerated indeed, but I’d definitely prefer not to think about these things if possible.

I think these are even experienced by those who use Reason too, since Bucklescript’s manual is still in OCaml. There are talks to rewrite their manual in a more accessible way, and if they did rewrite it, I really hope they’d stick with OCaml syntax, or at least provide both flavor… although I realize that would mean more maintenance effort.

All in all, if I do Bucklescript stuff in OCaml, I can’t help but feel kind of like a douche that doesn’t want to “fit into” the community by consciously using different syntax :smile:

Note: not a jab at the community, though! I’ve been helped numerous times on Discord and I am very grateful for that.

2 Likes