Skip to content

Provide let operators in the standard library#2170

Closed
lpw25 wants to merge 3 commits intoocaml:trunkfrom
lpw25:add-syntax-to-stdlib
Closed

Provide let operators in the standard library#2170
lpw25 wants to merge 3 commits intoocaml:trunkfrom
lpw25:add-syntax-to-stdlib

Conversation

@lpw25
Copy link
Copy Markdown
Contributor

@lpw25 lpw25 commented Nov 28, 2018

This PR provides let operators (see #1947) for List, Option, Result and Seq. These operators are placed in a sub-module Syntax in each of these modules, which also includes a definition of return.

This enables code like:

let find_and_sum k1 k2 t =
  Option.Syntax.(
    let* x1 = Hashtbl.find_opt k1 t in
    let* x2 = Hashtbl.find_opt k2 t in
      return (x + y)
  )

I'm open to other suggestions for the name Syntax. I came up with: Computation, Comp, Expression, Expr and Syntax, from which I liked Syntax the best. I would say that the name should not be based around the word "let" because I suspect that in the future we will want to put other operators in there (e.g. match* from #1955).

Some of the required functions for these operators did not previously exist. None of the modules had their monoidal product and List was missing its bind. Of these I've exposed the product for List and Option, and the bind for List. I didn't expose the product for Result or Seq because I didn't feel people were likely to want to use these functions outside of maybe using them as and operators.

The documentation of the newly exposed functions is very minimal so improvements there are welcome.

Decision queued on:

@nojb
Copy link
Copy Markdown
Contributor

nojb commented Nov 28, 2018

Why not put them directly in the corresponding module, instead of inside a submodule?

As I see it, a submodule is useful when the module in question is typically opened to avoid bringing these operators into the global namespace, but my impression is that one won't typically open these modules.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Nov 28, 2018

As I see it, a submodule is useful when the module in question is typically opened to avoid bringing these operators into the global namespace, but my impression is that one won't typically open these modules.

Submodules are also useful for the reverse situation -- where the submodule is always opened but you don't want to bring in everything in the parent. That is the case here. I literally never want to open List but I need to use open to use these operators.

@nojb
Copy link
Copy Markdown
Contributor

nojb commented Nov 28, 2018

Good point, fair enough.

@Drup
Copy link
Copy Markdown
Contributor

Drup commented Nov 28, 2018

@lpw25 In that case, you could also include the Syntax module in the main one.

Module that contains operators are called Infix in batteries/containers, but it seems we're going to need a new convention...

@dbuenzli
Copy link
Copy Markdown
Contributor

I'm open to other suggestions for the name Syntax.

I think it would be nice to have a name that can be used in general for bringing domain specific operators like specialized comparisons, arithmetic ops, monadic operators and other infix operators into the toplevel scope. I have the impression that you don't want to distinguish between these usages in practice (it would become a bit too bureaucratic) so having a single good name for all of these would be nice.

I'm not saying Syntax is necessarily a bad name but OTOH no new syntax is being defined. I would lean towards Ops which is short for the M.() notation and may prevent a few opens. Other than that with tongue in cheek I'd propose Open -- the module you are allowed to open.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Nov 28, 2018

I think it would be nice to have a name that can be used in general for bringing domain specific operators like specialized comparisons, arithmetic ops, monadic operators and other infix operators into the toplevel scope.

I'm not so sure about this. The let operators and any similar operators like match are of a very specific form. By opening one of these modules you are essentially choosing the kind of underlying computation for this piece of code. I think I prefer to have a name that means providing this specific kind of operation rather than a general provision of operators.

To put this another way, if someone wants to use some let operators for something other than monadic/applicative operations -- nothing immediately springs to mind but I'm sure there are plenty of things people might try -- then I would strongly prefer them not to put them in a Syntax module, since in their case let open Foo.Syntax in has a different underlying meaning.

I'm not sure its completely related, but I also like the parallel between this Bar.Syntax ( expr ) form and the computation expression form async { expr } used in F#. It feels like if you just put any old operators in Syntax then you are losing that parallel in some way.

All this is not to say that I'm against having a standardised name for modules containing all the operators associated with a type (e.g. Ops or Infix), I would just like to separately have a module containing only the "syntactic operators".

@dbuenzli
Copy link
Copy Markdown
Contributor

To put this another way, if someone wants to use some let operators for something other than monadic/applicative operations -- nothing immediately springs to mind but I'm sure there are plenty of things people might try -- then I would strongly prefer them not to put them in a Syntax module, since in their case let open Foo.Syntax in has a different underlying meaning.

I can see your point. However with the current proposal you are attributing a (monadic) meaning to the name Syntax which is a bit odd.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Nov 28, 2018

However with the current proposal you are attributing a (monadic) meaning to the name Syntax which is a bit odd.

True. Maybe a name based on "computation" is better as that is the most commonly used word to cover both monadic and applicative.

@dbuenzli
Copy link
Copy Markdown
Contributor

True. Maybe a name based on "computation" is better as that is the most commonly used word to cover both monadic and applicative.

In order to nurture a bit of confusion I propose either Seq or Haskell.

@pqwy
Copy link
Copy Markdown

pqwy commented Nov 28, 2018

I like Syntax as a convention. It's the module that rebinds the rebindable syntax, after all.

An alternative is to have sub-modules named after the meaning of the exported syntax, like List.Monad and Cmdliner.Applicative. But this strikes me as mouthful and unnecessary. Plus, we have an entire family of rebindable keywords now, and people will use them in various ways, so we end up with M1.Applicative.(let*) and M2.Monad.(and!).

To me, Syntax says "I'm changing the meaning of your syntax, go read the docs". And I like that.

@dbuenzli
Copy link
Copy Markdown
Contributor

To me, Syntax says "I'm changing the meaning of your syntax, go read the docs". And I like that.

But then why not put other operators in such a module ? Something @lpw25 finds undesirable (not sure I'm totally convinced by his point though).

@alainfrisch
Copy link
Copy Markdown
Contributor

  • Even if this adds some more names for existing functions, I think it would be good to expose return and bind (and perhaps product) in the parent modules. This is to support writing in explicitly monadic style, but without forcing to use the new let-syntax or at least the need to use open (e.g. let (let*) = List.bind in let* x = ... in ... .. List.return y).

  • Only exploring the design space: assuming we have directly List.bind and List.return, would it make sense to tell users to open Monad(List) now that this is possible? (Monad would just be a functor renaming some identifiers). Performance would not be great without a good inliner, though.

  • I don't feel fully comfortable with providing features that more or less force people to use open. The next PR that will add any identifier to one of those *.Syntax modules could easily break code because of shadowing. For instance, imagine this PR only proposed the let* form and not let+; people could start using let+ locally for their own purpose, including in contexts that also use the let* from List, and this would break if let+ were added later to List. This would suggest creating smaller sub-modules, which should really not be extended later.

@trefis
Copy link
Copy Markdown
Contributor

trefis commented Nov 28, 2018

For instance, imagine this PR only proposed the let* form and not let+; people could start using let+ locally for their own purpose, including in contexts that also use the let* from List, and this would break if let+ were added later to List.

Seems like these people would just be asking for trouble. Also, it would be very easy for them to fix their code in such a case.

@hcarty
Copy link
Copy Markdown
Member

hcarty commented Nov 28, 2018

Even if this adds some more names for existing functions, I think it would be good to expose return and bind (and perhaps product) in the parent modules. This is to support writing in explicitly monadic style, but without forcing to use the new let-syntax or at least the need to use open (e.g. let (let*) = List.bind in let* x = ... in ... .. List.return y).

I strongly agree with this. Being able to quickly and explicitly bring one or two of these let/match/etc operators into scope would be very helpful for small scale readability. I've found that approach somewhat more readable for new and experienced OCamlers users with the existing monadic syntax ppxs.

@alainfrisch
Copy link
Copy Markdown
Contributor

Seems like these people would just be asking for trouble.

I don't see why. Are you saying that the code will never mix different custom let binders? What about other identifiers? There is already return; could more identifiers be added later? If so, I suppose they will also have rather generic names, likely to clash with user identifiers.

Also, it would be very easy for them to fix their code in such a case.

I find this is a rather weak argument. Things are changing a bit, but we still consider maintaining backward compatibility of the core distribution an important feature, especially when breaking changes cannot be announced in advance with deprecation markers. Even if fixing the code base is easy enough, this requires intervention from the package author and from OPAM package maintainers (patching the code, creating a new release, making sure the previous one is marked as incompatible with the new OCaml, etc). If that libraries happened to be used by other OPAM packages, we can end up with breaking a good part of the eco-system for some time simply because we add more identifiers.

@trefis
Copy link
Copy Markdown
Contributor

trefis commented Nov 29, 2018

Are you saying that the code will never mix different custom let binders?

No, I'm just saying that I think people should explicitly bind their binders when they do that. But that's like, my opinion.

As for the "weak argument", it doesn't seem like much of an argument indeed. It just reflects the fact that I don't mind breaking code that shouldn't have been written in the first place.

But thinking more about it, I'm not sure that discussed style is as bad as I originally thought, and clearly: I have no idea what style people will adopt.
So please, disregard my intervention in this discussion :)

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Nov 29, 2018

Could I suggest not getting too bogged down in discussions on what we would do in the hypothetical situation that we wish to extend the signature of these modules. Yes we should probably be more careful in that situation than we would with other modules, but no its not a disaster if we absolutely had to add more operators since in practice it would probably break approximately no code and would give a nice warning about shadowing for cases where it did. The only cases where I could see us adding more values to these modules is for things like a future (match*) operator, in which case we obviously don't shadow anything in existing code.

Would people prefer it if I moved the return functions out of the Syntax modules? That way it would only be operators inside of them, and we would only be encouraging people to use open where it was strictly necessary.

I think it would be good to expose return and bind (and perhaps product) in the parent modules.

Do you mean e.g. having Seq.bind even though Seq.flat_map exists? I'm happy to do so, but I'd like there to be some consensus (or an executive decision) on how we want these things handled before I make the change.

Only exploring the design space: assuming we have directly List.bind and List.return, would it make sense to tell users to open Monad(List) now that this is possible?

I'm not completely against that approach, but there are some issues:

  1. We would need to check that the resulting operators could still be inlined in non-flambda mode
  2. You cannot do Monad.Syntax(List).( expr ) as that syntax is not currently supported (and may be ambiguous). You would have to use let open Monad.Syntax(List) in ... instead, which is not as nice.

It would also probably require having both of:

Monad.Syntax:
  functor (X : sig type 'a t val bind : ... val product : ... val map : .... end) ->
    sig val ( let* ) : ... val ( and* ) : ... val ( let+ ) : ... val ( and+ ) : ... end
Applicative.Syntax:
  functor (X : sig type 'a t val product : ... val map : .... end) ->
    sig val ( let+ ) : ... val ( and+ ) : ... end

@dbuenzli
Copy link
Copy Markdown
Contributor

Would people prefer it if I moved the return functions out of the Syntax modules?

I think it wouldn't do any harm (it can be added later if it is perceived as missing). I think I would certainly write Ok v rather than return v if I'd use the Result monad. It also give a better local hint which let* are being used assuming someone opened the syntax at the toplevel.


val ( let+ ) : 'a list -> ('a -> 'b) -> 'b list

val ( and+ ) : 'a list -> 'b list -> ('a * 'b) list
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For (and+) and (and* ) at least you must document what operation they are performing. I think that doing it for the let as well would be nice, even though it's clearer from the type.

@gasche
Copy link
Copy Markdown
Member

gasche commented Nov 29, 2018

The submodule discussion is very fun but I think there are other things to discuss as well, among which:

(To me having + for map and * for bind makes some sense, as bind is in some sense a "stronger" version of mapping with a value-dependent result structure/shape/effect.)

¹: I don't know offhand of modules with several distinct Monad structures of interest (I'm sure they exist, and probably examples can be built by lifting the case of numbers with several distinct Monoid structure), but all sequential collections (List, Seq, etc.) have a zipWith-style applicative structure that is distinct from their cartesian-product-esque monad.

My personal feeling is that we are moving a little too fast in solidifying conventions for rebindable-syntax lets. For extensions and attributes, @alainfrisch did a lot of work on case studies, presenting and discussing several use-cases for the feature, and evaluating each of the proposed syntaxes/designs on them. Where are those examples with the proposed operators here? In the #1947 discussion there were some experience reports from ppx_let, but the syntax used there is not exactly the same, and some of the questions need to be handled differently.

This is not directly related to #2170 itself, but personally I would like to see idiomatic real-world code that uses those new binding operators (we should also agree on a name for the feature, I would suggest "binding operator" rather than "let operator" given that and is also involved) with, let's say, list and option as a monad, and zipList and cmdLiner (or a parser, etc.) as applicatives.

In particular, I have the vague intuition that something is wrong with having (and* ) and (and+) defined to be the same thing in all cases. Shouldn't (and+) be more like List.combine than a cartesian product? I can't tell if this is a real issue or just sleep-deprivation speaking; if we had robust examples exercising these features, and not just abstract code with rewriting, we could easily tell for sure.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Dec 3, 2018

If a module has several distinct let-operator structures of interest¹, what's in the Syntax module? All of them?

I would expect individual Syntax modules for individual notions of computation. So for example, I would expect Seq.Syntax to be the normal monadic operations for Seq.t, with a separate Seq.Zip.Syntax for the zipping applicative form if people wanted to expose that.

Is there an implicit decision that * is for monadic operators, while + (or something else?) should be used for Applicative structure? Or is the the idea that + be used with map operators, so for the Functor structure? Is there consensus for that choice?

I've definitely been playing a bit fast and loose with the subtle difference between these two approaches -- refering to let* as monadic and let+ as applicative -- but really what I've implemented here is distinguishing between * for binding and + for mapping. So it would indeed be reasonable for a functor that was not an applicative to implement let+ but not and+.

This is also why I've put in both and+ and and* one is for mapping over a product and the other is for binding to a product. Of course, how you make a product doesn't depend on whether you are going to map over it or bind to it, so they are necessarily the same operation. I also think it looks slightly nicer to always use the same operator symbol for the let and the and.

If we instead said that * was for monadic and + was for applicative then I would not expect to have an and* operator. I would also not expect to see a functor implement let+ but not and+.

we should also agree on a name for the feature, I would suggest "binding operator" rather than "let operator" given that and is also involved

Sounds good.

In particular, I have the vague intuition that something is wrong with having (and* ) and (and+) defined to be the same thing in all cases. Shouldn't (and+) be more like List.combine than a cartesian product?

No. List.combine is the product for a ZipList style applicative. That applicative has no associated monad and also requires having a return that creates an infinite list instead of a singleton. We could expose it as List.Zip.Syntax, but it is definitely a different thing. (I also personally wouldn't expose it because I think the use of infinite lists in OCaml should not be encouraged -- Seq.t is a much more natural type to use for this kind of applicative).

My personal feeling is that we are moving a little too fast in solidifying conventions for rebindable-syntax lets. For extensions and attributes, @alainfrisch did a lot of work on case studies, presenting and discussing several use-cases for the feature, and evaluating each of the proposed syntaxes/designs on them. Where are those examples with the proposed operators here? In the #1947 discussion there were some experience reports from ppx_let, but the syntax used there is not exactly the same, and some of the questions need to be handled differently.

What exactly are you hoping to learn from these examples that you wouldn't learn by looking at existing code using ppx_let? Doing ad-hoc case studies isn't very helpful if you don't have some particular aim in mind or some hypothesis to test. I'm reluctant to spend my spare time manually converting other people's code to use this syntax without some clear objective in mind.

I'm unsure about moving more slowly on conventions. On the one hand, if we wait then perhaps some useful conventions will arise organically from the community and we can adopt them in the stdlib. On the other hand, we might wait and then have every different library using different conventions and find it is too late to try and get some uniformity via the stdlib.

Note that I already put some of the expected conventions into the manual, so if we do intend to wait and see then we might need to make some changes there as well.

@gasche
Copy link
Copy Markdown
Member

gasche commented Dec 3, 2018

I've definitely been playing a bit fast and loose with the subtle difference between these two approaches -- refering to let* as monadic and let+ as applicative -- but really what I've implemented here is distinguishing between * for binding and + for mapping. So it would indeed be reasonable for a functor that was not an applicative to implement let+ but not and+.

You are currently in the process of shaping the look and feel of monadic code (become more and more common) in OCaml for the years to come. I would very much like this look and feel to be explained precisely with appropriate examples and design guidelines. People new to monads should have some chance of following but, maybe even more importantly, people used to monadic or applicative code in their own extensions, or Haskell, or Coq/Agda/Idris, should be able to read whatever documentation and conventions we have and infer how to format their own knowledge/practices within those conventions.

In that respect, I think that being unclear about Functor vs. Applicative is not good, and that the various design choices you are pushing into the standard library should be clearly presented, discussed, and documented.

This is also why I've put in both and+ and and* one is for mapping over a product and the other is for binding to a product.

This discussion style is too abstract. Could you provide natural examples that look right with the current definition of and+? Show them with and* instead? Are there examples that would look nicer if and+ was the zipping one, instead of an alias for and*? (How do I express the zipping product if and+ is cartesian?)

(I also personally wouldn't expose it because I think the use of infinite lists in OCaml should not be encouraged -- Seq.t is a much more natural type to use for this kind of applicative).

Zipping is useful as a modular way to implement mapN operators, and as such it seems fairly common in lists. I've been working recently in parts of the codebase where List.map2 foo (List.combine bar baz) is a common sight.
(We can build a 1-cyclic list, but I think we could also consider defining zipping so that zipping a one-element list with another length results in implicit extension rather than a failure.)

What exactly are you hoping to learn from these examples that you wouldn't learn by looking at existing code using ppx_let? Doing ad-hoc case studies isn't very helpful if you don't have some particular aim in mind or some hypothesis to test. I'm reluctant to spend my spare time manually converting other people's code to use this syntax without some clear objective in mind.

For one, I haven't seen any code using ppx_let. The only natural example I remember seeing is one of cmdliner usage with a custom let%map extension, provided by @diml. Providing URLs to idiomatic ppx_let would be a useful first step.

As a user reading documentation on this feature, here are some questions that I would like to ask:

  • What does this syntactic style look like for monads or idioms that I am already familiar with? The exception monad, the state monad, parser combinators, zipping as an applicative, a web-forms or command-line-parameters applicative library, a concurrency monad. Abstract examples against the Monad or Applicative or Functor interface would be nice, but concrete examples are also necessary for understanding.
  • When let* and let+ are both available, why should I use one instead of the other? Can I see a let*-using example that can be profitably rewritten with let+ or vice-versa?
  • When should I use and* (and/or and+) rather than just a sequence of lets? Is it always useful, or only for some monads? Are the two always equivalent?
  • What do the monad/applicative laws look like with binding operators?
  • As a library author, which of those binding operators should I expose? What is the design rationale behind this choice?

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Dec 3, 2018

Are there examples that would look nicer if and+ was the zipping one, instead of an alias for and*? (How do I express the zipping product if and+ is cartesian?)

The zipping operations are not part of the usual list monad. The relationship between a monad and its applicative is often given as (<*>) = ap in Haskell land. This is equivalent to:

let+ f = f
and+ x = x in
  f x

being equal to:

let* f = f in
let* x = x in
  return (f x)

and clearly does not hold if you make and+ be the zipping operations.

Zipping is useful as a modular way to implement mapN operators, and as such it seems fairly common in lists.

Sure, but that doesn't make zipping a structural part of the list monad. Most useful operations on lists are not the core monadic operations.

(We can build a 1-cyclic list, but I think we could also consider defining zipping so that zipping a one-element list with another length results in implicit extension rather than a failure.)

(That seems pretty dubious to me, why would a list of length 1 be considered infinite but a list of length 2 be considered finite. But either way we're getting off topic here.)

Providing URLs to idiomatic ppx_let would be a useful first step.

As a user reading documentation on this feature, here are some questions that I would like to ask:
[...]
I'll come back to these when I have more time

@aantron
Copy link
Copy Markdown
Contributor

aantron commented Dec 3, 2018

For the submodule name, I suggest Option.Keywords. It's the most immediately self-explanatory and precise name I've come up with so far.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Dec 18, 2018

I think that maybe you could poll the other maintainers to see what they think.

Between asking on caml-devel and asking the other maintainers at Jane Street, I have 4 votes for and*/and+ (including @gasche) vs. my vote for and. I'm happy to take that as a final decision on this part of the proposal unless anyone strongly objects.

This part of the discussion has somewhat drowned out the other parts of this discussion. I think the remaining points to be addressed are:

  1. I should probably remove return from the Syntax modules
  2. Should we name the modules something else?

@trefis
Copy link
Copy Markdown
Contributor

trefis commented Mar 18, 2019

  1. I should probably remove return from the Syntax modules

I'd be in favor, for the same reason as @dbuenzli.

  1. Should we name the modules something else?

I wouldn't, I like Syntax.


At this point it seems like this is not going to make it in 4.08. Does that sound right to everyone?

Finally (though unrelated), it seems like at least some people are very eager to use this syntax: https://discuss.ocaml.org/t/let-syntax-backported-to-ocaml-4-02

@gasche
Copy link
Copy Markdown
Member

gasche commented Mar 18, 2019

At this point it seems like this is not going to make it in 4.08. Does that sound right to everyone?

This sounds reasonable to me: I don't think we have, collectively, taken enough time for discussion on this fairly impactful feature. This could be an issue for our next development meeting.

| Ok _, (Error _ as r2) -> r2
| Ok v1, Ok v2 -> Ok (v1, v2)

(** {1 Syntax module} *)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc comment will be superseded by the interface doc comment, hence it is redundant. Ditto for seq.ml.


let ( let* ) x f = concat_map f x

let ( and* ) a b = product a b
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not let ( and* ) = ( and+ )? For that matter, why not let ( and+ ) = product since parameter order doesn't matter?

@yawaramin
Copy link
Copy Markdown
Contributor

Aside from my nitpicks btw, I really hope this makes it in sooner than later :-)

@avsm
Copy link
Copy Markdown
Member

avsm commented Apr 16, 2020

Developer meeting discussion: too late to push stdlib changes into 4.11 due to imminent branching, but consider this for 4.12.0

@damiendoligez
Copy link
Copy Markdown
Member

ping @ocaml/caml-devel

@hyphenrf
Copy link
Copy Markdown
Contributor

hyphenrf commented Dec 1, 2021

will this make it pre-freeze?

@Octachron
Copy link
Copy Markdown
Member

We are after the freeze for 4.14/5.0, so the next relevant freeze would be the 5.1 one.

@gasche
Copy link
Copy Markdown
Member

gasche commented Feb 6, 2023

Note: we may want to wait until we have made a decision on #10979 to revisit this one, as they may influence the naming decisions we want here.

@lpw25
Copy link
Copy Markdown
Contributor Author

lpw25 commented Feb 6, 2023

Waiting until then seems reasonable. I'm unlikely to put more work into this PR myself, and would be happy for someone else to adopt it.

@yawaramin
Copy link
Copy Markdown
Contributor

yawaramin commented Feb 18, 2023

May I ask why that PR would impact naming decisions in this PR? Isn't the point of choosing uniform letop names like ( let* ), ( let+ ), etc. so that they are the same and thus predictable across all data types in the standard library? Wouldn't it be more of a learning curve to have e.g. let.list, let.option, let.result, and let.seq, and prevent polymorphic usage like functors?

@gasche
Copy link
Copy Markdown
Member

gasche commented Feb 18, 2023

The discussions in #10979 and #12015 are about fundamental perceived limitations with the current syntax of binding operators. We probably want to take the time to understand those before adding binding operators in the standard library (ossifying on a particular approach).

Wouldn't it be more of a learning curve to have e.g. let.list, let.option, let.result, and let.seq

A learning curve for who? For people who already know about monads / are familiar with monadic syntax, consistent use of let* may be better. But for people who don't know monads it may be that let.list, let.option, let.result, let.seq are more readable and more discoverable.

and prevent polymorphic usage like functors?

The current PR as it stands does not make functors convenient either, because it does not introduce modules that pack both a type definition and functor/applicative/monad operations. (And if we wanted to do this, I am personally skeptical that ( and+ ) would be the right name for the operation in the functor, I would suggest pair or product instead.) The "polymorphic usage through functors" use-case is out of scope here.

@emilpriver
Copy link
Copy Markdown

ship it

@yawaramin
Copy link
Copy Markdown
Contributor

If anyone is looking for ready-made letops: https://github.com/yawaramin/letops

@dbuenzli
Copy link
Copy Markdown
Contributor

I think this can be closed.

@lpw25's suggestion has been implemented for Result and Option in #13696 (in 5.4.0) and #13916 (to be in 5.5.0).

The second PR also tried to introduce them for List and Seq but failed on semantic uncertainty see the discussion there.

Except for the discussion which is widely cross-referenced I don't think it's worth keeping this PR open.

@nojb nojb closed this Dec 10, 2025
@nojb
Copy link
Copy Markdown
Contributor

nojb commented Dec 10, 2025

Except for the discussion which is widely cross-referenced I don't think it's worth keeping this PR open.

Indeed, closing PRs which are no longer actionable is good house cleaning. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.