Feature #19432
closedIntroduce a wrapping operator (&) to Proc
Description
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.
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.
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:
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
Updated by rubyFeedback (robert heiler) over 1 year ago
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?
Updated by joel@drapper.me (Joel Drapper) over 1 year ago
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:
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:
strong = -> (&content) { "<strong>" << content.call << "</strong>" }
em = -> (&content) { "<em>" << content.call << "</em>" }
content = -> { "Hello World" }
We can explicitly compose them like this
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.
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.
define_method :foo, method(:bar) & method(:baz)
The corrected implementation would be
class Proc
def &(other)
-> { other.call(&self) }
end
end
Updated by nobu (Nobuyoshi Nakada) over 1 year ago
- Status changed from Open to Feedback
Your first example seems unnecessary complicated.
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)
$ 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.
Updated by joel@drapper.me (Joel Drapper) over 1 year ago
Your examples of strong
and em
return a string but they don't buffer it anywhere. In order to use them like this strong { em { text("Hello") } }
, they would need to buffer the opening tag, yield, and then buffer the closing tag.
Implementation
def strong
@buffer << "<strong>"; yield; @buffer << "</strong>"
end
def em
@buffer << "<em>"; yield; @buffer << "</em>"
end
def text(value)
@buffer << ERB::Escape.html_escape(value)
end
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.
There must be other uses for taking a proc and wrapping it in another proc. <<
and >>
don't do that.