2007-08-06

RWEB 0.1.0

RWEB is a literate programming environment for Ruby. (Much of it is language-agnostic, but its focus is Ruby.) It is patterned somewhat after Donald Knuth's WEB literate programming tool, as well as its descendants, but has a couple of added twists in planning. When it is complete it will feature:

  • a utility, rtangle, which "tangles" an RWEB document to produce executable code in the format expected by the Ruby interpreter;
  • a second utility, rweave, which "weaves" an RWEB document into human-readable text in a variety of back-end formats (plain text and XHTML planned for the moment, but other formats are likely as well as a user plug-in system for end-user customization);
  • a facility to allow RWEB documents to be directly executed by the Ruby interpreter as if they were native Ruby code.

As of version 0.1.0's release, all three features are in place in "proof-of-concept" condition. The self-executing RWEB feature is working and unlikely to change. The tangling code, too, is complete and unlikely to visibly change. The weaving code is in place for plain text formatting only with an XHTML interface in place but incomplete and stubbed out. The back-end plug-in system is not even vaguely in place yet.

RWEB 0.1.0 is available for download from RubyForge and the documentation too.

For a brief taste of what RWEB can do at the moment, consider this simple, but complete, RWEB document, broken down for discussion.

#! /usr/bin/ruby -w
require 'rubygems'
require 'rweb'
eval RWEB.tangle(DATA)
__END__



This header of boilerplate is the trick that allows self-executing RWEB documents. It's a cheap trick, really, that just requires the RWEB library and then evaluates the string returned by tangling the special variable DATA. Since DATA is an IO object that contains everything after the __END__ directive, it means that it tangles and executes everything after __END__.

{style => Plain}
{title => RWEB Demo}



This portion is a set of directives. We're telling the weaver that the document style is plain text (the only one currently supported, although XHTML is syntactically supported) and the title of the document. Everything thereafter is the actual RWEB document.

This file contains a demonstration of the RWEB literate programming system for
Ruby. It is not held up as an example of good style, but only as an example of
the system's capabilities.

First we open up the mainline code. Note how the tag for opening the mainline
code has no name. Every RWEB document must have such a mainline "chunk". We
begin with just some requires.

<< {
require 'rubygems'
require 'rio'
}>>

Now this program uses the RIO library for no good reason. Let's define a
function that looks for RWEB documents and tangles them into Ruby scripts if
they haven't already been tangled. (We'll ignore dates for now.) It will also
weave documents for these.

<< utility functions {

{<<get rweb documents>>}

{<<get ruby scripts>>}

{<<find the differences>>}

}>>

Note that the contents of this chunk doesn't look at all like Ruby code. It is,
in fact, Ruby code, but only if the chunk references (contained in the {<<>>}
tags) are expanded. The blocks in question have not yet been defined, but this
isn't a problem. Chunks can be defined in any order. Only circularity is grounds
for complaint.

The functions for getting the rweb documents and ruby scripts are simple:

<< get rweb documents {
def rweb_documents
rio('.').files['*.rweb'].collect{|r| r.basename}
end
}>>

<< get ruby scripts {
def ruby_scripts
rio('.').files['*.rb'].collect{|r| r.basename}
end
}>>

The name of these chunks is inserted between the "<<" and the "{" tokens. Before
use the names have any leading and trailing whitespace truncated. This is for
purposes of readability.

Now let's take a look at comparing the two arrays:

<< find the differences {
def find_differences
rweb_documents - ruby_scripts
end
}>>

Now here we're going to re-open the mainline chunk. All this does is append any
lines in the re-opening to the alread-existing lines of the chunk. This re-
opening capability, combined with forward references, gives us lots of leeway
for structuring code and documentation for maximal comprehension.

<< {
{<<utility functions>>}

find_differences().each do |file|
system "rtangle #{file}.rweb #{file}.rb"
system "rweave #{file}.rweb #{file}.txt"
end
}>>


The rtangle output on this RWEB document looks like this:

# RWEB Demo
# =========

require 'rubygems'
require 'rio'
# utility functions
# -----------------

# get rweb documents
# ------------------
def rweb_documents
rio('.').files['*.rweb'].collect{|r| r.basename}
end

# get ruby scripts
# ----------------
def ruby_scripts
rio('.').files['*.rb'].collect{|r| r.basename}
end

# find the differences
# --------------------
def find_differences
rweb_documents - ruby_scripts
end


find_differences().each do |file|
system "rtangle #{file}.rweb #{file}.rb"
system "rweave #{file}.rweb #{file}.txt"
end


The output of rweave looks like this:

RWEB Demo
=========
This file contains a demonstration of the RWEB literate programming system for
Ruby. It is not held up as an example of good style, but only as an example of
the system's capabilities.

First we open up the mainline code. Note how the tag for opening the mainline
code has no name. Every RWEB document must have such a mainline "chunk". We
begin with just some requires.

RWEB Mainline
-------------
> require 'rubygems'
> require 'rio'

Now this program uses the RIO library for no good reason. Let's define a
function that looks for RWEB documents and tangles them into Ruby scripts if
they haven't already been tangled. (We'll ignore dates for now.) It will also
weave documents for these.

utility functions
-----------------
>
> {<<get rweb documents>>}
>
> {<<get ruby scripts>>}
>
> {<<find the differences>>}
>

Note that the contents of this chunk doesn't look at all like Ruby code. It is,
in fact, Ruby code, but only if the chunk references (contained in the {<<>>}
tags) are expanded. The blocks in question have not yet been defined, but this
isn't a problem. Chunks can be defined in any order. Only circularity is grounds
for complaint.

The functions for getting the rweb documents and ruby scripts are simple:

get rweb documents
------------------
> def rweb_documents
> rio('.').files['*.rweb'].collect{|r| r.basename}
> end

get ruby scripts
----------------
> def ruby_scripts
> rio('.').files['*.rb'].collect{|r| r.basename}
> end

The name of these chunks is inserted between the "<<" and the "{" tokens. Before
use the names have any leading and trailing whitespace truncated. This is for
purposes of readability.

Now let's take a look at comparing the two arrays:

find the differences
--------------------
> def find_differences
> rweb_documents - ruby_scripts
> end

Now here we're going to re-open the mainline chunk. All this does is append any
lines in the re-opening to the alread-existing lines of the chunk. This re-
opening capability, combined with forward references, gives us lots of leeway
for structuring code and documentation for maximal comprehension.

RWEB Mainline
-------------
> {<<utility functions>>}
>
> find_differences().each do |file|
> system "rtangle #{file}.rweb #{file}.rb"
> system "rweave #{file}.rweb #{file}.txt"
> end


This plain text version isn't perfect -- especially for, say, blogging, but it is a whole lot better than raw code ever will be. Too, as back ends get plugged in (XHTML is next on the queue), the documentation capabilities will be far superior to any other tool for documentation including even things like RDOC (or JavaDoc/Haddock/Doxygen) which use code comments to cook up documentation.

2 comments:

Richard Volpato said...

Hi.

Great to see this kind of work.

Ruby may indeed be highly expressive and supportive of all kinds of 'idioms', but for that reason, the literate programming tools provide another layer that can frame code with respect both overall aims and the local cunning of how code has been constructed.

I've been looking for anyone addressing this kind of problem. Great work: keep it up.

(btw, have you seen LEO, a literate programming / outliner for inspiration or use?)

Michael T. Richter said...

Thanks for the kind words. I have not seen LEO, no, but I will be correcting this oversight in my education forthwith. Thanks for the pointer.