There will be Ruby in the middle of this blog post. But first, a tangent in which we explore the lambda calculus. Don’t worry too much about being lost in this next section; we’ll fix that immediately afterward.
A Simple Language
There is a programming language named the lambda calculus. It’s rather minimal: an expression, E
, is one of three things:
- a variable,
V
, likea
,b
, orc
- a function of one parameter, written
λV.E
- two expressions applied to each other, written
E E
For example, the identity function is written:
λx.x
And a function that takes two arguments and produces the first is written:
λx.λy.x
You might want to try it out interactively.
We can encode numbers using this programming language, with some creativity. For example, this is the number zero:
λf.λx.x
And this is one:
λf.λx.f x
And this is two:
λf.λx.f f x
And so on. We can even add them, using this addition expression:
λm.λn.λf.λx.m f (n f x)
(This is far too tangental, but this definition of numbers and addition is an early example of object-oriented programming.)
For example, to add the number one and the number two, we’d write this:
(λm.λn.λf.λx.m f (n f x)) (λf.λx.f x) (λf.λx.f f x)
Name It
There is an oft-included “extension” to the lambda calculus that might help: naming. Let’s add this expression to our language:
- a name definition, written
VALUE ≡ E
So now we can write:
ONE ≡ λf.λx.f x
TWO ≡ λf.λx.f f x
ADD ≡ λm.λn.λf.λx.m f (n f x)
THREE ≡ ADD ONE TWO
Law Of Demeter
Here is another case, in another programming language, where naming is useful:
User.where('admin').each do |user|
user.articles.where('published_at IS NULL').each do |article|
article.update_attributes(published_at: Time.now)
end
end
Here it is after introducing some names:
User.admins.each(&:publish_articles)
And here it is after a further simplification:
User.publish_admin_articles
Take It Further
In the first naming section, we named concrete nouns; in the second, we named verbs. Let us now name a concept:
class SignUp
def initialize(params)
@account_params = params[:account]
@user_params = params[:user]
@credit_card_params = params[:credit_card]
end
def run
account = Account.new(@account_params)
if account.save
user = User.new(@user_params.merge(account: account))
if user.save
credit_card = CreditCard.new(@credit_card_params.merge(user: user))
if credit_card.charge
user.charged
end
end
end
end
end
Above we have named the concept of signing up, and given it behavior. We can name even more concepts: merging two users, generating flash messages, processing image files, building a model of a couch.
Taking It Too Far
The average human vocabulary is around 10000 words. While the jury is still out on what the maximum number is, we can all agree that keeping the number of names low is useful for keeping all of an app in your head at once.
There is another trick for more easily understanding an app: a common vocabulary. When you see a User
model, you know what it means; when you see an Enrichment
class, that’s puzzling. By naming more things uniquely, you have reduced the vocabulary overhead.
Or is this a red herring? Another way to keep the vocabulary low is to name everything, but have very few things to name. Push stuff into frameworks, libraries, and APIs when possible, and out of scope otherwise. You ain’t gonna need that Bracelet
class.
Not Taking It Far Enough
The idea of a common vocabulary is enticing. User, article, comment, controller, singleton, and enumeration are all names we Rails developers understand. Monad, cut, disjunction, pointer—those are other people’s vocabularies. But what if …
What if we outgrew our vocabulary and started poaching theirs? And not like how we poached “functional test” and “closure”—I mean actually use their words the way they are defined.
What if instead of this …
def map(&f = lambda {|x| x})
accumulation = []
each do |element|
accumulation << f.call(element)
end
accumulation
end
… we wrote this:
def map(&f = id)
inject([]) do |accumulation, element|
accumulation + [f.call(element)]
end
end
Heck, what if we went further and wrote this:
def map(&f = id)
inject(empty) do |accumulation, element|
accumulation + wrap(f.call(element))
end
end
This uses a method Kernel#id
to name the common lambda {|x| x}
abstraction; a method empty
that produces the empty version of whatever object this Enumerable
is mixed into; and a similarly-defined method, wrap
, that projects the given object into the Enumerable
. Now it produces arrays, linked lists, sets, tries, maybes, and so on, as needed.
And now we can use the names identity function, functor, and monoid, too.
Readability and Conceptualization
Just as our lambda calculus example was greatly simplified by naming our functions, our way-too-long methods can be improved by naming our methods. As with anything, though, there is a trade-off that you must carefully consider.
Here’s an example of some code with very few names:
def acquire_access_token_for(c)
res = Net::HTTP.start('github.com', 443, use_ssl: true) do |http|
r = Net::HTTP::Post.new('/login/oauth/access_token')
r.set_form_data('client_id' => '1234123',
'client_secret' => 'basdu9as',
'code' => c)
http.request(req)
end
case res
when Net::HTTPsuccess
b = res.body
if b['access_token'].any?
b['access_token'].first
else
raise b['error'].first
end
else
raise res.inspect
end
end
Here’s the same example, with more names:
def acquire_access_token_for(code)
access_token_response = access_token_post(code)
case access_token_response
when Net::HTTPsuccess
extract_access_token_from(access_token_response)
else
handle_access_token_failure(access_token_response.message)
end
end
private
GITHUB_CLIENT_ID = '1234123'
GITHUB_SECRET = 'basdu9as'
def access_token_post(code)
Net::HTTP.start(*http_connection) do |http|
request = Net::HTTP::Post.new(github_access_token_path)
request.set_form_data('client_id' => GITHUB_CLIENT_ID,
'client_secret' => GITHUB_SECRET,
'code' => code)
http.request(request)
end
end
def http_connection
[
github_access_token_uri.host,
github_access_token_uri.port,
use_ssl: true
]
end
def github_access_token_path
github_access_token_uri.path
end
def github_access_token_uri
URI.parse(github_access_token_url)
end
def github_access_token_url
'https://github.com/login/oauth/access_token'
end
def extract_access_token_from(successful_http_response)
body = CGI.parse(successful_http_response.body)
if body['access_token'].any?
body['access_token']
else
handle_access_token_failure(body['error'].first)
end
end
def handle_access_token_failure(error_message)
raise error_message
end
As you can see, there’s a trade-off: on the one hand, you can now read it with ease, debug more easily, and have a vocabulary with which to discuss it with others. On the other hand, it takes more vertical space.
Naming As a Building Block
Naming is the most powerful abstraction possible. By naming something, you give other people the ability to build atop it. By re-using a name you build a common vocabulary, encouraging more people to build. Naming turns a blob of code into a sequence of patterns. Giving a name to something develops it into a concept with analogies.
So, name it.