Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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?



> In your experience, is it ok to use Procs for example for extraction of block methods for cleanliness in refactors?

In addition to the efficiency issues of unnecessarily reifying blocks into Proc objects, there are some sharp edges that can cut you doing this. For instance, Ruby blocks and procs [0] have return semantics based on where they are defined (specifically, "return" in a block or proc returns from the method in which the block/proc is defined.) So if you have two identical blocks used in two different methods, and those blocks have return calls, and you refactor them to shared Proc defined somewhere else that is used in each method, you will find that the behavior is broken, and when it returns it will either return from the wrong method or produce a LocalJumpError, depending on the exact structure of the program.

Local return from a block/proc is done with "next" instead of "return" (and if neither a next or return is hit, the last expression evaluated will be treated as an implicit "next"), so a block without an explicit return can be converted to a proc and reused in different places more safely.

[0] using the convention of lowercase "proc" to refer to a Proc object that does not have lambda semantics, whereas those created with Kernel#lambdda or the stabby-lambda syntax are "lambdas".


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.


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.


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.


> 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.

In a block or proc, it returns from the method in which the block/proc was defined, not the calling method. These may or may not be the same; the proc may be called deeper in the call chain, or it might (erroneously) be called after the defining method had exited, producing a LocalJumpError.

This also means that implicit returns from a block/proc have different semantics than explicit returns, unlike lambdas/methods where implicit returns have the same semantics as explicit returns. (From a block/proc, the last evaluated expressions behaves as if it were passed to "next" rather than "return", resulting in a local return to the calling method rather than a return from the defining method.)


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.


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

It's called the "callable" pattern.


Sorry for the late reply. Some others have responded and I mostly agree, but I will add where I can.

>is it ok to use Procs for example for extraction of block methods for cleanliness in refactors?

As others have said, use Lambdas via `->() { do_thing }` over Procs if you want to pass them around. They are the "true" way to use first-class functions.

Also, if you want to pass methods around, you can do so, just be aware that they carry everything you might expect with them: they are called with the receiver still being the object they came from. Callbacks are uncommon in Ruby, but I've seen a few instances of passing methods of the current object into some other as a callback, e.g.

  some_interactor.do_complex_thing(
    order_id: 123, 
    on_success: method(:succeeded),
    on_failure: method(:failed),
  )
Then #do_complex_thing can use `on_success.call` and `on_failure.call`, which is nice when you want to stub these in unit tests without resorting to stubbing/mocking. You can just pass lambdas (first-class functions) as the value for :on_success and :on_failure kwargs in tests.

>Also, is there any particular Rails convention to place collections of useful procs?

No convention for a "place" for these. I generally dislike how Rails got people into using directory names for the "kind" of a module/class instead of actually mirroring the architecture. This is kind of why.

If what you really desire is a big bag of pure functions in the global scope, you should still at least put them inside of a namespace (a Ruby module). For example, if you wanted a big bag of type coercions, you might put them into a module:

  module CommonCoercions
    class << self
      def currency
        ->(value) { do_things }
      end

      def coordinates
        ->(value) { do_things }
      end
    end
  end
However, this isn't necessarily very efficient. Each call to CommonCoercions.currency is going to instantiate a new Lambda instance! You could alleviate this by caching the value in a singleton-scope instance variable:

  module CommonCoercions
    class << self
      def currency
        @currency ||= ->(value) { do_things }
      end

      def coordinates
        @coordinates ||= ->(value) { do_things }
      end
    end
  end
Even then, this isn't really common in Ruby. You might as well just have these be singleton methods outright (aka "class methods"):

  module CommonCoercions
    class << self
      def currency(value)
        do_things
      end

      def coordinates(value)
        do_things
      end
    end
  end
If you really want to pass them around as first-class functions, you can grab a reference to that function like so:

  CommonCoercions.method(:currency)
And then use them via `.call()` the same as any other lambda if you want to pass them around.

However, if you want to use them only locally within a particular class, in a Rails project you are better off just calling them directly on the module, or even delegating so that the method is available in the class as though it were a local instance method:

  module CommonCoercions
    def self.coerce_currency(value)
      do_things
    end
  end

  class SomeAction
    delegate :coerce_currency, to: CommonCoercions

    def some_method(params)
      coerce_currency(params.fetch(:amount))
    end
  end




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: