Project

General

Profile

Actions

Feature #19432

closed

Introduce a wrapping operator (&) to Proc

Added by joel@drapper.me (Joel Drapper) almost 2 years ago. Updated almost 2 years ago.

Status:
Feedback
Assignee:
-
Target version:
-
[ruby-core:112333]

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) almost 2 years 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) almost 2 years 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) almost 2 years 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) almost 2 years 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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0