3

I'm learning about the unary operator, &.

There are some great questions about using & in the parameters of a method invocation. Usually the format goes something like some_obj.some_method(&:symbol):

It seems like the main idea is ruby calls the to_proc method on :symbol when the unary operator is placed in front of the symbol. Because Symbol#to_proc exists "everything works".

I'm still confused about how everything just works.

What if I want to implement a "to_proc sort of functionality with a string". I'm putting it in quotes because I'm not really sure how to even talk about what I'm trying to do.

But the goal is to write a String#to_proc method such that the following works:

class String
  def to_proc # some args?
    Proc.new do
      # some code?
    end
  end
end
p result = [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]

This is how I did it:

class String
  def to_proc
    Proc.new do |some_arg|
      parts = self.split(/ /)
      some_proc = parts.first.to_sym.to_proc
      another_arg = parts.last.to_i
      some_proc.call(some_arg, another_arg)
    end
  end
end
p result = [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]

The main part I'm confused about is how I get the parameters into the String#to_proc method. It seems like:

def to_proc
  Proc.new do |some_arg| ...
end

Should be:

def to_proc some_arg
  Proc.new do |yet_another_arg| ...
end

Or something like that. How do the [2, 4, 6, 8] values get into the proc that String#to_proc returns?

Community
  • 1
  • 1
mbigras
  • 7,664
  • 11
  • 50
  • 111

4 Answers4

4

Just write this

[2, 4, 6, 8].map { |each| each.to_s(2) }

Though I guess that is not what you're looking for …

Here is how Symbol#to_proc is implemented.

class Symbol
  def to_proc
    proc { |each| each.send(self) }
  end
end

If you want you can define to_proc on an Array as follows

class Array
  def to_proc
    symbol, *args = self
    proc { |each| each.send(symbol, *args) }
  end
end

And then use

[2, 4, 6, 8].map(&[:to_s, 2])

Another alternative is using curry.

Though that does not work with bound methods, so you'll have to define a to_s lambda function first.

to_s = lambda { |n, each| each.to_s(n) }

[2, 4, 6, 8].map(&to_s.curry[2])

Though all of that seems more like academic exercises.

akuhn
  • 27,477
  • 2
  • 76
  • 91
3

When you run some_method(&some_obj), Ruby first call the some_obj.to_proc to get a proc, then it "converts" that proc to a block and passes that block to some_method. So how the arguments go into the proc depends on how some_method passes arguments to the block.

For example, as you defined String#to_proc, which returns a proc{|arg| ...} (a proc with one argument), and calls [...].map(&'to_s 2'), Ruby interprets it as

[...].map(&('to_s 2'.to_proc))

which is

[...].map(&proc{|arg| ... })

and finally

[...].map {|arg| ... }
Aetherus
  • 8,720
  • 1
  • 22
  • 36
2

The problem with your approach is that there's no way to deduce the type of the argument when it's always passed as a string.

By the way, to address your question:

How do the [2, 4, 6, 8] values get into the proc that String#to_proc returns?

They are some_arg here, which is not a variable you have to define but instead is a parameter that is automatically passed when the proc is called.

Here's a rewriting of the String patch and some usage examples:

class String
  def to_proc
    fn, *args = split ' '
    ->(obj) { obj.send(fn.to_sym, *args) }
  end
end

This works for the following example:

p result = [[1,2,3]].map(&"join -")
# => ['1-2-3']

but fails for this (your example):

p result = [2, 4, 6, 8].map(&'to_s 2')
# => TypeError

The problem is to_s('2') is being called, when the 2 should be an integer, not a string. I can't think of any way to get around this except for maybe some serialization (although one of the other answers shows how eval can work).

Now that the limitations of this approach are clear, it's worth comparing it to the more commonly used patch on Symbol to enable argument passing to proc shorthands (this taken from can-you-supply-arguments-to-the-mapmethod-syntax-in-ruby)

class Symbol
  def call(*args, &block)
    ->(caller, *rest) { caller.send(self, *rest, *args, &block) }
  end
end

a = [1,3,5,7,9]
a.map(&:+.(2))
# => [3, 5, 7, 9, 11] 

This way you can pass any type of arguments to the proc, not just strings.

Once you've defined it, you can easily swap out String for Symbol:

class String
  def call(*args, &blk)
    to_sym.call(*args, &blk)
  end
end

puts [1,2,3].map(&'+'.(1))
Community
  • 1
  • 1
max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • So in the context of `a.map(&:+.(2))` "caller" refers to `1,3,5,7,9` (each are Fixnums) and they are being sent a message that is `self` because it refers to the symbol `:+`? It has nothing to do with `Kernel#caller` correct? Also, can you elaborate on why both `*args` and `*rest` exist? – mbigras Dec 29 '16 at 18:01
  • `caller` is just a variable name, it's not the Kernel method. In the `a.map` example `caller` is a single element of the array. Each element has the proc called. If you say `1.tap(&:class)` caller will be `1`. With `[1].map(&:class)` caller is the number `1`, not the array. Honestly I'm not sure what the `rest` does (i copied that code). It seems to work just fine if you remove it. – max pleaner Dec 29 '16 at 18:07
1

Refactored code

You're free to choose the name for the proc block variable. So it could be yet_another_arg, some_arg or something_else. In this case, the object you're passing to to_proc is actually the object you want to receive the proc call, so you could call it receiver. The method and param are in the String, so you get them with String#split from self.

class String
  def to_proc
    proc do |receiver|
      method_name, param = self.split
      receiver.method(method_name.to_sym).call(param.to_i)
    end
  end
end

p result = [2, 4, 6, 8].map(&'to_s 2')
# => ["10", "100", "110", "1000"]

Note that this method has been tailored to accept one method name and one integer argument. It doesn't work in the general case.

Another possibility

Warning

eval is evil

You've been warned

This works with the exact syntax you wanted, and it also works for a wider range of methods and parameters :

class String
  def to_proc
    proc { |x| eval "#{x.inspect}.#{self}" }
  end
end

p [2, 4, 6, 8].map(&'to_s 2')
#=> ["10", "100", "110", "1000"]
p ["10", "100", "110", "1000"].map(&'to_i 2')
#=> [2, 4, 6, 8]
p [1, 2, 3, 4].map(&'odd?')
#=> [true, false, true, false]
p %w(a b c).map(&'*3')
#=> ["aaa", "bbb", "ccc"]
p [[1,2,3],[1,2],[1]].map(&'map(&"*2")')
#=> [[2, 4, 6], [2, 4], [2]]

It also brings security problems, though. With great power comes great responsibility!

Community
  • 1
  • 1
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124