{"action":"created","issue":{"url":"https://api.github.com/repos/ocaml/ocaml/issues/1947","repository_url":"https://api.github.com/repos/ocaml/ocaml","labels_url":"https://api.github.com/repos/ocaml/ocaml/issues/1947/labels{/name}","comments_url":"https://api.github.com/repos/ocaml/ocaml/issues/1947/comments","events_url":"https://api.github.com/repos/ocaml/ocaml/issues/1947/events","html_url":"https://github.com/ocaml/ocaml/pull/1947","id":346167131,"node_id":"MDExOlB1bGxSZXF1ZXN0MjA1MDk2NzIy","number":1947,"title":"WIP: Add \"monadic\" let operators","user":{"login":"lpw25","id":2631210,"node_id":"MDQ6VXNlcjI2MzEyMTA=","avatar_url":"https://avatars2.githubusercontent.com/u/2631210?v=4","gravatar_id":"","url":"https://api.github.com/users/lpw25","html_url":"https://github.com/lpw25","followers_url":"https://api.github.com/users/lpw25/followers","following_url":"https://api.github.com/users/lpw25/following{/other_user}","gists_url":"https://api.github.com/users/lpw25/gists{/gist_id}","starred_url":"https://api.github.com/users/lpw25/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lpw25/subscriptions","organizations_url":"https://api.github.com/users/lpw25/orgs","repos_url":"https://api.github.com/users/lpw25/repos","events_url":"https://api.github.com/users/lpw25/events{/privacy}","received_events_url":"https://api.github.com/users/lpw25/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":39,"created_at":"2018-07-31T12:33:08Z","updated_at":"2018-08-16T15:52:37Z","closed_at":null,"author_association":"CONTRIBUTOR","pull_request":{"url":"https://api.github.com/repos/ocaml/ocaml/pulls/1947","html_url":"https://github.com/ocaml/ocaml/pull/1947","diff_url":"https://github.com/ocaml/ocaml/pull/1947.diff","patch_url":"https://github.com/ocaml/ocaml/pull/1947.patch"},"body":"Based on a few recent comments and PRs, I thought it might be a good time to revive the idea of adding some support for \"monadic\" syntax to OCaml. I can't find the last attempt at this on GitHub or Mantis, but like last time the idea is to allow people to define \"let\" operators like `let*` and `let+`. The general form of the operators is `let` followed by a sequence of infix operator characters. (Note that this deliberately excludes `let!` -- the existence of `open!` and `method!` would make that too confusing).\r\n\r\n# Outline\r\n\r\nThis proposal aims to be fully general whilst making it easy to implement the common cases. In particular, it focuses on supporting functors, applicatives and monads easily. The translation chosen is a generalisation of the one used by [ppx_let](https://github.com/janestreet/ppx_let). Essentially, given a let binding like:\r\n\r\n```ocaml\r\nlet+ x = A\r\nand++ y = B\r\nand+++ z = C in\r\n  expr\r\n```\r\n\r\nit is translated directly to:\r\n\r\n```ocaml\r\nlet def = A\r\nand def2 = B\r\nand def3 = C\r\nand func = fun ((x, y), z) -> expr\r\nand (bind, single) = (let+) in\r\nbind func ((and+++) ((and++) (single def) def2) def3)\r\n```\r\n\r\nThis translation essentially treats the input to a let binding as a pair of a tagged list of bindings and a function. The `bind` function handles pairing the bindings with the function, whilst the `single`, `and++` and `and+++` functions create the list of bindings. The `bind` and `single` functions are combined to form the `(let+)` value.\r\n\r\n# Example\r\n\r\nFor a monad, I suggest the following approach to using these let operators, using `list` as an example:\r\n\r\n```ocaml\r\nlet id x = x\r\n\r\nmodule List = struct\r\n\r\n  type 'a t = 'a list\r\n\r\n  let map = List.map\r\n\r\n  let bind f l =\r\n    let l = List.map f l in\r\n    List.concat l\r\n\r\n  let product xs ys =\r\n    List.fold_right\r\n      (fun x acc -> List.map (fun y -> x, y) ys @ acc)\r\n      xs []\r\n\r\n  let (let+) = map, id\r\n  let (and+) = product\r\n  let ( let* ) = bind, id\r\n  let ( and* ) = product\r\n\r\nend\r\n```\r\n\r\nSo we use `let+` for map, `let*` for bind and set both the `and`s to the monoidal product. This gives the expected behaviours:\r\n\r\n```ocaml\r\n# let map =\r\n     List.(\r\n       let+ x = [1; 2; 3] in\r\n       x + 1\r\n     );;\r\nval map : int list = [2; 3; 4]\r\n\r\n# let map_and =\r\n     List.(\r\n       let+ x = [1; 2; 3]\r\n       and+ y = [7; 8; 9] in\r\n       x + y\r\n     );;\r\nval map_and : int list = [8; 9; 10; 9; 10; 11; 10; 11; 12]\r\n\r\n# let bind =\r\n     List.(\r\n       let* x = [1; 2; 3] in\r\n       let* y = [7; 8; 9] in\r\n       [x + y]\r\n     );;\r\nval bind : int list = [8; 9; 10; 9; 10; 11; 10; 11; 12]\r\n\r\n# let bind_and =\r\n     List.(\r\n       let* x = [1; 2; 3]\r\n       and* y = [7; 8; 9] in\r\n       [x + y]\r\n     );;\r\nval bind_and : int list = [8; 9; 10; 9; 10; 11; 10; 11; 12]\r\n\r\n# let bind_map =\r\n     List.(\r\n       let* x = [1; 2; 3] in\r\n       let+ y = [7; 8; 9] in\r\n       x + y\r\n     );;\r\nval bind_map : int list = [8; 9; 10; 9; 10; 11; 10; 11; 12]\r\n```\r\n\r\n(I've never found list to be a particularly useful monad and I think it makes these examples a bit less compelling than if they were using a more useful monad like `lwt` or `async`, however list is the simplest monad that makes clear how these operators work so I used it anyway).\r\n\r\nObserve that this approach aligns cleanly for the functor and applicative cases: for functors we can define `let+`, for applicatives we can define `let+` and `and+`, and for monads we can define all four operators. This also naturally supports monads for which the applicative operations are more efficient than the equivalent monadic operations -- i.e. where `let+ ... and+ ...` is more efficient than `let* ... let+ ...`. \r\n\r\n# Differences from `ppx_let`\r\n\r\nFor comparision, the `ppx_let` translation essentially takes:\r\n\r\n```ocaml\r\nlet+ x = A\r\nand y = B\r\nand z = C in\r\n  expr\r\n```\r\n\r\nand translates it to:\r\n\r\n```ocaml\r\nlet def = A\r\nand def2 = B\r\nand def3 = C\r\nand func = fun ((x, y), z) -> expr\r\nand (bind, and_) = (let+) in\r\nbind func (and_ (and_ def def2) def3)\r\n```\r\n\r\nA key difference between the translation in this PR and the one used by `ppx_let` is the addition of the `single` part of the `let` operators. Note that it was always `id` in the example above. One reason for this addition is that with some monads it is more efficient to gather together multiple computations and bind/map them all at once rather than to merge them together using their monoidal product and then bind/map that -- one notable example is `Deferred.t` in the async library. Having the `single` combinator allows the type of the argument to `bind` to differ from the type of the things being bound.\r\n\r\nAnother difference from `ppx_let` is the support for mixing different `let` and `and` operators together. This is partly to allow the direct alignment from operators to the functor, applicative and monad use cases -- as described above. It also makes the construct more general allowing, for example, a computation that can bind on multiple sorts of things to mix them together in a single `let ... and ...` construct.\r\n\r\n# `match` operators\r\n\r\nThese have now been split into #1955\r\n\r\n# Open questions\r\n\r\n- Currently the implementation is entirely as a translation in the parser, much as other operators are. I generally prefer to avoid elaboration and instead to give things their own AST nodes, but I wanted to keep its implementation in line with other operators for this initial attempt. Should I change the implementation before we merge? Should I give all operators there own AST nodes -- that would certainly make `pprintast.ml`s job a lot easier?\r\n\r\n- Is there any reasonable way to support qualified versions of these operators? By this I mean allowing you to use `List.(let*)` without opening `List`. I personally think it will be quite difficult to find an aesthetic syntax, and I think that much of the value of these operators does not require it. Once we have implicits this will be much less of an issue.\r\n\r\n- Currently, the `let*` is a single token from the lexer. Should it instead be two separate tokens?\r\n\r\n- How should this be integrated into the standard library? My personal preference is not to do any such integration yet, and to wait for the arrival of implicits before adding generic support for monads etc."},"comment":{"url":"https://api.github.com/repos/ocaml/ocaml/issues/comments/413594027","html_url":"https://github.com/ocaml/ocaml/pull/1947#issuecomment-413594027","issue_url":"https://api.github.com/repos/ocaml/ocaml/issues/1947","id":413594027,"node_id":"MDEyOklzc3VlQ29tbWVudDQxMzU5NDAyNw==","user":{"login":"Drup","id":801124,"node_id":"MDQ6VXNlcjgwMTEyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/801124?v=4","gravatar_id":"","url":"https://api.github.com/users/Drup","html_url":"https://github.com/Drup","followers_url":"https://api.github.com/users/Drup/followers","following_url":"https://api.github.com/users/Drup/following{/other_user}","gists_url":"https://api.github.com/users/Drup/gists{/gist_id}","starred_url":"https://api.github.com/users/Drup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Drup/subscriptions","organizations_url":"https://api.github.com/users/Drup/orgs","repos_url":"https://api.github.com/users/Drup/repos","events_url":"https://api.github.com/users/Drup/events{/privacy}","received_events_url":"https://api.github.com/users/Drup/received_events","type":"User","site_admin":false},"created_at":"2018-08-16T15:52:37Z","updated_at":"2018-08-16T15:52:37Z","author_association":"CONTRIBUTOR","body":"I have the same impression about this PR than when I'm visiting a touristic market in a country where I don't speak the language. Surely I'm getting ripped off, but I'm not sure how and how much just yet.\r\nWith the current version of this PR, I feel like it suits JST's coding style very well, but we are going to need to change it completely in a few release when we see how it's used by the rest of the user base.\r\n\r\n1. The basic idea of this PR is not that much about monads, it's about reifying the `(let)` operator in the language, so it can be defined internally, without macros, ppx, metaprogramming, or other fancy things. Monads are a very good excuse for this, but it's only part of the story, as shown by the various additions in `ppx_let` and lwt's ppx. That's why this PR introduces some fancy things to also reify `(and)` and `(match)` in https://github.com/ocaml/ocaml/pull/1955 . \r\n  \r\n   So, why stop there ? If we start reifying syntactic constructs in the language, we might as well do it properly, and reify all the core language operators. Almost all of them can be desugared to lambda-calculus, similar to let in this PR. Lwt's ppx has `while` and `for`, @yminsky wants `if`, I'm sure a lot of people will want `let rec` for very amusing things with monads and lazyness. Both from a consistency and use-case point of view, there is really no reason to stop at `let` (and the do notation in haskell also does a lot more). \r\n   I realize @xavierleroy really wants this to be about textbook monads, and I understand him, but usage already shows that people will want more and having half the language constructs but not the others sounds quite annoying from a language consistency point of view. Amusingly, we made the same mistakes with the ppx shortcut `let%ppx`, which was only on `let` in the initial version because, surely, the rest would be useless. ;)\r\n\r\n2. The fact that `let` and `let*` have different typing rules (one generalize, the other doesn't) is really problematic from an ergonomics point of view. Users are not going to expect such fundamental difference in behavior (especially since most users don't understand generalization very well, but expect it implicitly).\r\n\r\n\r\n3. The current implementation that does parser-level transformation is, in my opinion, fairly distasteful. It also shows that maybe this shouldn't be in the language in a first step, and we should simply relax the grammar, and let ppx do things that needs to be done. We would then let things evolve a bit more until we are really sure all the elimination forms are the right one, and then we can integrate them properly.\r\n"}}
{"actor":{"display_login":"Drup"}}