gray kangaroo

A code-along is a story about coding in which you are encouraged to “code up” all the examples in the story. In a code-along you should not cut-and-paste.

In this code-along, we will explore how basic ideas from modular arithmetic can be used to create LEGO artifacts consisting of repeating patterns. More specifically, we will define brick functions whose outputs are controlled by predicates that evaluate to true for all points having attributes satisfying a particular congruence relation.

Consider the program shown below. The function isEven is a predicate that evaluates to true for all (x,z) values whose sum is congruent modulo 2 to 0; otherwise isEven evaluates to false. The function blackWhite is a brick function whose body consists of a conditional expression that evaluates to (i.e., returns) a BLACK brick in cases where isEven(x,z) is true and evaluates to a WHITE brick otherwise. The body of the function chessboard consists of a call to the traverseWithin function which iterates over the points in the set { (0,0,0), …, (8,0,8) } putting BLACK bricks at some points and WHITE bricks at others. The results is a chessboard pattern.

open Level_5;

fun isEven (x,z) = (x+z) mod 2 = 0;

fun blackWhite (x,y,z) = if isEven(x,z) then BLACK 
                         else WHITE;

fun chessboard () = traverseWithin (0,0,0) 
                                   (7,0,7) 
                                   blackWhite; 

build (8,1,8); 

chessboard();

show "chessboard";

level-5-ca-chessboard01

Code-along Program 1

The program below, is similar to the previous program which was used to create a chessboard. One difference is that congruence relation has changed slightly. However, the main difference is that the predicate for determining whether a point has attributes satisfying our congruence relation involves only the x value of the point. Let x1 denote a value for which property x1 evaluates to true. This implies that among the points traversed, all points whose x value is x1 will have attributes satisfying our congruence relation (e.g., (x1,0,0), (x1,0,1), (x1,0,2), and so on).

open Level_5;

fun bars () =
    let
        fun property x  = x mod 3 = 0;  
        
        fun buildBars(x,y,z) = 
            if property x then BLACK 
            else EMPTY;
    in
        traverseWithin (0,0,0) (6,0,6) buildBars
    end;

build(7,1,7);

bars();

show "bars";

level-5-ca-grid_01

Code-along Program 2

In this program the predicate for determining whether a point has attributes satisfying our congruence relation is involves the x and z values of the point. However, note that the predicate property has a body consisting of a Boolean expression involving the orelse operator. This means that the expression is less restrictive (more often true) than the expression in property of the previous program.

open Level_5;

fun bars () =
    let
        fun property (x,z)  = x mod 3 = 0 
                              orelse 
                              z mod 3 = 0;
      
        fun buildBars(x,y,z) = 
            if property (x,z) then BLACK 
            else EMPTY;
    in
        traverseWithin (0,0,0) (6,0,6) buildBars
    end;

build(7,1,7);

bars();

show "bars";

level-5-ca-grid_02

Code-along Program 3

In this program the predicate for determining whether a point has attributes satisfying our congruence relation is involves the x and z values of the point. However, note that the predicate property has a body consisting of a Boolean expression involving the andalso operator. This means that the expression is more restrictive (less often true) than the expression in properties of both the previous programs.

open Level_5;

fun grid () =
    let
        fun property (x,z) = x mod 3 = 0 
                             andalso 
                             z mod 3 = 0;  
           
        fun buildGrid(x,y,z) = 
            if property (x,z) then BLACK 
            else EMPTY;
    in
        traverseWithin (0,0,0) (18,0,18) buildGrid
    end;

build(19,1,19);

grid();

show "grid";

level-5-ca-grid_03

Code-along Program 4

This program involves a much more complex predicate. Placing blue bricks at points that satisfy the function property will result in the construction of a grid of 3D crosses. Note that the function property contains numerous redundant computations (we leave it to the reader to improve the code).

One thing that stands out in this example is the complexity of the property needed to create a single cross. Perhaps there is a better way.

open Level_5;

fun crossField () =
    let
        fun property (x,y,z) =
            let
                val up       = x mod 5 = 1 
                               andalso 
                               z mod 5 = 1;

                val sideways = x mod 5 < 3  
                               andalso 
                               y = 1 
                               andalso 
                               z mod 5 = 1;

                val back     = x mod 5 = 1 
                               andalso 
                               y = 1 
                               andalso 
                               z mod 5 < 3;

            in
                up orelse sideways orelse back  
            end;
        
        fun buildCross(x,y,z) = 
            if property (x,y,z) then BLUE
            else EMPTY
    in
        traverseWithin (0,0,0) (32,2,32) buildCross
    end;

build(33,3,33);

crossField();

show "crossField";

level-5-ca-grid_05

Code-along Program 5

In this example, the function cross builds a 3-dimensional cross-shaped artifact using calls to Level_4 put functions. Note that Level_5 is defined in such a way that it contains all of the Level_4 functions. Therefore, having a declaration that explicitly opens Level_4 is unnecessary.

In this program, a predicate called buildHere is declared which evaluates to true for points corresponding to the lower-left corner of a 3x3x3 cube in which a 3D cross can be constructed. If we wanted a field of crosses without spaces between them, the buildHere function would have needed to perform mod 3 operations. However, we want crosses to be separated from each other by a distance of 2 cells so we perform mod 5 operations.

It should be noted that the function cross is not a brick function (i.e., it does not return a brick value as its result). For this reason, cross is unsuitable for use with traverseWithin. However, in Level_5 there is another traversal function called applyWithin for which our cross is a suitable argument. Specifically, applyWithin is a traversal that applies a point_function to every point it traverses. Bricklayer defines a point_function to be a specific function type. A function that takes a 3D point as input and returns the unit value as output is of the type point_function. Using this definition, we see that cross is a function whose type is point_function.

Below is the actual definition of the point_function type and the signature of the function applyWithin used in Bricklayer.

type definition point_function = point → unit
function signature applyWithin : point → point → point_function → unit

A strong argument can be made that for certain kinds of patterns, combining Level_4 and Level_5 construction techniques is better than trying to express the pattern exclusively using Level_4/Level_5 functions.

open Level_5;

fun crossField () =
    let
        fun cross (x,y,z) =
            (
                put (3,1,1) BLUE (x,y+1,z+1);
                put (1,3,1) BLUE (x+1,y,z+1);
                put (1,1,3) BLUE (x+1,y+1,z)
            );
        
        fun buildHere (x,y,z) = x mod 5 = 0 
                                andalso 
                                y = 0 
                                andalso 
                                z mod 5 = 0;
        
        fun buildCross(x,y,z) = 
           if buildHere (x,y,z) then cross(x,y,z) 
           else ();
    in
        applyWithin (0,0,0) (32,2,32) buildCross
    end;

build(33,3,33);

crossField();

show "crossField";

level-5-ca-grid_05