When there's a large skill gap between two or more pairing developers, it can become a frustrating experience for everyone involved. I suspect this happens more often when more junior folks are at the keyboard.

Experienced developers have learned a high level language to communicate with their peers about problems that their less skilled partners may not yet be fluent in. If they are forced to downgrade from, "Let's make a map," to, "Type a percent sign followed by an opening curly brace," they are now operating with one brain tied behind their back. That's the simple stuff too. Rope data structures, Schwartzian transforms, and adding a custom GenServer to the application's supervision tree are right out!

The junior is not in a better position. They often feel like their job is to understand as much as possible of what the other person is doing to learn from it. Of course, it's common for most problems to quickly exceed their capacity for taking things in and, once they are overwhelmed, it's easy to feel like they can't bring any knowledge they've acquired to bear in a useful manner. It's totally understandable if they essentially shut down in such circumstances. The hope of learning is no longer on the table.

The good news is that I believe there is a better and easier way.

Better Goals

Juniors, I've got great news for you! You don't need any skill to be useful to another developer. In fact, you could be an inanimate object and still help. It's also perfectly normal for a less skilled developer to pair with a more skilled developer. There are entire strategies for how to do this well, like Backseat Navigator.

So the bar is low. You don't have to expect a ton from yourself. However, the great thing about low bars is that they are pretty easy to step over. With a few simple tricks, you can be out performing precious rubber ducks in no time.

Here's what you need to do.

First, stop trying so hard to keep track of everything that's going on. In fact, especially in the beginning, just don't. It may seem counter-intuitive, but you can't remember to be useful when you're struggling not to drown.

My daughter tells this hilarious story about another girl on her swim club, that we will call Sam. When Sam told the coach that she had signed up to swim the breaststroke in an upcoming meet, the coach said, "Sam, why would you do that? You need to think about me! You suck at the breaststroke and I have to watch you swim it!" That's great advice, you juniors need to think more about me, or at least your pairs.

Here's how you do that. You develop small scripts of preprogrammed actions that you're going to take when certain cues are met. You will know when to do this by listening for magic words that are extremely common in programming. Whenever you identify a set of these magic words, you'll begin alternating between prewritten questions and purely mechanical actions. Neither of those require much thought and they are tuned to maximize the benefit your pair is receiving.

Don't worry, this doesn't mean you aren't going to learn anything anymore. As time goes on with you practicing these simple scripts, they should slowly fade into the background and free you up to participate more and more in the actual process. Even before you are ready to lend a hand, you can learn just by watching the experts work.

Starter Scripts

Below you will find five scripts that I feel are useful in pretty much every pairing scenario. While they certainly don't cover everything you will face when programming, you should be able to get considerable mileage out of these starting points.

Magic Words: "Let's define a function…"

Variations

  • "We need a function…"
  • "We need to name this operation to make it more obvious what it does…"
  • "Let's extract this code…"
  • "We need to be able to reuse this in several places…"

Actions

  1. Question: "Which module does it belong in?" / "In this file?"
  2. Mechanical: Add the boilerplate code for a function (def … do … end in Elixir) in the proper file.
  3. Question: "What should we call it?"
  4. Mechanical: Type the selected name after def and space, then add an empty set of parentheses to hold the parameters.
  5. Question: "How many arguments will it take and what should we name those?"
  6. Mechanical: Add the parameter definitions with commas between them.

Discussion

Creating a chunk of reusable code is one of the most fundamental actions in all of programming. It's universal and we do it all the time. What's great is that it's harder than it looks, which makes it a perfect opportunity for a pair to lend a hand in easing the burden.

It may sound silly, but if your pair utters, "we should probably extract this," and you just start walking them through the steps like you're a government employee helping them fill out an application, you will seem like a mind reader. Even better, we've baked in the hard hitting questions like naming. Naming things is probably the worst never-ending responsibility that programmers wrestle with. You reminding them when it's time to take that step and making space for the ensuing discussion is the very definition of helping them do their job.

Remember that it's on the person at the keyboard to take care of syntactical details like placing parentheses around the parameter list and separating those parameters with commas. Practice those steps until they are truly mechanical!

Growth Opportunities

As you gain practice and skill applying this script, you can provide more help along the way. One of the best ways to do that is to start participating in the naming discussions. Remember that functions are the actions or verbs of the programming world, so try to think of words like that which fit your current situation. What if you don't know what your current situation is? Ask! "What will this chunk of code actually do in plain English?" If you listen closely, the answer will likely be in your partner's explanation.

If your project uses type specifications, add-on questions about them and a step to put them in place. Aside from dialing in the parameters further, this is the perfect chance to think through the possible return values before you implement.

If your programming language supports multiple function heads, this script will also cover a significant amount of the code's conditional logic. You can merge in the later branching script to kill two birds with one stone. See that script for details.

Magic Words: "Let's iterate over these items…"

Variations

  • "We need to walk over the collection…"
  • "We have to remove those items from the list…" (Enum.filter(…))
  • "We have some X, but what we really want is some Y…" (Enum.map(…))
  • "We need to know how many are…" (Enum.count(…))
  • "Let's group those together…" (Enum.group_by(…))
  • "Now we need to summarize all of that…" (Enum.reduce(…))

Actions

  1. Question: "Which iterator do we need?" (Skip this if the magic words gave it away.)
  2. Mechanical: Start the function call (Enum.whatever(…)).
  3. Mechanical: Open the documentation for that function, if your editor does not show you which parameters are needed.
  4. Question: "What are we iterating over?" (Again, skip this if you already know.)
  5. Mechanical: Just inside the parentheses type the name of the variable holding the collection that you are iterating over.
  6. Question: "What do we pass in this argument?" (Repeat this and the following step for any arguments before the anonymous function.)
  7. Mechanical: Type a comma followed by the argument or expression.
  8. Question: "And what do we need to do in here?" (Only if the iterator needs an anonymous function.)
  9. Mechanical: Type fn … -> … end, and then work through the function definition script above, skipping the naming bit.

Discussion

The other half of defining functions is calling them, and we do that even more often. While you could absolutely define a generic function calling script, it may be more useful to think of them in terms of specialized operations. Here the focus is on iteration, which is often very similar across many of those calls. Other areas worth scripting might be string parsing, mathematical operations, and data manipulation.

Also remember that the largest aspect of iteration is typically defining the anonymous function that the iterator will call. As noted in the script, we already have an entire process for resolving that. Don't forget to use it!

Growth Opportunities

Work out similar scripts for the other common uses mentioned above, a generic function call script to fall back on, and any other common use cases specific to your work. At my company, database queries would definitely make the cut, just to give one example.

As you master more and more of this mechanical process, turn your focus to trying to follow which data structure results from each iterator call. Maybe you start with a map, but drop it down to just a list of values, remove some of those (still a list), and then count (now a number). It's very common to sling several iterators together to accomplish some task, so getting to where you can follow what's popping out the other side will give you huge hints about what's most likely to come next.

Speaking of slinging iterators together, in Elixir and other languages like it, that probably means that you'll be building pipelines. See the script below for details on that.

Magic Words: "Now we need to handle each of those cases…"

Variations

  • "Let's handle some edge cases…"
  • "There are a couple of ways this can go…"
  • "We need to extract a few values…" (Use pattern matching.)
  • "Let's perform a few (boolean) checks…" (Use a cond statement.)

      # example
      cond do
        a_test?(var) ->
          
    
        another_test?(var) ->
          
    
        true ->
          
      end
  • "We should handle the errors this call can return…" (Use a case statement.)

      # example
      case var do
        {:ok, result} ->
          
    
        {:error, message} ->
          
      end
  • "There are a couple of inputs we could receive here…" (Use multiple function heads.)

      # example
      defmodule Serializer do
        def serialize(map) when is_map(map), do: 
        def serialize(list) when is_list(list), do: 
        def serialize(unexpected), do: 
      end

Actions

  1. Question: "Do we want to branch with case, multiple function heads, or something else?" (Skip this if the magic words gave it away.)
  2. Mechanical: Add the boilerplate code for the desired style of branching. (See examples above.)
  3. Question: "What's a case that we need to handle?" (Repeat this and the following steps until no more branching is needed.)
  4. Mechanical: Type the test and any needed boilerplate. (->, , do:, etc.)
  5. Question: "And what code do we need to run for that case?"
  6. Mechanical: Work out the conditional code with applications of your function calling, pipelines, and other scripts.

Discussion

While the iterators handle looping in most modern languages, the other half of flow control is conditional logic. A massive portion of programming is separating the various cases that need handling and applying the suitable code for each of them. Along with functions and iterators, this script should cover a large majority of the structure of any code written.

It may surprise some folks to see pattern matching listed above as a form of branching, but it often serves that role. A case statement is just a syntactical way to try several pattern matches until one fits. Multiple function heads provide the same service for function arguments. Even a bare pattern match is a type of branching with two possible outcomes: either it matches or an error is thrown.

This script assumes you will work through each condition before moving on to the next one. However, you may want to stay flexible. Sometimes it's easier to think through each of the cases that needs handling (writing all of the tests as you go) and then go back and add the handling code.

Growth Opportunities

Probably the biggest skill you can eventually grow into with this script is to merge it with the function definition script. In functional languages with pattern matching and multiple function heads, that's where a huge portion of the branching really happens. We tend to handle both needs at the same time: defining a chunk of code to handle the various possibilities that need handling.

It's okay if you're not great at that at first. Go ahead and define the case statement to keep the discussion rolling. When you're done, look to see if you just defined a function that contains nothing but a case statement. If so, you can almost surely rewrite it into a single function with multiple heads. That's a very mechanical process that makes for great practice!

Magic Words: "It's time to turn this into a pipeline…"

Variations

  • "Now pipe that into…"
  • "Let's run this through a couple of iterators to get what we need…"

Actions

  1. Mechanical: Remove the first argument of the function call and the trailing comma, if there is one.
  2. Mechanical: Place that argument on the line above.
  3. Mechanical: Insert a pipe operator (|>) before the function call.
  4. Question: "Should we keep piping?" (Repeat this and the following steps until the answer is no.)
  5. Mechanical: Add a line below the current one and start it off with a pipe operator.
  6. Mechanical: Work out the next function call using the appropriate script.

Discussion

This script is extremely specific to Elixir or languages like it, but the concept is universal. In Elixir, we structure a large amount of code around pipelines. When you can identify features like that for any language, developing a quick script gives you a very repeatable mechanical process that continually dumps you into what comes next. That guides us through the process of constructing something.

Growth Opportunities

As your comfort grows with this script, start scanning for places in the code where you have assigned several variables in a row. It may even be the same variable over and over again. Those sections are probably crying out for a little pipeline magic. Transforming them is another great practice opportunity.

By the way, it's very okay to perform these acts of manual practice while your partner watches on. It buys them some valuable thinking time while they don't have to worry about what operation is happening right now.

As your pipelines grow more complex, you can learn to transition into with statements. A with statement is a supercharged pipeline that can abort early, pipe into arguments other than the first one, carry forward more than one argument, and more. In time, you can develop a script for that and practice identifying the right times to make the switch.

Magic Words: NONE

Variations

  • "How do we begin?"
  • "What's next?"

Actions

  1. Question: "What are the overall steps we need to work through?"
  2. Mechanical: Outline the listed steps in code comments where you will soon be writing the implementation.
  3. Mechanical: Shift your focus to the first step you wrote down and consider if it's simple enough to start on now (meaning something like you know which function call to start with) or if you need more information.
  4. Mechanical: If you need more information, restart the script from the beginning but just focus on accomplishing this one small step.
  5. Mechanical: Implement the step using tools like your function calling scripts.

Discussion

Similar to the well known problem that authors face when staring down a blank page, there's not much scarier to the programmer than watching a blinking cursor in an empty editor window. What's next? You have literally infinite choices. Good luck!

We could talk about how to get past this problem for many blog posts. Folks already have. However, this script is one of the easier processes that typically gets me moving in some useful direction.

Let's work through a simple example. James is just sitting down to pair with Dana on reading in a large input file and using it to add records to the database. Let's listen in on their imaginary conversation.

James: "What are the steps we need to take to get this done?"

Dana: "It seems like we will need to read in the file, validate and transform the records found inside, then add them to the database."

James: "That's sounds great." (James starts typing.)

  # 1. Read file
  # 2. Transform records
  # 3. Validate records
  # 4. Save to database

James: "I split out transform and validate, and also thought we might want to do them in this order. We may change that as we go. I'm thinking about reading in the file. Is it as simple as one call to File.read or will we need to do some parsing?"

Dana: "Well, we need to deal with each line individually. Also, it's in the CSV format so we need to separate that to find the fields. I guess you could say it will need some light parsing."

James: "Got it." (Resumes typing.)

  # 1. Read file
  #    a. Read input one line at a time
  #    b. Decode CSV
  # 2. Transform records
  # 3. Validate records
  # 4. Save to database

At this point, James feels that he knows enough to start writing some code. If he didn't, they could keep breaking down the steps.

I tend to drop the code in directly below the line of the outline where it tells me what to do. I'll later go back and purge the comments when I feel they are no longer useful. You could also choose to replace the lines as you go.

Remember that these steps are a starting point, not a pact signed in blood. If you discover new steps or feel the need to modify steps as you work, that's great! It means the process is working. Once you've started the wheels rolling, you can more easily understand what actually needs doing.

This script is useful at all beginnings. Starting a project, defining a new function body, working out one clause of a branching statement, and much more. It ties in with all of the scripts above and more.

Growth Opportunities

In the beginning you may feel the need to ask very specific questions and type exactly what your pair dictates. That's totally fine. It's okay to let your pair guide the process.

As you gain experience, you may feel more able to insert steps, rearrange the order, or otherwise contribute to the plan. I've tried to show how this might go in the example above.

Sans Keyboard

Scripts can remain useful when you're not the person driving the keyboard. You can still ask all the leading questions at the key moments. Simply skip over the mechanical steps as your partner will be handling those.

Again, this is still helpful. The goal of a pair is to share some of the mental load of creation. Just prompting for the right times to consider the right things provides that.

Since you won't need to worry about mechanics in this role, try to provide more help in discussions like naming or all of the cases that need handling. You don't have to have all of the answers. Ask as many probing questions as needed to help draw them out of your partner. You'll learn a lot from watching them go through that process.

Add You Own Scripts

The scripts above are just meant to get you started. They represent generic needs that should exist in pretty much any programming context. However, you know your work way better than I could. It's important to recognize tasks that you always end up doing and to try codify those into the steps involved. Going through that process will help you turn overwhelming interactions into comfortable learning scenarios.

Use the sample scripts above as starting points. If you are building a script for time zone conversions, start with the iterators script. Both involve functions to manipulate related data types. Translate the magic words and questions into things you've heard your coworkers say. Add mechanical steps for the things you've seen them do. Most importantly of all, if you can't work out what they are doing in a given step, definitely ask! That's the whole point. You are trying to gain insight into how they are working through what needs doing.