Software Philosophy

Literate programming really works for tutorials

 #literate programming   #tutorial   #example project   #markdown   #generated   #html   #css   #javascript   #node   #writ   #gulp   #jshint   #github   #github pages 

2016-09-19

Have you heard about literate programming? It's a technique that lets you write a computer program the same way as if you were writing a piece of literature. And while it may seem exotic and unnecessary, there are some cases when it's quite useful. I believe I have found one of them. In this post, I will describe a solution for writing a tutorial in the spirit of literate programming by using nodejs-based, language-agnostic, literate programming tool.

Background

Literate programming is a technique of writing computer programs in natural (spoken) language. When using this technique, documentation receives most of programmers attention while code is treated as a formality. Code snippets are typically embedded in the essay and get extracted for compilation during project build.

The main idea is to regard a program as a communication to human beings rather than as a set of instructions to a computer.

~ The CWEB System of Structured Documentation by Donald E. Knuth and Silvio Levy

First literate programming tool, developed by Donald Knuth, was called WEB and worked with Pascal only. Short after, Knuth developed CWEB, which is WEB for C language. In response to WEB, Norman Ramsey created noweb—a literate programming tool compatible with any programming language. It started a language-agnosticity trend in the field.

Research

So, I was faced with a task of writing a tutorial on using hyper-text-slider with node and browserify. Since I have this habit of making my life more exciting, and since writing a tutorial is an inherently dull task, I started thinking. From this thinking emerged a question​—Is it possible to automatically test a tutorial?

After few minutes of Googling around, I had my answer—literate programming.

HyperText Slider is a web slideshow component that utilizes CSS3 transitions to control background and foreground animations of changes between slides. It has declarative (HTML-only) API which is very easy to use, and JavaScript API for fine-grained control. Using hyper‑text‑slider with node and browserify means using a node-base build system for front-end technology stack (HTML, CSS and JavaScript). I needed a literate programming solution that supports front-end stuff and has a plugin for gulp (my build utility of choice for node-based projects).

As it turns out, number of literate programming solutions for JavaScript in suprisingly big (npm search literate gives 288 results). I really disliked docco which seemed to be the most popular one. It was advertized as quick and dirty yet it was still too complicated for my taste. After trying a few, I ended up choosing writ. It does only one thing—extracts any code from Markdown (with very basic support for macro-definitions).

Solution

  1. Documentation (tutorial), human-written, contains snippets of code needed for a website to be built. It is a single source of information (actual sources). Tutorial is written in markdown and hosted on Github which automatically renders markdown files into nice HTML documents.
  2. Code snippets are extracted from the documentation by invoking writ from a gulp script. Writ produces one output file for each markdown file, so the tutorial must have been divided into multiple pages—3 source files (HTML, Sass and JavaScript), a shell script that sets up the project, and a gulpfile that builds generated sources. There actually are 2 gulpfiles in the project. First that invokes writ (human-written), second that builds the project (generated). Extracted JavaScript and Sass code is linted by invoking linter plugins after writ. All generated sources are checked into the source control so that the project can be cloned and built.
  3. Project is built by invoking gulpfile script which was extracted from the documentation. Building process produces browser-runnable files (HTML, CSS, JavaScript), which are also checked into the source control. Github Pages is active with master branch set as source so that compiled project can be run in the browser.

This way, I'm killing three birds with one stone. The project is:

  1. A tutorial on creating a web slideshow using hyper-text-slider, node and browserify,
  2. An example project which can be cloned and compiled,
  3. A website which shows live results of the compilation.

Configuring Writ

There are two gulpfiles in the project. Generated one has the default name (gulpfile.js). Handwritten one must be named differently (.writ.gulpfile.js).

NOTE

Below paragraphs require basic understanding of gulp. If you're not familiar with it, please read Build Configuration chapter from the tutorial. If you know gulp, it may still be useful to read it as it contains soure code of generated gulpfile.

We need gulp, gulp-util, gulp-writ, gulp-rename2, gulp-eslint, gulp-stylelint, merge-stream and del modules (they must be installed with npm install in order to make this gulpfile work).

'use strict';

var gulp = require('gulp');
var gutil = require('gulp-util');
var writ = require('gulp-writ');
var rename = require('gulp-rename2');
var eslint = require('gulp-eslint');
var stylelint = require('gulp-stylelint');
var merge = require('merge-stream');
var del = require('del');
var child = require('child_process');

The project contains following folders:

  • doc/ containing the tutorial,
  • src/ containing generated sources,
  • dist/ containing build output files (browser-comsumable).

As it will be used as a dependency of generate, clean task must be defined first. Deleting files with del module requires using gulp in asynchronuous mode (task function has a callback which is called by del). We want to delete all files generated in previous run of writ which are contained in src folder, but without deleting the folder itself.

gulp.task('clean', function(callback) {
  return del([ 'src/**/*', '!src/' ], { force: true }, callback);
});

The tutorial consists of following files:

  • doc/0_introduction.markdown,
  • doc/1_setup.sh.md,
  • doc/2_gulpfile.js.md,
  • doc/3_script.js.md,
  • doc/4_index.html.md,
  • doc/5_style.scss.md.

First file (0_introduction.markdown) does not contain any code snippets so it has different extension. This way, it is still properly rendered on Github yet not catched by writ's glob pattern.

Each file is prefixed with a number which indicates reading order of the tutorial. Numbers also keep the files properly ordered when listed on Github. gulp-writ plugin removes .md extension but we need to use rename2 in order to remove number prefixes. setup.sh and gulpfile.js will be placed in project main folder while all generated sources will be placed in /src.

function removeNumberPrefix(pathObj, filePath) {
  return pathObj.dirname(filePath) +'/'+ pathObj.basename(filePath).replace(/^[0-9]+_/, '');
}

gulp.task('generate', [ 'clean' ], function() {
  var sources = gulp.src([ 'doc/*.md', '!doc/*.sh.md', '!doc/*gulpfile.js.md' ])
    .pipe(writ().on('error', gutil.log))
    .pipe(rename(removeNumberPrefix))
    .pipe(gulp.dest('src/'))
  ;
  var setupAndConfig = gulp.src([ 'doc/*.sh.md', 'doc/*gulpfile.js.md' ])
    .pipe(writ().on('error', gutil.log))
    .pipe(rename(removeNumberPrefix))
    .pipe(gulp.dest('.'))
  ;
  return merge(sources, setupAndConfig);
});

Generated sources are ready to be linted.

gulp.task('lint:sass', [ 'generate' ], function() {
  return gulp.src([ 'src/*.sass' ])
    .pipe(stylelint({
      reporters: [ { formatter: 'string', console: true } ],
    }))
  ;
});

gulp.task('lint:javascript', [ 'generate' ], function() {
  return gulp.src([ '*.js', 'src/*.js' ])
    .pipe(eslint())
    .pipe(eslint.format())
    .pipe(eslint.failAfterError())
  ;
});

gulp.task('lint', [ 'lint:sass', 'lint:javascript' ]);

Linted sources are ready to be built. In case of this tutorial, building the project means invoking another gulp process with generated gulpfile.

gulp.task('dist', [ 'lint' ], function() {
  child.execSync('gulp');
});

Running Writ

Invoking generate task extracts sources from the tutorial.

gulp --gulpfile=.writ.gulpfile.js generate

Invoking dist task extracts sources, lints them and builds the project.

gulp --gulpfile=.writ.gulpfile.js dist

Above command is equal to invoking lint task and then running gulp with generated gulpfile.

gulp --gulpfile=.writ.gulpfile.js list
gulp

Invoking dist task after changing the documentation will propagate changes into src/ and dist/ folders. It should be invoked before each commit.

Wrap Up

After setting everything up, all that remains is writing the tutorial. It will be automatically tested in a sense that contained code snippets will be linted and checked if they compile successfully. Resulting program must still be run by a human, but it's much less work than manually testing each step of the tutorial.

Implementing a watch task would add more automation into the process It's there in my tutorial's .writ.gulpfile.js, but as it's not essential, I haven't include it this blogpost.

I must say that writing a tutorial using literate programming was an exciting experience. Writ is a great, simple tool for literate programing in Markdown. I really recommend using it. I'm curious of your opinion on literate programming and writ. If you have any thoughts to share or questions to ask, please do in the comments below.

References

  1. literateprogramming.com is a website that gathers literate-programming-related white paper fragments,
  2. In his original paper on literate programming, Donald Knuth describes first attempt of implementing this concept in a framework called WEB,
  3. Literate Programming—a book by Donald Kunth—contains a collection of his papers on this technique alongside some new material,
  4. Cunningham & Cunningham Wiki contains an interesting discussion on literate programming.
  5. Haskell Wiki contains information on how to do literate programming by using only Haskell's native features,
  6. Github Pages Help website contains information on how to set a source branch for Github Pages in your project,
  7. Tutorial described in this blogpost is hosted on github.

Maciej Chałapuk

blog comments powered by Disqus