0

I sometimes use this construct:

class Test
  def today
    date = Date.today

    def date.is_friday?
      strftime("%A") == "Friday"
    end

    date
  end
end

it works fine...

t = Test.new
t.today # => <Date: 2016-09-03 ((2457635j,0s,0n),+0s,2299161j)>
t.today.is_friday? # => false

I have always thought of this as a nested method. But others are adamant that Ruby does not have nested methods. OK, I'm not hooked on the name. But what DO you call it? And is there a better way to achieve the same functionality?

Les Nightingill
  • 5,662
  • 1
  • 29
  • 32
  • http://stackoverflow.com/questions/4864191/is-it-possible-to-have-methods-inside-methods – dinjas Sep 04 '16 at 04:47
  • 1
    Why not just use [Date#friday?](http://ruby-doc.org/stdlib-2.3.0/libdoc/date/rdoc/Date.html#method-i-friday-3F): `t = Test.new.today; t.friday?`? Why define a method`is_friday?` on each `Date` instance returned by `Test.new.today`? – Cary Swoveland Sep 04 '16 at 05:18
  • Good idea, Cary... that would be a very good approach for the example I've shown. I probably should've shown an example closer to the way I usually use this construct. In a Rails context, mostly I extend some method with a #to_s, or #to_json. So there may not be a ready-made class (e.g. Date, above) to extend with the functionality I need. – Les Nightingill Sep 04 '16 at 12:40
  • I just happened to see your comment addressed to me. For me to be notified of such comments you need to include my username, or part of it, such as @cary. – Cary Swoveland Sep 06 '16 at 18:25

3 Answers3

3

When people talk about nested methods/functions, they generally mean things that are only visible/usable from within the enclosing method. This isn't true of your example - once defined, it can be called on that object by anyone.

What you're showing is Ruby's singleton methods: the ability to define a method on one instance of a class (for example when people say class method in ruby, those are actually singleton methods on that instance of the Class class)

As to whether there is a better way, that is unanswerable since you haven't said what the problem you are trying to solve is.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Thanks for your good insight Frederick. The problem I'm trying to solve is typically in a Rails context, where a model has a method, whose return value might be used in other methods, but to which I would like to attach a #to_s extension for inclusion in a view, or perhaps attach a #to_json extension for inclusion in an api. – Les Nightingill Sep 04 '16 at 12:21
  • That sounds like you might be jnterested in decorators or or presenters or custom serialisers – Frederick Cheung Sep 04 '16 at 12:39
1

Calling def inside a method is usually the wrong way to do this. What you want is to define a module that encapsulates this and potentially other methods, then mix that in on any objects as necessary:

module DateExtensions
  def is_friday?
    wday == 5
  end
end

It's worth noting that strftime might return values other than "Friday" because localization may be in effect. You could get "Freitag" or "Vendredi" depending on where you are. The wday method returns a predictable numerical value.

Now you can mix this in:

class Test
  def today
    date = Date.today

    date.extend(DateExtensions)

    date
  end
end

By declaring these methods inside a module it's a lot more obvious they're part of a package. It also means you don't need to define a brand new method for each instance of a Date you create.

A more Ruby way of doing this is to define your own subclass of Date that adds in this additional behaviour.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • This also only defines the method on the singleton class of that object. I don't see why this would be any better. One now needs to find the "DateExtensions" module to see what it actually does. – Pascal Sep 04 '16 at 09:01
  • That's why I say a more Ruby way is a subclass. At least with the module approach you have a container for all these methods that get hacked on and you can track it down that way: `date.method(:is_friday?).source_location` I would hope from the name it's pretty obvious it "extends Date" in comparison to the original code where it had no context given at all. – tadman Sep 04 '16 at 09:54
  • @tadman I think you're right that a subclass or module is ideomatic ruby. As pascal suggests, though, it seems to make the code less readable, when the extension is just a very small method, compared to my hack of defining the singleton method right where it's used. I was hoping for a way to define the extension within the Test class. Is that possible? – Les Nightingill Sep 04 '16 at 12:32
  • If you're using a modern version of Ruby you can always [try using `refine` to do this](https://ruby-doc.org/core-2.3.0/doc/syntax/refinements_rdoc.html). – tadman Sep 04 '16 at 12:42
0

Method definitions are code just like any other code. They can appear in methods just like any other code. And just like any other code, they will be executed every time the method runs.

So, this

class Foo
  def foo
    def bar
    end
  end
end

is not a nested method, but simply a method that defines a method. It is easy to see that this is not a nested method, by simply calling it twice, paying attention to the warnings generated and inspecting the defined methods of the surrounding module:

foo = Foo.new

Foo.public_instance_methods(false)
#=> [:foo]

foo.foo

Foo.public_instance_methods(false)
#=> [:foo, :bar]

foo.foo
# (irb):3: warning: method redefined; discarding old bar
# (irb):3: warning: previous definition of bar was here

As you can see, bar is not nested inside foo, instead it is defined besides foo in class Foo. It is only defined after foo has run, and it gets re-defined every time foo runs.

Your example is a bit different, of course, since it doesn't keep overwriting the method in the same class, instead it defines it in a different class every time it is called.

Note that there are plans of forbidding this kind of usage, see Feature #11665: Support nested functions for better code organization.

But what DO you call it?

It's a method that defines a method.

And is there a better way to achieve the same functionality?

It's kinda hard to tell what exactly it is you are trying to achieve.

You could create a module with your method and extend all the objects with it. That would be the closest analog.

Other solutions would require a larger restructuring of your code, e.g. using the Decorator Design Pattern or, in your specific Rails use case, a Presenter.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653