Keep your code DRY by avoiding WET
Everyone that’s been programming for a while knows the value of keeping your code DRY (Don’t Repeat Yourself). The last thing that you want to do is have WET (Written Everything Twice) code. That’s right, it’s such an important concept that there’s two acronyms for it!
There are many benefits to keeping code as DRY as possible.
It’s usually faster to turn a chunk of repeated code into a method, that way the next time you have to call it, you can do so in one line.
If you need to change your code, you only need to do so in one spot.
Your code is far easier to understand if every section is unique. Otherwise you risk reviewers looking at a section, saying ‘I’ve seen this before’, and not noticing that this time it’s a tiny bit different.
So it’s always good practice to put any repeated code into a method that can be called repeatedly. But what about those times when you do want to change your code block very slightly? For those instances, you have the yield keyword.
How Yield Works
The yield keyword allows you to temporarily pass control from the function in the middle of its execution. Essentially, you can build a function that has a yield in it, and then you can replace that ‘yield’ with code of your choice every time you call the function.
When you call a function with a yield, you include a block of code, surrounded by curly braces, that will replace any instance of the yield keyword. The following two functions have identical outputs.
def hello()
return “Hello World!”
end
hello() #=> “Hello World!”
def hello2()
return yield
end
hello2(){“Hello World!”} #=> “Hello World!"
Multiple yields can be used in the same function, and each one will be replaced by the same code block.
def double()
yield
yield
end
double(){print “Hi!”} #=>”Hi!Hi!”
Of course, code blocks would be fairly useless without the ability to pass variables through the yield. In the below code snippet, the name variable is passed to the yield block, where it is called person.
def greet(name)
print “You see #{name}. ”
puts yield(name)
end
greet(‘Dave’){|person| “Hi #{person}!”} #=> ”You see Dave. Hi Dave!”
greet(‘Dave’){|person| “#{person} says hello!”} #=> ”You see Dave.
Dave says hello!”
You may have noticed in the last example that yield but life a bit easier for us. Notice that in the above two examples, we put the name at either the beginning or end of the second sentence. If we were passing the rest of the sentence into a function, we would need a bit of extra logic to determine where exactly the name should go. But thanks to yield giving us direct access to the variable inside the function, we can easily put it where we want.
Functions vs Yield Blocks
The same functions can be called multiple times, but a yield block must be written in its entirety every time a function must yield. Therefore it is best practice to use the function for as much as possible, and only use yield for the mutable parts of the code.
Yields and functions work identically for passed variables. Yield functions only have access to the variables passed to it when it is called, just like functions. Variables will be passed to yield in the default manner; this means that changing an integer, string, or float in a yield will not affect the value outside the yield; but changing an array or hash will.
The return keyword ends the functions that the yield is in. This means that anything that appears after the yield will not execute. This is something you’ll probably want to avoid in most cases, so just use implicit return.
Within the function containing yield, you can treat yield as a function for the purposes and syntax of passing it values and retrieving values from it. For example, if a yield block takes in a string and returns an array, you can use yield(string)[3] to pass in the string and get the 4th element of that array. You can also set variables equal to the output of yield: foo = yield.
Using Yield with Class Iterators
One of the most common uses of yields is adapting classes for use in iterators. Imagine that we have a People class, and that each instance of this class has various attributes: name, age, career, etc.. If we wanted to make functions that could easily filter out specific people, we might have a few functions that look like this.
def filterPeopleByAge(minAge,maxAge)
people = Person.all.filter do |person|
person.age >= minAge && person.age <= maxAge
end
return people
end
filterPeopleByAge(18,26)
#Returns everyone between 18 and 26
def filterPeopleByName(name)
people = Person.all.filter do |person|
person.first_name == name
end
return people
end
filterPeopleByName (‘Dave’)
#Returns everyone named Dave
def filterPeopleByOccupation(occupation_array)
people = Person.all.filter do |person|
occupation_array.include?(person.occupation)
end
return people
end
filterPeopleByOccupation ([‘Doctor’,’Lawyer’,’Professor’])
#Returns all doctors, lawyers, and professors
But that certainly seems like a lot of repeated code. We’re only changing a single thing across each of those functions: the filter condition. We can’t simply write a function that can handle searching across every possible attribute (at least, not one that’s tidy or easily legible). But by using a yield, we can directly control the condition statement. The function and yield below can perform the role of the above functions in a much shorter and more legible manner.
def filterPeople()
people = Person.all.filter do |person|
yield(person)
end
return people
end
filterPeople(){|person|person.age > 18 && person.age < 26}
# Returns all people between 18 and 26
filterPeople(){|person|person.first_name = ‘Dave’}
#Returns all people whose first name is Dave
filterPeople(){|person|
[‘Doctor’,’Lawyer’,’Professor’].include(person.occupation) }
#Returns all doctors, lawyers, and professors
This generic function is even more powerful than the previous set of functions for three reasons. First, it's still just as valid even if we add additional attributes to the person class. Second, it only takes one line to call and create new search criteria. And finally, we can easily use it to combine search criteria.
filterPeople() {|person| person.age < 40 && person.occupation == 'Lawyer'}
#Returns all lawyers under 40
An even MORE generic solution
The previous search function looks pretty good on paper, but in practice it has a glaring flaw. Yield ONLY has access to values that are passed to it from the yielding function.
def filterPeople(args)
people = Person.all.filter do |person|
yield(person,args)
end
return people
end
minAge = 18
maxAge = 40
filterPeople(){|person|person.age > minAge && person.age < maxAge}
#THIS DOES NOT WORK. The yield cannot access the Age values because they were not passed in during the filterPeople function.
If we want to be able to have a smarter search function, we need to allow it to accept some parameters. We'll create a generic array args that we can use to hold our criteria.
def filterPeople(args=nil)
people = Person.all.filter do |person|
yield(person,args)
end
return people
end
Now we are freely capable of passing parameters to the yield through the function. And because the function and yield block are all called together, it should be fairly legible as to what args[foo] is referring to.
minAge = 14
maxAge = 18
filterPeople([minAge,maxAge]){|person,args|person.age > args[0] && person.age < args[1]}
minAge = 30
occupations = ['Artist','Race Car Driver','Athlete']
filterPeople([minAge,occupations]){|person,args|person.age > args[0]
&& args[1].include?(person.occupation)}
Now we have a function which allows us to create any arbitrary search criteria that we want for the Person class! And we can call it on a single line no less!
Top comments (1)
A few asides before I get into it:
ruby
after triple backticks to get Ruby syntax highlighting instead of just white text. It helps a lot for readability.return
statements aren't always necessaryDRY
can be overdone, everything in moderation, and premature abstraction can bite you.#
instead of//
snake_case_names
overcamelCaseNames
Now then, article wise.
It may be good to mention that
yield
is effectively the same as this:yield
just implies it.block_given?
is also a nice feature for checking if the caller actually gave a block function or not.Functions vs Yield blocks
Point One
Not sure what you mean here? A
yield
ed block is a function, and can be reused. You may want to clarify thatfunction
is really amethod
, becausefunction
can be more commonly understood to meanproc
orlambda
instead in Ruby.Point Two
yield
ed functions are closures, which means they can see the context where they were created.Now when you mention not being able to change
Integer
s that's more because they're primitive values. This will work the same acrossProc
s,block
s,lambda
s,method
s, and anything else in Ruby.For Strings though, those are mutable unless you have frozen strings on:
Point Three
return
will end any function, whether or not ayield
is involved. Now inside a function,return
does some real interesting and potentially confusing things depending on what type it is:Can't say I ever really understood that one myself, keeps catching me whenever I start using
proc
for some reason in my code so I tend to go forlambda
/->
when possible.lambda
also is more explicit about arguments andarity
than aproc
is, so it's easier to tell something broke.Remember though that block functions are
Proc
s:Point Four
This comes back to why, for me, I prefer the explicit passing of a block. Less magic and more easily understandable from a glance. Just remember that's a stylistic choice for me, do what makes more sense for your team.
Using Yield with Class Iterators
Enumerable
is a very common use of this:...which gets us all of the fun
Enumerable
methods likemap
and others.Now to your example:
A few quick cleanups and we have:
filter
will return anArray
making the assignment redundant. The name for a function used to filter is a predicate.Now a more pragmatic example of this might be to use the Enumerable trick from above:
This will allow you to directly control the
People
you get back:Second Implementation
Your second implementation makes this a bit more complicated:
...but you got real close to something fun in Ruby in the process. Let's assume we still have an
Enumerable
person class out there like above.We're going to get into some metaprogramming, so buckle up, it's a trip.
For this one we'll need
Object#send
,===
,to_proc
, andHash
to do some fun things withkwargs
. If you've ever seen ActiveRecord, this will look familiar:We can do that using
===
andfilter
in Ruby by making our own class which responds to===
:Why the
to_proc
and the===
? It allows us to do both of these:This pattern is super powerful, and ended up expanded in a gem I wrote called
Qo
. The problem is you have to do some more lifting to get it to work withArray
andHash
values to match against them.Ruby has a lot of potential for flexibility, especially when you know about
===
andto_proc
and how to leverage them.Other thoughts
Keep writing, it takes a bit to really get into all the fun stuff, and feel free to ask questions on any of that, I got a bit carried away in responding.