Writing Good Documentation and Examples

2015-12-02

I was going to start this as a rant for how bad most docs are but instead I'll just list what I think are some best practices. I know no one will read this but what else can I do 😉

Make sure your docs actually work

I downloaded a new and popular text editor today. The first thing I tried to do, turn off the line numbers, I couldn't figure out. So I google it. The first answer is from the official docs on the official website of the editor. They say "open a console and type bla.bla.bla". So I open a console and type "bla.bla.bla" and I get an error. Not a great way to make a first impression if you want a sale or a adoption or whatever.

Keep your docs up to date!

Make sure you're explicit about requirements

Last week I downloaded an sdk. I follow the instructions to setup it up. It says "install java" and gives to link to the generic java sdk. I download the first one, the JDK7 x64. Install. Nothing works. Turns out I need the 32bit version of Java. It doesn't tell me this. I just guess because it references 'system32'. Thanks. I download and install JDK7 x86. I get further but later something dies again, no error. Turns out after writing customer service I need JDK6 32bit not JDK7 32bit. 2 other similar issues getting started. 3 hours later I'm tired, I haven't gotten to actually try the sdk. Guess what, I'm not going to try again tomorrow.

When writing examples, try to use variable names that don't suggest magic.

What I find is often people will look at some code and think that many of the identifiers have special meanings. So for example

  canvas.width = 300;
  canvas.height = 150;

Since there's an official HTML tag called a canvas as in <canvas> is that name "<code>canvas" significant? Am I setting the properties or some official object like document or window? If the code is very short and self contained like

  var canvas = document.querySelector("#c");
  canvas.width = 300;
  canvas.height = 150;

Then maybe it's ok but generally, at least for samples I try to pick names I hope make it clearer there's no magic

  someCanvas.width = 300;
  someCanvas.height = 150;

Don't use property names that suggest magic

   var model = new Model();
   model.texture = graphicsSys.createTexture();

Is model.texture a user property or some official property required by the system?

   var model = new Model();
   model.myTexture = graphicsSys.createTexture();

Maybe it's subtle but I'd argue this second style is clearer as an example. It's unlikely the library used a property starting with my.

Don't augment official objects

In the example above it can still be confusing. myTexture still looks like an official property of Model. We can fix that by making our own objects.

   var modelInfo = {
     model: new Model(),
     texture: graphicsSys.createTexture();
   };

Don't ever show bad practices (unless it's clearly labeled)

They'll get repeated FOREVER! Most people will just copy and paste your example. If it's using a bad practice they'll have copied and pasted that bad practice into their code.

Examples:

var tex = gl.createTexture();
tex.width = 256;
tex.height = 256;

This is a bad practice. gl.createTexture() can return null if the context is lost.

gl.uniform4f(gl.getUniformLocation("u_color"), r, g, b, a);

This is a bad practice, looking up locations is slow. 99% of WebGL programs render every frame so even if your example renders only once don't show people a bad example since they'll likely copy it into their code that does it every frame.

var ext = gl.getExtension("OES_texture_float");
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0,
              gl.RGBA, gl.FLOAT, someFloat32Array);

This is a bad practice. Extensions should ALWAYS be checked as they may not exist on the user's machine.

Don't use magic numbers

You could have code like this

    var perspectiveMatrix = makePerspective(
            60, width / height, 0.1, 1000);

Which makes perfect sense to me as I've done it 1000 times but the person reading my example has not. How about this?

    var aspectRatio = width / height;
    var fieldOfView = 60;
    var zNear = 0.1;
    var zFar = 1000;
    var perspectiveMatrix = makePerspective(
            fieldOfView, aspectRatio, zNear, zFar);

Consider labeling often confusing units

Some good examples are angles, are they in radians or degrees? Time, is it in seconds, milliseconds, microseconds?

If I see

  cube.rotation += rotationSpeed * deltaTime;

I have no idea what that means. On the other hand

  cube.rotationRadians += rotationSpeedRadiansPerSecond * deltaTimeSeconds;

I have a much better idea of what the units are and how I'd go about adjusting them. Whether you do this in your own code (I'm trying to do it more and more) it's certainly arguable it's good for examples. That means fieldOfView above should probably have been fieldOfViewDegrees.

If the example is doing something more than 2 or 3 lines comment it.

No, I don't do this in my own code but when writing an example I want to make sure everything is clear. Let's say I'm writing docs on how to load a texture in WebGL. I've written a paragraph explaining how to do it and now I have a sample like this

  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                new Uint8Array([0, 0, 255, 255]));
  var image = new Image();
  image.src = "resources/noodles.jpg";
  image.addEventListener('load', function() {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image);
    if (isPowerOf2(image.width) &amp;&amp; isPowerOf2(image.height)) {
       gl.generateMipmap(gl.TEXTURE_2D);
    } else {
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
    drawScene();
  });

The problem is this sample is going to be looked at out of context. Your new users may have only skimmed the text you wrote or maybe have arrived at your sample by someone sending them a link directly to your code. While some people believe no comments are best in real code (read the code!) comments are arguably GREAT in examples.

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Fill the texture with a 1x1 blue pixel.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                new Uint8Array([0, 0, 255, 255]));

  // Asynchronously load an image
  var image = new Image();
  image.src = "resources/noodles.jpg";
  image.addEventListener('load', function() {
    // Now that the image has loaded copy it to the texture.
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image);

    // Check if the image is a power of 2 in both dimensions.
    if (isPowerOf2(image.width) &amp;&amp; isPowerOf2(image.height)) {
       // Yes, it's a power of 2. Generate mips.
       gl.generateMipmap(gl.TEXTURE_2D);
    } else {
       // No, it's not a power of 2. Turn off mips 
       // and set wrapping to clamp to edge
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }

    // since the texture has changed, draw the scene again
    drawScene();
  });

Write self documenting code

I'm arguably terrible at this but .. Let's take the previous example where I added lots of comments. What if I did this?

  var texture = gl.createTexture();

  var blue = [0, 0, 255, 255];
  fillTextureWith1x1Pixel(texture, blue);

  asynchronouslyLoadImage("resources/noodles.jpg", function(image) {
    copyImageToTexture(image, texture);

    // since the texture has changed, draw the scene again
    drawScene();
  });

  function fillTextureWith1x1Pixel(texture, color) {
    var level = 0;
    var width = 1;
    var height = 1;
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                  new Uint8Array(color));
  }

  function asynchronouslyLoadImage(url, callback) {
    var image = new Image();
    image.src = url;
    image.addEventListener('load', function() {
      callback(image);
    });
  }

  function copyImageToTexture(image, texture) {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, image);

    setTextureFilteringBasedOnDimensions(image.width, image.height);
  }

  function setTextureFilteringBasedOnDimensions(width, height) {
    if (isPowerOf2(width) &amp;&amp; isPowerOf2(height)) {
       gl.generateMipmap(gl.TEXTURE_2D);
    } else {
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
       gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    }
  };

Now there's almost no comments but maybe it's clearer? I don't generally write my code like this but I'm trying to do it more and more.

Closing Thoughts

I'm not saying this is the only or one true way but at the same time it's hard to remember what it's like to be a noob. Everytime I see a someone new to using an API or library I worked on and I see the mistakes made I often see where I could have made the docs and samples better and hopefully they would not have been confused. I'm also not perfect at this and you'll find plenty of places where I've not followed these rules. Sadly, almost every time it's turned into more work for me down the line where I have to answer questions about why something isn't working and it turns out it's because they copied a bad example I wrote or because they didn't understand that I wasn't showing the best practice in that sample.

Comments
More JavaScript vs C#/C++
iPhone rant