Add infix operators for function composition#2097
Add infix operators for function composition#2097alainfrisch wants to merge 1 commit intoocaml:trunkfrom
Conversation
|
I am not in favour, because experience shows that once such operators are added, they do get used and result in point-free code that is less readable and harder to refactor. (An argument to the converse might be that they get defined sufficiently frequently that they should be factored out into the stdlib, but I'm not sure if that is true.) |
|
Well, many other language with similar syntaxes have those operators, many alternative ocaml stdlibs have them, and I can confirm I add it periodically in my own modules. Let's trust users to decide on their own which style fits best their use cases. (If I was in a better mood, I would start arguing that providing support for better debugging experience is dangerous since it will encourage users not to give enough thoughts to their code upfront.) |
|
I think when designing a library it's important to be at least reasonably opinionated, which will be reflected by some users feeling constrained, and others being guided into what are hopefully idiomatic code patterns. Exactly what stance should be taken with the stdlib I do not know, but we should maybe consider the issue a bit more. (If you are concerned that we should not need to use debuggers, we should surely be striving for particularly legible code whose behaviour is obvious by inspection, no?) |
|
@alainfrisch Well, my own concern is that the point regarding generalization is still pretty valid. On the other hand, this problem on its own is more than enough to doom pointfree style in OCaml, and users will have to learn about generalization at some point in their OCaml life, so it's not so problematic. |
dbuenzli
left a comment
There was a problem hiding this comment.
Personally I think it's a good idea to have a function composition operator in a functional programming language.
However one thing I think needs to be considered is if we really need two of them. I think one is sufficient, it will make code reading more regular and put less operator burden on the reader's mind.
I personally would only go with the ( % ) operator of the proposal which follows the f o g mathematical notation and the natural reading order of f (g x)
Postfix notation is common in some domains, for instance for applying syntactic substitutions as in tσ (https://en.wikipedia.org/wiki/Substitution_(logic) even says "Applying that substitution to a term t is written in postfix notation"). Moreover, the pipe operator |
c2f807a to
ce04834
Compare
|
BTW, general note: I understand the desire not to encourage certain sorts of things, but if people are doing them anyway, let there at least be a standard notation for them to make it easier to read people's code when you come in cold. One can discourage the pointless, er, point-free style in other ways, including just plain explaining that it's not a good idea. |
FWIW, I don't believe that point-free is a bad idea in all contexts. Would @dbuenzli 's proposal for Bool.negate in #2010 be considered as point-free programming? I think so, and I don't think it's a bad style. I've some good use cases in my code base, and the fact that other similar languages and alternative stdlibs offer the operators should be an indication that point-free style is not universally bad. Next step would be to add disclaimers on ( := ) because, uh, imperative style is not very OCaml-idiomatic; and then on |
It clearly isn't a bad idea in all contexts. |
|
Very happy to see this: point-free style can be very clarifying in the right places. I also agree with @dbuenzli that we should only have |
|
Existing stdlib alternatives expose the two functions, and so do F# and Elm. I don't think F#, Elm, Containers and Batteries are widely considered as encouraging people to write unreadable code. Anyway, at least for the code I have in mind in my own code base, it would really feel weird to have only f1
|> (%) f2
|> (%) f3than: f3
% f2
% f1because the former shows more clearly the sequencing of operations when reading the code in the natural direction (similarly to f1
%> f2
%> f3I can see the argument that So let's accept both version and trust people to not make a soup of unreadable code with them! |
|
I also like having the two versions. In my experience they are useful in different places; depending on the domain one is more readable than the other. There are also domains of maths (typically category theory) where I often use the inversed-composition order (often written |
|
Let me put that this way: I prefer to have both of them rather than none or only Note that my concern was not about people writing unreadable point free code, my concern is always about how much definitions you need to keep in your mind when you read code (something I care about due to my own limitations). One last argument that was not made is that infix operators is a scarce ressource in OCaml and some space should be kept for end-users and their combinator libraries (counter of course is the @alainfrisch should we move on to remove |
|
@dbuenzli I agree entirely about the number of definitions. The problem with the composition operators is that you'll probably end up wanting both versions. However consider then what happens when you're reviewing a really important piece of code, which perhaps is of one of the forms in @alainfrisch 's examples above. What happens in my case is that I would want to be extra certain the composition was the one in the correct order, so I'd probably go and check the definition of the operator and do some more thinking, whereas the behaviour would have been immediately obvious if the operator had not been used in the first place. Even if composition operators are added, I don't think functions that are useful in their absence should be removed. |
Yes, I'd say so, if you're fine with
It doesn't sound crazy to me to deliberately forbid the use of the new operators (or just one of them) in some code bases or some specific parts which you are in charge of reviewing. During internal code reviews, I regularly ask PR authors to avoid some features (such as imperative features, or even some operators such as I've a lot of sympathy for arguments about the cognitive burden with operators, as I was initially quite reluctant to the use of |
|
The reasons for having both operators make sense to me now, and I completely agree that having blessed operators is better than having people define them ad hoc. |
|
I'm not 100% sure what the right answer is here. We've evaluated this question in Base/Core and decided not to include such operators, but we might of course be wrong. But @@ is a good example of the pitfalls here. I think adding @@ has made things worse, because its presence in the standard library increases its use, and I think its use is largely pernicious. My worry is that % and %> will do the same. Another thing one can do is to add a sub-module with these operators, so that people can open that module when they want it in their namespace. We've in fact done this internally as a way of sharing a standard choice about naming operators, without having them by default in the ambient environment. |
|
I'm not sure why |
|
@gasche : I know how you hate to participate to stdlib discussions, but since you are the only maintainer who expressed a positive opinion on this PR, do you think you'd like to overrule other negative opinions and merge this PR? Otherwise we can close this PR and wait for someone else to propose the same thing (so that we will have two maintainers in favor). :-/ |
|
I'm not sure about the two-maintainer rule: it seems to me that any change to the language (including the standard library) should be given enough time to be sure that no problem (or better solution) has been overlooked. This of course depends a bit on how far we are in the release cycle. Independently of that, I'm rather supportive of having composition operators (and both of them, as I do not see one being better than the other), for the sake of uniformity. Yet, I would follow @yminsky in suggesting new operators be in a submodule, so that one has to declare he is using them. Anything added to |
|
I'm happy to officially support the proposal and the choice of having two operators. @dbuenzli and @mshinwell have expressed their preference for having only one operator, but I didn't sense a very strong position from their comments, more like a personal preference/recommendation, and I think we can overrule them. I have no strong opinion on having a sub-module for the infix operators. I'm fine either way. On the other hand, I would be firmly against moving I looked at your implementation, and I don't like the 7/11 reference in the .mli comment (not only because of the risk of trademark dispute from the convenience store chain). Nobody knows what the numbers of precedence levels are, and I think that characterizing these levels by numbers would be a mistake in the first place (that OCaml currently avoids). One cannot understand this comment without looking at the precedence table, and if we are looking at it anyway the comment is useless. I personally don't think we need to be explicit about documenting precedence (here or for other operators). We could refer readers to the manual section on OCaml lexical conventions (but we have to be careful to do this in a future-proof way by not giving (sub)section numbers). If we want to be explicit about precedence, I would just say that it is "at the same level as Edit: we moved the discussion on precedence levels to a more appropriate thread. |
In case this what not obvious, my remark was a half-joke about the asymmetry that external contributions are sometimes easier to get in than PRs from maintainers. Of course there is no rush and we should give enough time to discuss these extensions.
I've no strong opinion here. If we add some namespacing, what about adding a
No, I don't think this is possible. The best one could do is mark those as being deprecated (and probably keep them forever, the cost of removing them will always be higher than keeping them). |
This was added following a reviewer request so as to be coherent with existing docstrings in Stdlib. Are you suggesting we should remove all such references, or just not add them for the new operators discussed here? |
|
@gasche I didn't express a preference for only having one operator, I expressed a preference for having no operators. :) If they must be introduced, I think I would prefer to see them in some separate namespace as @alainfrisch and @yminsky suggest. |
|
(Also, I don't understand why the precedence of operators should not be documented precisely.) |
|
For reference, precedence information was added in #1167. I tend to agree with @gasche , but may I kindly suggest that we keep the discussion for elsewhere -- e.g. comments on #1167, or a new Mantis ticket, or new PR if a solution is clear enough. For the present PR, we should only aim at being consistent with existing declarations; migrating to a different documentation style later will not be more costly. |
|
Sorry, I missed that there was a GPR for this change. I'll go discuss that there and remove my previous message here. Thanks! |
|
(Found the problem, I forgot to add the new files...) |
45ebe22 to
979a702
Compare
stdlib/fun.mli
Outdated
|
|
||
| module Ops : sig | ||
| val ( %< ) : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b | ||
| (** Function composition: [f % g] is equivalent to [fun x -> f (g x)]. |
6368df4 to
5673b30
Compare
stdlib/fun.mli
Outdated
|
|
||
|
|
||
| val ( %> ) : ('a -> 'b) -> ('b -> 'c) -> 'a -> 'c | ||
| (** Function reverse-composition: [f %> g] is equivalent to [fun x -> g (f x)]. |
There was a problem hiding this comment.
Travis is failing because this line is over 80 columns.
There was a problem hiding this comment.
Btw. it would be nice to have a toplevel target like make pr-check and be documented here that runs most of these CI checks to avoid too many suffering round trips with the CI.
5673b30 to
2047b14
Compare
| \end{tabular} | ||
| \end{latexonly} | ||
|
|
||
| \ifouthtml |
There was a problem hiding this comment.
Two lists of modules follow this directive you should add the Fun module there too in alphabetic order (this is unfortunately not checked by the check manual script /cc @Octachron)
There was a problem hiding this comment.
Thanks! One should add a guide on how to add modules to the stdlib (e.g. in CONTRIBUTING.md, section "Contributing to the standard library"), since this requires advanced skills .
There was a problem hiding this comment.
Actually there are some instructions here but they seem to be incomplete.
There was a problem hiding this comment.
The check could be updated, but an improved documentation sounds like a better first step. I will have a look.
2047b14 to
239a3db
Compare
dbuenzli
left a comment
There was a problem hiding this comment.
Technically the PR looks correct.
|
Thanks @dbuenzli! @garrigue @damiendoligez : I think you were the two maintainers opposed to the solution without the sub-module. Are you happy with the current state? |
|
Ok, we had a caml-devel meeting today, and there appears to be a rather widespread but silent support for having the operator in the default scope. (@xavierleroy mentions "operators in the default scope" as hi solution number 1, and "operators in a sub-module" as solution number 2 -- but of course he counts from 0). @damiendoligez does not seem completely closed at the idea of actually doing that, contrary to my interpretation of his reaction here, and @garrigue opposition also seemed not too strong. So perhaps there is some hope to have the operators in the default scope. I'll let @damiendoligez make the final decision! |
|
Thanks for the report(s) @alainfrisch. However now that you have done the work I'd still suggest to keep the |
|
Ok, to be clear, you're suggesting to move the operators to Stdlib, but still keep an empty Fun module in this PR, in order to make it easier to move |
|
Yes. |
|
@alainfrisch Since I wasn't at the meeting yesterday (not having expected topics such as this to be discussed), I'm going to restate my opposition to having the operators in the global scope. I honestly find your statement about "gratuitous humiliation" above a bit unhelpful. I don't think either myself or anyone else in favour of namespacing using submodules for operators is doing so to humiliate people. I am in favour of it, if we must have the operators, mainly because I have a genuine concern that these operators will otherwise become a de facto part of programming in OCaml. They can easily decrease legibility of code if applied indiscriminately, just like the current situation with The argument that all operators must go in the global scope doesn't seem scalable to me, even notwithstanding the above. The only reasonable solution I can see for the long term is some kind of namespacing with users being able to choose what they want to use. One other point about the namespacing is that if we start out with the operators in a submodule and there is then clamour from the community for them to be in the default scope, that can always be done, whereas the reverse is much more problematic. What is @xavierleroy 's solution number zero? |
@mshinwell This is a bit OT but as far as I read above nobody actually described these "pernicious" uses except for labelling them as such. The only material things I read is @alainfrisch saying he was against gratuitous uses of it (something that also holds for I think judicious touches of |
|
@dbuenzli it's not really about the mnemonic. |
Give up on this proposal and keep having no function composition operators in the standard library. The feature is not very useful and we're unable to reach consensus. |
This is also my preferred solution and @mshinwell's too. |
This was actually discussed during lunch... |
|
@damiendoligez As said, the final word is for you. Can you either close this PR, merge it in its current state, or say if you are ok with operators at the toplevel? |
|
At this point, having converged on |
|
To give credit to the idea that it might still be a good idea to have these names somewhere in the stdlib (to be clear I don't mind the Long before |
|
Ok, let's be realistic: no maintainer would click on Merge with Damien, Xavier and Mark being against the proposal. No need to make this last any longer. |
This PR extends Stdlib with infix operators for function composition:
[EDIT: now the operators are called
%<and%>, and put in a submodule Stdlib.Fun.Ops]This addition is not intended to particularly encourage point-free programming, but to support it for cases people find it useful and to provide standard names for operations which people would expect to find in the Stdlib.
The same functions (with those names) exist in:
F# and Elm expose those operators under different names (<< and >>).
The question about the existence of infix operators is regularly raised on SO:
In #2010, it is proposed to add a
Bool.negate : ('a -> bool) -> ('a -> bool)to support point-free programming on "Boolean predicates". With the composition operators,Bool.negate predcould directly be written as(not % pred)or(pred %> not).(@pierreweis expressed some concerns about composition operators, but this was 20 years ago, and I'm not sure the arguments hold.)