Sunday, July 22, 2012

"I just figured out how to do this!" does not constitute a tutorial.

This is my biggest blog-reading pet peeve; authors who show little, say much, or seem oblivious to alternative solutions--in some way or another, they do not provide a good argument. Yet, they still expect you to agree with them. The worst part, for me, is when i see "tutorial" in the title. Often fallowing that, long paragraphs explaining how and why the fallowing code does what it does, is what it is, and on and on. We have all probably read something like this at one point or another and the format has a lot of problems.

It may not be honest. The author may have no authority on the subject. If you write a library, you may write a small tutorial that succinctly shows its usefulness--you certainly are the authority of your own library. In doing so you should be able to provide short examples that get the user from point A to point B, and expose enough of the API for the user to go beyond the tutorial. But after finishing a tutorial on "how to build a tile-based map" or "how to calculate the area of a polygon", i generally only expect to leave with the knowledge of one person's idea that they had for solving the issues they needed solved for their implementation, without the ability to expand on their ideas or understand the problem domain and invent new solutions. These types of articles rarely instill a good understanding of the theory they profess and instead show off just one implementation. If such a tutorial can't be copied and pasted, then one needs to write their own implementation and understand the underlying theory anyway.

The sharing of such information is in no way a bad thing, but it's not really a tutorial about how to get from A to B, is it? It's really about how the author got from A to B--her or his personal journey. The authors should therefor not spend time in theory and skip straight to implementation. What I see is the steps to reproduce an experiment.
  1. Observe some problem, or behaviour. (observation)
  2. Try to understand or describe the problem. (hypothesis)
  3. Try to solve the problem, based on your understanding. "If i write this code, then ... should happen" (predictions)
  4. Compile and run. (testing)
  5. Debug (go to step 2) until it works how you expect. (analysis) 
(For a graph, see: http://physics.ucr.edu/~wudka/Physics7/Notes_www/node6.html)

That people want to share their experiments is terrific. It's how scientific communities thrive. They just aren't tutorials.

Readers of these articles may be beginners, in which case we have the blind leading the blind. The given solution may lack an idiomatic solution and its readers may not know any better. If a considerable proportion of tutorials were written by new programmers, then the experience, wisdom, and knowledge of the old programmers would be lost. I see this especially in newer languages where few or no veterans exist that maintain accessible blogs, but there is no shortage of new programmers, excited ones who want to learn everything.

Overly verbose instructions make them hard to fallow. The worst part of these blogs and articles is their verbosity. If an article wants to offer something useful, show, don't tell. Requiring long explanations of abstract concepts preliminary to understanding the fallowing code, or seeing why it's sane, indicates how little usefulness the concept really has. The author is attempting to do two things at once: teach you fundamental concepts and show you how to use them. If the author doesn't successfully give you all the knowledge you need to know, then the code is useless. For example, i would not recommend explaining how to make a binary tree using templates and std::unique_ptr by first explaining how to use templates, then how to use std::unique_ptr, then explaining binary trees.

It's pretty fucking egotistical. The article is all about the author's code, of course. It's supposed to be. There is good content in there, but the reader has to sift through the author's long paragraphs about how/why the code works. This basically gives him or her free licence to spend time talking about her or his own code. That's fine, but not when it goes into length about theory, it goes back to being dishonest.

At the end of the day, it's a good thing that we share our experiments, ideas, and beliefs with each other, but we shouldn't treat showing off our code as instructional. We should instead admit that we like showing off code for the sake of showing off code. We like to point at problems and say "look how i solved that!". Let's be more honest about it.

Friday, June 15, 2012

Mapgen in C, C++, LISP, Haskell, Factor, and Forth

I set out to pick up a few new languages (the first being LISP), but my overall goals require time, too. I want to make a simple roguelike and one simple sub-problem in doing so is generating a map. Rather than writing a library to generate a map, i'd prefer to have an application that does it instead.

Here's a sample:

mapgen$ ./mapgen -n 3 -d '(30,30)'                                                                                                


##############################
##############################
##############################
##############################
#######################.....##
#######################.....##
#######################.....##
###############.............##
###############.#######.....##
###############.#######.....##
###############.#######.....##
###############.########.#####
###############.########.#####
###############.########.#####
###############.########.#####
###############.########.#####
###############.########.#####
###############.########.#####
###############.########.#####
###############.####.....#####
###########......###.....#####
###########......###.....#####
###########......###.....#####
###########......###....######
###########......###....######
####################....######
####################....######
####################....######
##############################
##############################





For this simple test, i don't care that the rooms overlap--this just puts (-n) 3 boxes of '.' chars in a map that's (-d) 10x10 tiles. What fallows is a quick look at how the experiment went. You can look at all the implementations here. Among them, C and C++ were my only string languages. I started on a Python version (my first language), but it felt unnecessary. I'd appreciate feedback on any of these examples so that they may be improved.

The problem is overly simplified and some of the implementations are more complete than others, but i thought someone might find it useful to see how one can solve the same problem with a bunch of different languages. Rosetta Code lets you do this, but only for examples even more trivial.

Size and Complexity

Line counts are notoriously bad at measuring code quality, but it does give some useful information.

  • C++ : 374 (574 if you count my pre-built vector class)
  • C : 176
  • LISP: 75
  • Haskell: 125
  • Forth: 96 (incomplete)
  • Factor: 123
mapgen.cpp actually has 134 lines, but the overhead of writing a generic Grid class (which might get used later) and iterators to go along with it had a cost. However, some functions were almost entirely reduced, thanks to this method. For example, dig_room() takes a room (a struct with the fields left, right, up, down) and replaces every wall tile with a floor tile. In C:

void dig_room( Grid g, const Room r )
{
    int x, y;
    for( x = r.left; x < r.right; x++ ) {
        for( y = r.up; y < r.down; y++ ) {
            Vector v = { x, y };
            *grid_get( g, v ) = '.';
        }
    }
}

And C++

void dig_room( const Room& r )
{
    std::fill( mgMap.reg_begin(r), mgMap.reg_end(r), '.' );
}

In Grid.h, i was able to define an iterator the incremented over a room and any function that requires such an iteration will be able to use it easily. However, in C, i will probably use the above loop in every necessary instance.

Theoretically, the templated Grid and generic iterators should save lines of code and developer time down the road, but without the ability to run wild with that in C, i made a very succinct Grid that required almost no time to make at all (in fact, mapgen.c only took an hour to write, if that).


void init_grid( Grid* g, Vector dims )
{
    const int AREA = dims.x * dims.y;
    
    g->dimensions = dims;
    g->tiles = malloc( AREA );
    memset( g->tiles, '#', AREA );
}


void destroy_grid( Grid* g )
{
    free( g->tiles );
    g->tiles = 0;
}


char* grid_get( Grid g, Vector p )
{
    return g.tiles + g.dimensions.x*p.y + p.x;
}


Comparing LISP and Haskell, i find that Haskell's purity added to the amount of work done. 



splitGap :: Int -> Int -> [a] -> ([a],[a],[a])
splitGap start size lst = (before, middle, after)
  where 
    (before,rest) = splitAt start lst
    (middle,after) = splitAt (abs size) rest


digRow :: Range -> MRow -> MRow
digRow (start,end) row = 
  before ++ replicate size TFloor ++ after
  where
    size = end - start + 1
    (before,_,after) = splitGap start size row


digRoom :: RMap -> Area -> RMap
digRoom rmap ((x,y),(u,v)) =
  ybefore ++ map (digRow (x,u)) rows ++ yend
  where 
    (ybefore,rows,yend) = splitGap y (v-y+1) rmap

verses


(defun dig-at (m x y)
  (setf (row-major-aref m (+ x (* y (array-dimension m 0)))) #\.))


(defun dig-room (map room)
    (loop for j from (area-up room) below (1+ (area-down room)) do
          (loop for i from (area-left room) below (1+ (area-right room))
                do (dig-at map i j))))

While Haskell's random monad also produces several extra lines, like in C++, some algorithms get reduced in code-size thanks to higher-level featuers:


(defun splatter-pattern (map n)
  (let* ((height (array-dimension map 1))
         (width  (array-dimension map 0))
         (rooms (loop for i from 1 to n 
                      collect (random-area width height))))
    (loop for i from 0 below (length rooms) do
          (dig-room map (elt rooms i))
          (when (> (-(length rooms)i) 1)
            (let* ((a (random-point (elt rooms i)))
                   (b (random-point
                       (elt rooms (randr (1+ i)
                                         (- (length rooms) 1))))))
              (dig-hallway map a b))))))

verses



splatter :: RandomGen r => Int -> r -> RMap -> RMap
splatter n gen m = 
  digRandomHallways (foldl digRoom m rooms) g2 rooms
  where 
    (g1,g2) = split gen
    rooms = take n $ randomRooms g1 (length (m!!0),length m)
    center ((x,y),(u,v)) = ((x+u) `quot` 2, (y+v) `quot` 2)

Next: Forth and Factor. For those who don't know, Forth is from the 70's and Factor is based on a recent academic language, Concat. Concat seems very Forth based, and so Factor and Forth seemed like logical analogues. It seems like this type of language gets ignored almost entirely by the mainstream community, and in research, but i find fascination in them.  Factor, in particular, which shows a working high-level, functional Forth, with first-class functions.

Unfortunately, one unfamiliar with these languages will find the fallowing source code indecipherable.

Forth:

char . CONSTANT FLOOR
...



: ij>tile *width* * + *tiles* + ; 
...


: (dig) ij>tile FLOOR swap C! ;
: dig { room }
    room down  @ 1+ room  up  @ DO 
    room right @ 1+ room left @ DO 
        I J (dig)
    LOOP LOOP ;

Factor: (comments removed) 
: ij>tile ( i j -- i' map   ) *width* * + *map* get ;
...

: left-right ( room -- l r ) [ left ] [ right ] bi ;
: up-down    ( room -- u d ) [  up  ] [ down  ] bi ;
...

: (room-map) ( room quot: ( i j -- ) -- )
    [ [ up-down [a,b] ] keep ] dip 
    '[  _ left-right [a,b] swap
        _ curry each ] 
    each ; inline recursive

: (dig) ( i  j -- ) [ FLOOR ] 2dip ij>tile set-nth ;
: dig   ( room -- ) [ (dig) ] (room-map)  ;

Again, we see that we have written more lines in the higher level language! However, Factor's left-right is used in several places and adds clarity, though admittedly, (room-map) just felt like a good thing to implement when i could have simplified it by not making a generic map function. (The idea that something like that would be useful violates the YAGNI principal.) 

But both my Factor and Forth solutions could possibly be improved, i didn't invest as much time as i would have liked to.

Even though higher level languages tend towards more lines in this small example, their theory lies more in scalability. But then again, in compared to C, there's no reason i couldn't have done the exact same thing in C++.

Future Plans

For my roguelike, mapgen will read stdin to initialize the options for constructing the map. For now, the mapgen repository acts as a scratchpad for trying different approaches. C has probably been my favorite so far. While i enjoy sculpting a clean interface in C++, C shines in its simplicity. Russia's pencil to NASA's zero-gravity pen. (But you can't blame me for thinking a zero-gravity pen is neat!) 

I want to continue to develop the C and C++ versions in parallel, but at least one other as well. Of all the languages i tried, i had the most hope for Factor. Stack-based languages take a unique approach that piques my interest. Programmers commonly write procedures that take one variable, convert it to an intermediate, and then an output value. In an imperative language, it might look like this:

a = f()
b = g(a)
c = h(b)

Functionally,

c = h( g( f(a) ) )

But, stack based:

a f g h c

This leads to interesting quirks, and it's fun to play around with. Stack manipulation is like a puzzle on top of your otherwise normal, every-day algorithm. It can lead to cryptic functions and terse generalizations, but still, i think deserves much more attention.

And yet i don't plan to continue to continue implementing either Forth or Factor. I will learn them and keep an eye of the development of concatenative languages.

Conclusions

If high level languages do provide productivity gains, then they should scale better than lower levels. (I.E., given enough complexity, the C implementation would grow larger than C++, LISP larger than Haskell, and Forth larger than Factor.) Still, for a short program, low-level languages show an elegant brevity.