This blog post is a reflection on the slide titled “What’s in Your Toolkit?” from Rich Hickey’s excellent talk, Simple Made Easy. The single slide contained some of the most programming wisdom I’ve seen in my career. This is the second blog post in this series, and dives into Methods vs. Functions.
Let he or she who has not confused methods and functions cast the first stone.
I know I’ve done it. I still do it sometimes.
As programmers, two similar tools in our abstraction toolkits are methods and functions. These provide the simplest way of code reuse. Bundle up a few of these lines of code, and go to town! Forget writing
a + b,
add(a, b) is what we’re doing these days.
In Rich Hickey’s talk, Simple Made Easy, he covers why methods complect (complicate) the structure of an application, where functions allow your applications to stay simple.
This blog post will do a deep dive into the difference between methods and functions, and then explore why functions lend to simpler programs.
What is a Function? What is a Method?
In short, a method is attached to an object, whereas a function is not. StackOverflow has a great answer the topic, so we won’t belabor the point too much here.
Let’s take a look at some code samples. We’ll start off with a function:
def calculate_netpay(gross_pay, tax_rate, donation_amount) pay_less_donations = gross_pay - donation_amount pay_less_donations - (pay_less_donations * tax_rate) end
Here we have a function with 3 parameters that calculates the net pay for an employee in the United States under the simplest of scenarios. It’s moderately more interesting than a simple
def add(a, b); a + b; end example.
There are a few properties about this function worth noting:
- It’s written in Ruby, so it’s floating in the global namespace. Anyone can call
calculate_netpayfrom wherever they are.
- It’s a pure function. It has no side effects and its return value is solely the result of its input parameters.
- It has 3 parameters, which is right on the edge of having too many.
Next up, let’s take a look at the same functionality but implemented as a method:
class Paystub def calculate_netpay(tax_rate, donation_amount) pay_less_donations = gross_pay - donation_amount pay_less_donations - (pay_less_donations * tax_rate) end end
Here the method presumably does the same thing as the function before, but this time it’s done via a
Let’s enumerate some of the interesting properties here:
- The behavior is presumable the same as before
- We have one less parameter. It looks like we’ve gotten rid of the
gross_payparameter, and we are instead using a definition of
gross_paydefined on the
- Ruby is a nice language, so it means we don’t have to type as much. But these ergonomics come at a loss of specificity. We have an implied
selfand a potential method call in here. If we were to write some Crappy Ruby™, we could rewrite it as follows.
class Paystub def calculate_netpay(tax_rate, donation_amount) pay_less_donations = self.gross_pay() - donation_amount pay_less_donations - (pay_less_donations * tax_rate) end end
Here it’s clearer where we are pulling that
gross_pay from (the instance itself). It’s also clearer that
gross_pay isn’t an attribute on
Paystub, but a method call. Goofy. (Please don’t write Ruby like this.)
And here is where methods start to get complicated.
gross_pay is now reaching somewhere on
Paystub to do it’s job. The process of calculating the net pay is no longer a pure function.
All bets are off and the doors have been thrown wide open. Is calling
#gross_pay really expensive? Does it need to be set before we call
Paystub#calculate_netpay? What happens if it’s unset?
You’ll notice that our old friend STATE has crept in.
How Do Methods Complicate?
Methods complicate programs because they allow a coupling with state.
In a previous blog post, we explored why state makes programs complex, and values make programs simpler. Based on their signatures, it’s ambiguous whether methods interact with state on an object. Methods can be pure, but there is no guarantee.
Functions on the other hand are free-wheelers. They can affect state, but likely only global state. They could also inflict side effects, but so can methods. Functions don’t have a
self or a
The complications around methods come in as soon as the language’s
this starts to show up. When not using that to call a method (e.g.
self.call_my_method()) and using it to interact with an attribute, we are bringing state into the execution of our method. This permeation of state will slowly poison our program with null checks and ambiguity.
Let’s assume that the
gross_pay from the above example was an attribute on our class instance. Watch how it starts to complicate the program.
class Paystub # (Not using attr_writer here to be explicit for the # non-Rubyists reading.) def gross_pay=(val) self.gross_pay = val end def calculate_netpay(tax_rate, donation_amount) if self.gross_pay pay_less_donations = self.gross_pay - donation_amount else raise 'An error?' end pay_less_donations - (pay_less_donations * tax_rate) end end
Because we have this stateful
gross_pay attribute, we introduce a dangerous property into our
Paystub class: It can be half-baked. From here, one of two things will happen:
- The conditional checks for
gross_paywill spread to callers, forcing each caller to check to make sure
Paystub#gross_payis set before calling
Paystub#calculate_netpay. All callers will have to make this check or roll the dice.
- The conditional check will be pushed into the
Paystub#calculate_netpay, causing its execution to be less simple or non-deterministic. Callers now have to worry about the possibility of an exception or some nonsense return value.
Either way, callers are burdened with the knowlege that
Paystub#calculate_netpay is not always straightforward. Do this a few hundred more times and your application will be a nice Ball of Mud.
A Way Out
So what can be done? Generally, prefer stateless functions over methods even if the methods are stateless. Methods open up the possibility of state.
export nature makes writing and organizing stateless functions much simpler.
So as our projects grow and we look to keep our applications simple, we now have a new tool in our toolkit to do so: Prefer Functions over Methods.
This is where our values become merely buckets of data with no decoration. In the example above, there may be a case for a
Paystub to be a rich value object where calculating net pay is just a derivation of its own values. This is a great point and some excellent feedback.
So I’m leaning toward presenting an order of operations for preferring functions over methods:
- Move logic from methods into stateless functions. This helps you see.
- Once you have stateless functions, ask the question if you have Anemic Domain Models.
- If so, carefully move logic back into methods to improve your domain models.
Let’s see how that code might look if we take our stateless function approach to the extreme, modeled loosely on the above example:
# Here we assume that the `paystub` parameter is just a value object # with no methods hanging off of it. It’s just a bucket of data. def calculate_netpay(paystub) paystub.gross_pay - paystub.employee_taxes - paystub.benefits_deductions - paystub.donations end
So we have a different smell, which is that our parameter object should likely encapsulate the concept of net pay. When we see code shaped like this in a stateless function, here is one the times where we want to move back to a method:
class Paystub def netpay gross_pay - employee_taxes - benefits_deductions - donations end end
The nice part about Ruby is that this code even looks cleaner, and remains simple.
Here we have a good sequencing of cleaning up existing code, and then finding the best home for it. It’s great to be able to see this tradeoff and walk through counterexamples.
If you enjoyed this post, you may also enjoy another post in this series: State vs. Values.
If you’re interested in receiving blog posts like this regularly, join hundreds of developers and subscribe to my newsletter.
thismight resolve to something in your language, but it will be nonsense. There’s a big object in the sky in Ruby, so
selfinside of a method in Ruby will still do something. ↩