Do You Believe in Programming Magic?

Development

Unlike pulling a rabbit out of a hat, “magic” in programming is often performed under the guise of productivity. In this post, we’ll look at what defines a magical programming experience for better or worse.

If you were in Rails around 2007 you might be quick to describe it as “auto-magical,” and this was a good thing. Magic meant freedom from the tyranny of endless boilerplate, freedom from hours of tedious configuration, freedom to be productive.

In time, the programming community and ecosystem has grown and changed. There was a period of violent anti-Rails phobia, and the pendulum now seems to be swinging back in its favor. Either way, the one constant complaint that comes up is “it’s too magical.”

What exactly does this phrase mean, and what could we maybe do about it? Rails is where I’ve got the bulk of my magic experience, so I’ll focus on that.

Magical Experiences by Example

Magic is all about doing the unexpected. You don’t expect an assistant to survive being sawn in half, but there they are, smiling and waving.

Rails does this really well. If you name your things correctly and follow conventions, it is able to do an extraordinary amount of predicting what you want and doing it right the first time.

One example is routes. Web apps need to tie in the code to an HTTP endpoint somehow. Rails uses a routes file to direct a request to a specific browser action.

Rails.application.routes.draw do
  resources :users
end

That’s all you need to set up these eight endpoints:

$ rake routes
   Prefix Verb   URI Pattern               Controller#Action
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PATCH  /users/:id(.:format)      users#update
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy

Even cooler is that it now seems like Rails knows that you’ve got a User model and that linking to an instance of that model should send them to the appropriate place. For example:

<% @user = User.where(id: 10).first %>
<%= link_to "A User", @user %>

This would generate a link that looks kinda like this:

 <a href="users/10">A User</a> 

Wow. If the user hasn’t been saved to a database and doesn’t have an id, they’ll be sent to users/new. That’s even more magical.

This same line of code is smart enough to do the right thing out of the box and tie in a very complicated system of Models, Views, Controllers, and Routes. I love it. This is a positively magical experience.

Rails does this all over the place, from has_many declarations from Active Record, to command line scaffold generating. When you get it right, the experience is unparalleled.

So why then would Rails be “too magical” if it is a helpful positive experience? What would “just magical enough” look like?

Anti-Magic

Another word for magic might be implicit behavior. I didn’t tell my router about a User model — it implied that if I passed one in that it should route to that endpoint. The opposite of magic would then be explicitness; nothing gets done unless you explicitly do it.

In this world, a magician would bring the audience to the rabbit farm.

Rails adds a few helper methods to all objects via Active Support. One of the crowd favorites is blank?. It will not only check an object for nil or false (the only two false-y values in Ruby), but it will also check it for empty? since we often want to skip behavior of empty collections.

This is magic that works well most of the time.

[].blank?
# => true
User.new.blank?
# => false

user = User.where(name: "does not exist").first
user.blank?
# => true

Another bit of magic is Active Record auto creating methods on model instances for you. For example, if you have a User model with a name string, Rails is smart enough to add that accessor for us. User.new(name: "Schneems").name works with no configuration needed. Other helper methods are also added, like:

User.new(name: "schneems").name?
# => true
User.new.name?
# => false

Now what happens when we blend our magic together? Say we’re running a bitcoin-backed checking company; we might have a Check model where we store written checks. Because we want to handle all our edge cases like a 12-year-old Preston Waters receiving a check without an amount, we add a blank field to our Check model.

check = Check.new(amount: 1_000_000, blank: true)
check.blank?
# => true

Oops, looks like our Active Record magic killed our Active Support magic. If you know how both of those things work and where they come from, then this example is pretty contrived and almost expected. If you don’t know what library or where adds what methods, it can be difficult to debug.

This is the “too magical” behavior that people talk about. It kills productivity, it isn’t expected, and it may go undetected and cause us to lose all our bitcoin “money” via a bug. Our programmers won’t even think to test that case. When you finally realize there’s a bug, how do you know where to look? If you don’t know about Ruby’s introspection ability, you may be stuck for hours.

It’s frustrating and demoralizing. It hurts.

Three Rules for Non-Awful Programming Magic

Three things make up a non-magical programming experience:

  1. High-level documentation. How do I use your framework?
  2. Method or class-level documentation. How do I use your API?
  3. Errors. When things go wrong, do I get helpful errors?

Let’s break all that down.

High-level documentation

I want high-level documentation like http://guides.rubyonrails.org. For most libraries, this type of guide is small enough that it fits directly in the README. I want examples of common use cases and how this software handles them. Often “you’re doing it wrong” is a response to a misuse of a particular bit of code.

If your library was a business, and issues were support tickets, it should be a goal to answer every invalid bug report with a link to documentation. Not in an RTFM kinda mean way, but in an “it was possible for you to self serve without having to wait for me” way.

In other words, can a good programmer who is familiar with your documentation navigate your library successfully with little to no surprise?

Maintainers should document common problems and their appropriate solutions. Since most libraries aren’t run as a business, it also means users should be active in adding documentation for use cases they didn’t find in the guides. This is part of the fee you pay for free software, but don’t worry. You will make dividends on it when you realize you have the same problem a year later, and Past You already documented the perfect fix.

Low-level documentation

Programming doesn’t end where tutorials and guides stop. APIs need to be documented. In Ruby, that’s at the class and method level.

Take for example our previous link_to helper: http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to

It has a high-level description — what does it do? It shows method signatures, enumerates the options, and adds some small examples. When you poke under the hood, link_to does some truly magical things, but you don’t need to know that to understand how to use it. If the API is properly documented, you should be able to use it.

Again, if you’re finding docs inadequate or missing, pay it forward to yourself and submit a documentation patch. Maybe check out DocsDoctor to kickstart your documentation habit.

Good errors make good interfaces

I love a good error message. It tells you exactly how you screwed up and how to fix it. I believe a good error message is the true litmus test for magical or nonmagical code. Nick Sutterer posted this graphical representation of my feelings:

Good errors

How great would it have been if we had been notified when we added a blank field to our model it error-ed? What if it told us about the possible conflict and gave us an escape hatch to force the behavior? Here’s a positive example:

$ rails g scaffold users
[WARNING] The model name 'users' was recognized as a plural, using the singular 'user' instead. Override with --force-plural or setup custom inflection rules for this noun before running the generator.

Here the framework realized we were deviating from a Rails convention we may not have been aware of. It automatically did the right thing and gave us a way to force the behavior if we are a power user and really wanted to stray off the Omakase path.

Rails has a ton of these helpful errors and is getting more every day — I’ve been adding them since 2012. When there are errors that can’t be detected, that is when we have a bad time.

One example is that Active Record has a validate and a validates class method. They do different things, and there’s really no way you can infer based on arguments which the user wanted. It’s not magic by traditional standards, but it fails my small test by failing silently when misused.

Having read the guides, API docs, and running code using the wrong method, it’s still possible (easy even) to use the wrong method and not realize it for hours, maybe even weeks. This should be an indication to the maintainers that this interface is lacking.

If we can’t detect incorrect usage (I’ve tried), maybe we should deprecate one of the methods and make it more instructional. Perhaps we change validate to validates_with_custom_method so it’s more than a one-character difference.

Making behavior less magical through feedback and errors isn’t always the easiest, but it’s the price we pay for all that productivity when things work as planned.

(Don’t) Fool Us

Next time you throw up your hands and scream “too magical,” ask if a feature follows the three rules:

  1. High-level documentation. How do I use your framework?
  2. Method or class-level documentation. How do I use your API?
  3. Errors. When things go wrong, do I get helpful errors?

If not, what could be done to wrangle it into compliance?

Anyone can add docs to any project — that’s an easy rule to fix. Good exceptions can be hard to add. You might not have the experience to add it to a project. I consider lack of informative failure modes a bug, and they should be reported like one. How did you find the bad behavior? What did you expect? What happened instead? What type of an error or message could have helped? Open an issue and provide some helpful and constructive feedback.

Working with any library or framework is a tradeoff. By choosing someone else’s library, you’re making a bet that it will be faster for you to learn how to use that software than it would be for you to write your own.

Hiding complexity is what these libraries are supposed to do. When done well, it’s a feature; when done poorly, it is torture. Maintainers and users can both work together to pull back the curtain and make magical programming fun again.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.

  • Pingback: Do You Believe in Programming Magic?-IT大道()

  • Pingback: 15-02-2016 - wut more links? - Magnus Udbjørg()

  • Oz

    Very nice. I missed the quote from Asimov, but I suppose you assumed that we all knew it! Magic can cause confusion, or joy and ease.

    • David Schofield

      (Did you perhaps mean Clarke? Arthur C. Clarke’s Third Law: “Any sufficiently advanced technology is indistinguishable from magic.” – Profiles of the Future (revised edition, 1973)

      • Oz

        oops. yes, of course.

  • Yevhene Shemet

    Rails contains almost no magic, just sane conventions you can override if you need.

    • Schneems

      I disagree, rails has lots of behavior that don’t pass my three tests. Maybe you can say more about why you feel that way.

    • Oz

      Rails has vast amounts of magic. It takes substantial effort to understand the conventions. That unveils the magic. But until then…

  • James McCarthy

    A good read, thanks. There is a little typo in the about @schneems http://keeprubyweird.com link is broken.

  • Ted Johansson

    Magic is awesome when it is useful. One example of what I consider “bad magic” is the implicit controller action. It encourages inconsistency, is intentionally obscure, and is even referred to in the manual as “minimally useful.”

  • msg7086

    “Good errors make good interfaces” This is so important! When I was rolling back the database, I accidentally typed STEP=2016**** instead of VERSION= and wiped my production database.

    • Danny

      Ouch. That’s classic rails though, I can’t help but imagine the maintainers just laughing at us making these kinds of mistakes since they don’t seem to put any effort into making catastrophic errors like this difficult to do accidentally.

      Rails might be great for spinning up a weekend project or something, but IMO if you plan to still be actively working on an app for more than like a month, rails is a bad choice because you lose the speed benefits but are stuck with the thorny parts.

      • Schneems

        I’m a contributing maintainer of Rails. I spent the better part of last few months adding protections against dropping your production database by mistake in Rails 5 master (https://github.com/rails/rails/pull/22967). Unfortunately in this case if you’re going backwards in your schema we can’t detect that you’ve gone further than you intended, we could add a check “did you mean to rollback a migration” but once you run again with “YES” and you’re entering the wrong number it would still have the same effect. Maybe we could show “Did you mean to rollback a migration on production, this will revert of migrations”, that seems like a pretty reasonable idea.

        FWIW, I wrote the article and think that the tradeoffs Rails offers are still in user’s favor. I also think that the codebase can get much much better with some docs and better errors, but we can’t do that in a vacuum, we need active users who report issues and open up PRs.

        • Danny

          Thanks for the response! I’m glad to hear that, I haven’t looked at Rails 5 at all. The toughest thing for me when working on rails apps is that so much of the content I come across (both official docs and stackoverflow, etc) seems to be geared towards the beginner/hobbyist who wants the quickest possible solution and who isn’t so worried about edge cases, programming defensively, making things obvious and explicit for the next programmer who comes along, etc. I know there’s also a subset of the rails community writing solid production code, but it’s hard to push through the noise.

          I think more explicitness to prevent accidents and more documentation pointing out gotchas, production best practices, etc. will go a long way. For instance the behavior you mentioned of confirming rollbacks in production if it goes back > 1 migration seems like a great idea. I’ll keep this in mind if I see opportunities to submit docs updates and such.

          • msg7086

            I must have missed this thread. Thanks for all the responses. Glad to see someone willing to help to prevent these tragic things. I’d suggest that any STEP that’s beyond migration_table.size should trigger an error message unless overriding.

            (Actually I only write `up` now, leaving the `down` no-op.)

  • Vibhor Mahajan

    By convention,
    check = Check.new(amount: 1_000_000, blank: true)
    should look like
    check = Check.new(amount: 1_000_000, is_blank: true)

    So, typical usage would look like

    check.is_blank? and not check.blank?

    • Vibhor Mahajan

      Rails is based on convention over configuration. It means that there are ways to screw up if you don’t know the conventions. It might not be fit for all kinds of projects but the productivity boost is real

  • EricS

    This really stood out for me…”Sufficiently advanced programming magic is indistinguishable from a good interface.”

    I’d love for you(all?) to do a deeper dive into this aspect of writing great “magic” code.This deserves greater elaboration. This, for me, was the lightbulb switched on moment of the article and I think it’s going to help me immensely to think of it this way.

    The three rules are great touch stones, but creating magical code that is “…indistinguishable from a good interface” is where the money is. I know it’s out of scope for this article but would love to hear more insights from Codeship on this (Google search results aside).

    Also, to riff a little on what was said here…I think the best magic code is the one you don’t think of as magic at all. For instance, who thinks of our favorite programming language as magic code? But it is. All of what we do as programmers is built off magic code that abstracts out common patterns and behaviors. No one bitches about magic code until it bites them in the ass.

    So, I’d rather think of it as “bad magic” vs “good magic”. There’s a huge difference between Penn and Teller and those who accidentally kill their assistants.

  • Pingback: Container-Ready Rails 5 | 神刀安全网()

  • Pingback: Magie beim Programmieren – Fachinformatiker Anwendungsentwicklung()