[ruby-core:112333] [Ruby master Feature#19432] Introduce a wrapping operator (&) to Proc

Issue #19432 has been reported by joel@drapper.me (Joel Drapper). ---------------------------------------- Feature #19432: Introduce a wrapping operator (&) to Proc https://bugs.ruby-lang.org/issues/19432 * Author: joel@drapper.me (Joel Drapper) * Status: Open * Priority: Normal ---------------------------------------- I don't know if this concept exists under another name, or whether there’s a technical term for it. I often find myself wanting to wrap a proc in another proc. Here's a snippet from a recent example where a visitor class renders a TipTap AST. Given a `text` node, we want to output the text by calling the `text` method with the value. But if the text has `marks`, we want to iterate through each mark, wrapping the output for each mark. It's possible to do this using the `>>=` operator if each proc explicitly returns another proc. ```ruby when "text" result = -> { -> { text node["text"] } } node["marks"]&.each do |mark| case mark["type"] when "bold" result >>= -> (r) { -> { strong { r.call } } } when "italic" result >>= -> (r) { -> { em { r.call } } } end end result.call.call end ``` This is quite difficult to follow and the `result.call.call` feels wrong. I think the concept of wrapping one proc in another proc would make for a great addition to `Proc` itself. I prototyped this using the `&` operator. ```ruby class Proc def &(other) -> { other.call(self) } end end ``` With this definition, we can call `&` on the original proc with our other proc to return a new proc that calls the other proc with the original proc as an argument. It also works with `&=`, so the above code can be refactored to this: ```ruby when "text" result = -> { text node["text"] } node["marks"]&.each do |mark| case mark["type"] when "bold" result &= -> (r) { strong { r.call } } when "italic" result &= -> (r) { em { r.call } } end end result.call end ``` -- https://bugs.ruby-lang.org/

Issue #19432 has been updated by rubyFeedback (mark potter). I don't think I have ever run into a situation where I had to use .call.call. Are there examples where that is required? ---------------------------------------- Feature #19432: Introduce a wrapping operator (&) to Proc https://bugs.ruby-lang.org/issues/19432#change-101800 * Author: joel@drapper.me (Joel Drapper) * Status: Open * Priority: Normal ---------------------------------------- I don't know if this concept exists under another name, or whether there’s a technical term for it. I often find myself wanting to wrap a proc in another proc. Here's a snippet from a recent example where a visitor class renders a TipTap AST. Given a `text` node, we want to output the text by calling the `text` method with the value. But if the text has `marks`, we want to iterate through each mark, wrapping the output for each mark. It's possible to do this using the `>>=` operator if each proc explicitly returns another proc. ```ruby when "text" result = -> { -> { text node["text"] } } node["marks"]&.each do |mark| case mark["type"] when "bold" result >>= -> (r) { -> { strong { r.call } } } when "italic" result >>= -> (r) { -> { em { r.call } } } end end result.call.call end ``` This is quite difficult to follow and the `result.call.call` feels wrong. I think the concept of wrapping one proc in another proc would make for a great addition to `Proc` itself. I prototyped this using the `&` operator. ```ruby class Proc def &(other) -> { other.call(self) } end end ``` With this definition, we can call `&` on the original proc with our other proc to return a new proc that calls the other proc with the original proc as an argument. It also works with `&=`, so the above code can be refactored to this: ```ruby when "text" result = -> { text node["text"] } node["marks"]&.each do |mark| case mark["type"] when "bold" result &= -> (r) { strong { r.call } } when "italic" result &= -> (r) { em { r.call } } end end result.call end ``` -- https://bugs.ruby-lang.org/

Issue #19432 has been updated by joel@drapper.me (Joel Drapper). Yes, I included an example in the original description. If we need to wrap a Proc in another Proc, there doesn't seem to be a clean way to do that. My first thought was to use this pattern: ```ruby when "text" result = -> { text node["text"] } node["marks"].each do |mark| case mark["type"] when "bold" result = -> { strong { result.call } } when "italic" result = -> { em { result.call } } end end result.call end ``` However, this raises a stack level to deep error. We have the `<<` and `>>` operators to compose Procs such that their return value is the input of the next Proc. We also have currying to transform a Proc with multiple arguments into a sequence of single-argument Procs. What we don't have is the ability to compose two or more Procs where one Proc wraps the other Proc. Here’s a simple example. Given these Proc: ```ruby strong = -> (&content) { "<strong>" << content.call << "</strong>" } em = -> (&content) { "<em>" << content.call << "</em>" } content = -> { "Hello World" } ``` We can explicitly compose them like this ```ruby strong.call { em.call { content.call } } ``` But if we started with the `content` and wanted to layer on `strong` and `em` conditionally, there isn't a clean way to do that. The `&` operator would allow for compositions like this ``` (content & strong & em).call ``` Or processes that transform a result iteratively. ```ruby result = content result &= strong if bold? result &= em if italic? result.call ``` Correcting my original post, I think it would be better if `&` coerced `self` into a block when calling the other Proc, since then it could be used to compose methods that accept blocks, e.g. ```ruby define_method :foo, method(:bar) & method(:baz) ``` The corrected implementation would be ```ruby class Proc def &(other) -> { other.call(&self) } end end ``` ---------------------------------------- Feature #19432: Introduce a wrapping operator (&) to Proc https://bugs.ruby-lang.org/issues/19432#change-101809 * Author: joel@drapper.me (Joel Drapper) * Status: Open * Priority: Normal ---------------------------------------- I don't know if this concept exists under another name, or whether there’s a technical term for it. I often find myself wanting to wrap a proc in another proc. Here's a snippet from a recent example where a visitor class renders a TipTap AST. Given a `text` node, we want to output the text by calling the `text` method with the value. But if the text has `marks`, we want to iterate through each mark, wrapping the output for each mark. It's possible to do this using the `>>=` operator if each proc explicitly returns another proc. ```ruby when "text" result = -> { -> { text node["text"] } } node["marks"]&.each do |mark| case mark["type"] when "bold" result >>= -> (r) { -> { strong { r.call } } } when "italic" result >>= -> (r) { -> { em { r.call } } } end end result.call.call end ``` This is quite difficult to follow and the `result.call.call` feels wrong. I think the concept of wrapping one proc in another proc would make for a great addition to `Proc` itself. I prototyped this using the `&` operator. ```ruby class Proc def &(other) -> { other.call(self) } end end ``` With this definition, we can call `&` on the original proc with our other proc to return a new proc that calls the other proc with the original proc as an argument. It also works with `&=`, so the above code can be refactored to this: ```ruby when "text" result = -> { text node["text"] } node["marks"]&.each do |mark| case mark["type"] when "bold" result &= -> (r) { strong { r.call } } when "italic" result &= -> (r) { em { r.call } } end end result.call end ``` -- https://bugs.ruby-lang.org/

Issue #19432 has been updated by nobu (Nobuyoshi Nakada). Status changed from Open to Feedback Your first example seems unnecessary complicated. ```ruby def text(s) "<text>#{s}</text>" end def strong(s) "<strong>#{s}</strong>" end def em(s) "<em>#{s}</em>" end def markup(s, *marks) result = -> { text s } marks.each do |mark| case mark when "bold" result >>= ->(s) { strong(s) } when "italic" result >>= method(:em) end end result.call end puts markup(*ARGV) ``` ```shell-session $ ruby feature-19432.rb test <text>test</text> $ ruby feature-19432.rb test bold <strong><text>test</text></strong> $ ruby feature-19432.rb test italic <em><text>test</text></em> $ ruby feature-19432.rb test bold italic <em><strong><text>test</text></strong></em> $ ruby feature-19432.rb test italic bold <strong><em><text>test</text></em></strong> ``` Anyway, I can't get the reason `Proc#<<` and `Proc#>>` are not enough. ---------------------------------------- Feature #19432: Introduce a wrapping operator (&) to Proc https://bugs.ruby-lang.org/issues/19432#change-101833 * Author: joel@drapper.me (Joel Drapper) * Status: Feedback * Priority: Normal ---------------------------------------- I don't know if this concept exists under another name, or whether there’s a technical term for it. I often find myself wanting to wrap a proc in another proc. Here's a snippet from a recent example where a visitor class renders a TipTap AST. Given a `text` node, we want to output the text by calling the `text` method with the value. But if the text has `marks`, we want to iterate through each mark, wrapping the output for each mark. It's possible to do this using the `>>=` operator if each proc explicitly returns another proc. ```ruby when "text" result = -> { -> { text node["text"] } } node["marks"]&.each do |mark| case mark["type"] when "bold" result >>= -> (r) { -> { strong { r.call } } } when "italic" result >>= -> (r) { -> { em { r.call } } } end end result.call.call end ``` This is quite difficult to follow and the `result.call.call` feels wrong. I think the concept of wrapping one proc in another proc would make for a great addition to `Proc` itself. I prototyped this using the `&` operator. ```ruby class Proc def &(other) -> { other.call(self) } end end ``` With this definition, we can call `&` on the original proc with our other proc to return a new proc that calls the other proc with the original proc as an argument. It also works with `&=`, so the above code can be refactored to this: ```ruby when "text" result = -> { text node["text"] } node["marks"]&.each do |mark| case mark["type"] when "bold" result &= -> (r) { strong { r.call } } when "italic" result &= -> (r) { em { r.call } } end end result.call end ``` -- https://bugs.ruby-lang.org/

Issue #19432 has been updated by joel@drapper.me (Joel Drapper). Your examples of, `strong` and `em` return a string but they don't buffer it anywhere. In order to use them like this, they would need to buffer the opening tag, yield, and then buffer the closing tag. ```ruby strong { em { text("Hello") } } ``` **Implementation** ```ruby def strong = @buffer << "<strong>"; yield; @buffer << "</strong>" def em = @buffer << "<em>"; yield; @buffer << "</em>" def text(value) = @buffer << ERB::Escape.html_escape(value) ``` Given this interface, if you were visiting an AST (from Markdown or an editor like TipTap), it would be useful to have an operator for Proc that wraps one proc in another, as I showed in the original post. ---------------------------------------- Feature #19432: Introduce a wrapping operator (&) to Proc https://bugs.ruby-lang.org/issues/19432#change-102201 * Author: joel@drapper.me (Joel Drapper) * Status: Feedback * Priority: Normal ---------------------------------------- I don't know if this concept exists under another name, or whether there’s a technical term for it. I often find myself wanting to wrap a proc in another proc. Here's a snippet from a recent example where a visitor class renders a TipTap AST. Given a `text` node, we want to output the text by calling the `text` method with the value. But if the text has `marks`, we want to iterate through each mark, wrapping the output for each mark. It's possible to do this using the `>>=` operator if each proc explicitly returns another proc. ```ruby when "text" result = -> { -> { text node["text"] } } node["marks"]&.each do |mark| case mark["type"] when "bold" result >>= -> (r) { -> { strong { r.call } } } when "italic" result >>= -> (r) { -> { em { r.call } } } end end result.call.call end ``` This is quite difficult to follow and the `result.call.call` feels wrong. I think the concept of wrapping one proc in another proc would make for a great addition to `Proc` itself. I prototyped this using the `&` operator. ```ruby class Proc def &(other) -> { other.call(self) } end end ``` With this definition, we can call `&` on the original proc with our other proc to return a new proc that calls the other proc with the original proc as an argument. It also works with `&=`, so the above code can be refactored to this: ```ruby when "text" result = -> { text node["text"] } node["marks"]&.each do |mark| case mark["type"] when "bold" result &= -> (r) { strong { r.call } } when "italic" result &= -> (r) { em { r.call } } end end result.call end ``` -- https://bugs.ruby-lang.org/
participants (4)
-
joel@drapper.me (Joel Drapper)
-
joel@drapper.me (Joel Drapper)
-
nobu (Nobuyoshi Nakada)
-
rubyFeedback (mark potter)