GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Name It

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, like a, b, or c
  • a function of one variable, 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

Mulva? Gipple? Dolores!

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.