Comment by chao-

Comment by chao- 20 hours ago

6 replies

The "aesthetically pleasing" aspect of blocks is not mutually exclusive with real, first-class functions! Ruby is really more functional than that. Ruby has both lambas and method objects (pulled from instances). For example, you can write:

  let isLarge = a => a>100;
as a lambda and call via #call or the shorthand syntax .():

  is_large = ->(a) { a > 100 }
  is_large.call(1000)
  # => true
  is_large.(1000)
  # => true
I find the .() syntax a bit odd, so I prefer #call, but that's a personal choice. Either way, it mixes-and-matches nicely with any class that has a #call method, and so it allows nice polymorphic mixtures of lambdas and of objects/instances that have a method named 'call'. Also very useful for injecting behavior (and mocking behavior in tests).

Additionally, you can even take a reference to a method off of an object, and pass them around as though they are a callable lambda/block:

  class Foo
    def bar = 'baz'
  end

  foo_instance = Foo.new
  callable_bar = foo_instance.method(:bar)
  callable_bar.call
  # => 'baz'
This ability to pull a method off is useful because any method which receives block can also take a "method object" and be passed to any block-receiving method via the "block operator" of '&' (example here is passing an object's method to Array#map as a block):

  class UpcaseCertainLetters
    def initialize(letters_to_upcase)
      @letters_to_upcase = letters_to_upcase
    end

    def format(str)
      str.chars.map do |char| 
        @letters_to_upcase.include?(char) ? char.upcase : char
      end.join
    end
  end

  upcase_vowels = UpcaseCertainLetters.new("aeiuo").method(:format)
  ['foo', 'bar', 'baz'].map(&upcase_vowels)
  # => ['fOO', 'bAr', 'bAz']
This '&' operator is the same as the one that lets you call instance methods by converting a symbol of a method name into a block for an instance method on an object:

  (0..10).map(&:even?)
  # => [true, false, true, false, true, false, true, false, true, false, true]
And doing similar, but with a lambda:

  is_div_five = ->(num) { num % 5 == 0 }
  (0..10).map(&is_div_five)
  # => [true, false, false, false, false, true, false, false, false, false, true]
kace91 17 hours ago

That is interesting! I haven't explored Procs much, since I use ruby for a shared codebase at work and I was originally a bit afraid of trying to push unidiomatic ideas in the codebase.

In your experience, is it ok to use Procs for example for extraction of block methods for cleanliness in refactors? or would I hit any major roadblocks if I treated them too much like first-class functions?

Also, is there any particular Rails convention to place collections of useful procs? Or does that go a bit against the general model?

  • vinceguidry 15 hours ago

    You shouldn't have much difficulty, Ruby converts blocks to Procs whenever it needs an actual object. Their semantics are intentionally kept the same. This is unlike lambdas, whose semantics are closer to methods.

    Pass the wrong number of of arguments to a Proc or block, it will pass nil for missing args and omit extras. Pass the wrong number of arguments for a method or lambda and you get an ArgumentError. Use the return keyword in a lambda, it returns from the lambda, just like if you call return in a method. In a block or Proc, it returns from the calling method.

    So I would feel comfortable leaning on them for refactoring as it's as Ruby intended. Just use lambdas when you want to turn methods into objects and Procs when you want to objectify blocks.

    You should get ahold of a copy of Metaprogramming Ruby 2 if you find yourself refactoring a lot of Ruby. It's out of print, but ebooks are available.

    • vidarh 14 hours ago

      Just to clarify here: Both lambdas and procs are Proc objects. Blocks gets turned into Proc objects when you take their value.

      So just be clear about whether you're talking about a proc or a Proc...

      > In a block or Proc, it returns from the calling method

      No, in block or a Proc it returns from the scope where it was defined.

      Usually this is the same, and so it doesn't usually matter much, but if you pass a proc or a block down to another method, then a return within the block will still return from the method where it was defined.

      This can occasionally be useful, as you can use it to pass in a predicate to a method that can control if/when you want to return, irrespective of how deeply nested.

      • vidarh 9 hours ago

        Too late to edit now, and this is what I get for quibbling about casing:

        > No, in block or a Proc it returns from the scope where it was defined.

        Should of course read:

        > No, in a block or a proc it returns from the scope where it was defined.

  • Mystery-Machine 15 hours ago

    This sounds like a really innovative idea. I haven't seen a dedicated place for "collection of useful procs", but one emerging pattern is to use `app/services` and then have a bunch of single-responsibility service classes that each have call or perform method and then you use the service when you need some shared functionality. It's like a proc, but instead it's a class with `#call` method.

    • vidarh 9 hours ago

      > It's like a proc, but instead it's a class with `#call` method.

      It's called the "callable" pattern.