Basic Properties

Code Along

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.

This code along explores how Boolean expressions can be used to define basic properties and how these properties can be used to create Bricklayer artifacts.


Property 1

In this example a brick function, called brickFn,  is created that returns a BLUE brick when it is applied to the origin and returns an EMPTY brick when it is applied to any other coordinate. This behavior is achieved through a conditional expression that performs an equality comparison between the expression consisting of the variable point, the formal parameter to brickFn,  and the tuple value (0,0,0). The function traverseWithin is used to traverse the entire virtual space applying brickFn to every coordinate encountered and updating each cell visited with the result of the brickFn function application. The result, shown in Figure 1, is an artifact consisting of a single BLUE brick placed at the origin.

open Level_5; 

fun brickFn point = if point = (0,0,0) then BLUE else EMPTY;

build (32,32,32);

traverseWithin (0,0,0) (31,31,31) brickFn;

show "unit brick";
property_01
Figure 1: A unit brick. Property is point = (0,0,0).

Property 2

In this example a brick function, called brickFn,  is created that returns a BLUE brick when it is applied to any coordinate whose y and z values are equal to 0 and returns an EMPTY brick when it is applied to any other coordinate. This behavior is achieved through a conditional expression that performs an equality comparison between the tuple expression (y,z) and the tuple value (0,0). The function traverseWithin is used to traverse the entire virtual space applying brickFn to every coordinate encountered and updating each cell visited with the result of the brickFn function application. The result, shown in Figure 2, is a row of BLUE bricks placed at the following coordinates.

(0,0,0), (1,0,0), … (31,0,0)

open Level_5; 

fun brickFn (x,y,z) = if (y,z) = (0,0) then BLUE else EMPTY;

build (32,32,32);

traverseWithin (0,0,0) (31,31,31) brickFn;

show "brick row";
property_02
Figure 2: A brick row. Property is (y,z) = (0,0).

Property 3

In this example a brick function, called brickFn,  is created that returns a BLUE brick when it is applied to any coordinate containing a 0 and returns an EMPTY brick when it is applied to any other coordinate. This behavior is achieved through a conditional expression that performs the following equality comparison.

 x * y * z = 0

Note that the above comparison evaluates to true if and only if one or more of the variables in the product x * y * z evaluate to 0.

The function traverseWithin is used to traverse the entire virtual space applying brickFn to every coordinate encountered and updating each cell visited with the result of the brickFn function application. The result, shown in Figure 3, are three BLUE brick walls. The first wall is the xy-plane when z = 0, the second wall is the xz-plane when y = 0, and the third wall is the yz-plane when x = 0. Overlaps occur at the following locations.

  1. along the x-axis where both y = 0 and z = 0,
  2. along the y-axis where both x = 0 and z = 0,
  3. along the z-axis where both x = 0 and y = 0, and
  4. at the origin where x = 0, y = 0, and z = 0.
open Level_5; 

fun brickFn (x,y,z) = if x * y * z = 0 then BLUE else EMPTY;

build (32,32,32);

traverseWithin (0,0,0) (31,31,31) brickFn;

show "brick walls";
property_03
Figure 3: Brick walls (view from back). Property is x * y * z = 0.

Property 4

In this example a brick function, called brickFn,  is created that returns a BLUE brick when it is applied to any coordinate in the xz-plane where y = 0 whose x-value is a multiple of 5, and returns an EMPTY brick when it is applied to any other coordinate. This behavior is achieved through a conditional expression whose branching behavior is controlled by the following condition.

 y = 0 andalso x mod 5 = 0

The function traverseWithin is used to traverse the entire virtual space applying brickFn to every coordinate encountered and updating each cell visited with the result of the brickFn function application. The result, shown in Figure 4, is an artifact consisting of 7 BLUE lines whose x coordinate values are: 0, 5, 10, 15, 20, 25, and 30.

open Level_5; 

fun brickFn (x,y,z) = if y = 0 andalso x mod 5 = 0 then BLUE  
                      else EMPTY;

build (32,32,32);

traverseWithin (0,0,0) (31,31,31) brickFn;

show "vertical bars";
property_04
Figure 4: Vertical bars. Property is y = 0 andalso x mod 5 = 0.

Property 5

In this example a brick function, called brickFn,  is created that returns a BLUE brick when it is applied to any coordinate in the xz-plane where y = 0 in which the sum of the x and z values is a multiple of 5; otherwise an EMPTY brick is returned. This behavior is achieved through a conditional expression whose branching behavior is controlled by the following condition.

 y = 0 andalso (x + z) mod 5 = 0

The function traverseWithin is used to traverse the entire virtual space applying brickFn to every coordinate encountered and updating each cell visited with the result of the brickFn function application. The result, shown in Figure 5, is an artifact consisting of 13 diagonal BLUE lines.

open Level_5; 

fun brickFn (x,y,z) = if y = 0 andalso (x + z) mod 5 = 0 then BLUE 
                      else EMPTY;

build (32,32,32);

traverseWithin (0,0,0) (31,31,31) brickFn;

show "diagonal bars";
property_05
Figure 5: Diagonal bars. Property is y = 0 andalso (x + z) mod 5 = 0.

Question: Can you modify this program so that the diagonal lines run the other way?


Property 6

This example shows how a brick function and a traversal can be use to define a function, called myPut, whose behavior is equivalent to Bricklayer’s put function. Below is an example of equivalent function calls.

myPut (2,3,4) BLUE (0,0,0)   ≡   put (2,3,4) BLUE (0,0,0)

The body of the function myPut is a let-block whose declarations contain a brick function as well as the start and end points of a traversal. The brick function is called brickFn and has a body that is an expression consisting only of the variable brick. Note that brick is a formal parameter to the myPut function. The body of the let-block consists of a traversal. Specifically, the function traverseWithin is used to traverse a virtual space covering only the cells which correspond to the shape that would be created by a corresponding put function call. The lower left corner of this space is a the coordinate (x,y,z), the startPoint, and the upper right corner of this space is equal to the start point plus the brick size. As usual the expression that calculates the coordinate of the upper right corner involves subtracting 1 to avoid an off-by-one error.

The top-level of the program contains a myPut function call and a put function call. This serves to demonstrate the equivalence between the two function calls.

open Level_5; 

fun myPut (xSize,ySize,zSize) brick (x,y,z) = 
    let
        fun brickFn (x,y,z) = brick;
        
        val startPoint = (x,y,z);
        val endPoint   = (x + xSize - 1, y + ySize - 1, z + zSize - 1);
    in
        traverseWithin startPoint endPoint brickFn    
    end;

build (32,32,32);

myPut (2,1,4) BLUE (0,0,0);
put   (2,1,4) RED  (2,0,0);

show "implementing put";
property_06
Figure 6: An implementation of put.

Property 7

This example shows how a brick function and a traversal can be use to define a function, called myPutMultibrick, whose behavior is equivalent to Bricklayer’s putMultibrick function. Below is an example of equivalent function calls.

myPutMultibrick (2,3,4) [BLUE] (0,0,0)   ≡   putMultibrick (2,3,4) [BLUE] (0,0,0)

The body of the function myPutMultibrick is a let-block whose declarations contain the following.

  1. a variable called newBrick that is bound to a random brick selection function,
  2. a brick function whose body consists of the call newBrick ().
  3. the start and end points of a traversal whose shape corresponds to the shape of the brick we want to create.

The trick in this example is to that every call to brickFn will return a randomly selected brick drawn from brickList.

The top-level of the program contains a myPutMultibrick function call and a putMultibrick function call. Visual inspection lets us confirm that the brick selections between the two artifacts are identical. This suggests, but does not imply, that (under the hood) both function calls construct their artifact by visiting (i.e., processing) cells in the same order. Relying on this observed behavior is not recommended. The reason being that neither the putMultibrick function nor the traverseWithin function provide any guarantees regarding the order in which cells are visited. Furthermore, future improvements to the Bricklayer library may involve modifications to the implementations of the putMultibrick and/or traverseWithin functions.  In this case, it is entirely possible that the sequence in which cells are visited by the putMultibrick is different from the sequence of cells visited by the traverseWithin function, in which case, the artifacts constructed will no longer be identical.

open Level_5; 

fun myPutMultibrick (xSize,ySize,zSize) brickList (x,y,z) = 
    let
        val newBrick        = generateRandomBrickFn brickList;
        fun brickFn (x,y,z) = newBrick ();
        
        val startPoint = (x,y,z);
        val endPoint   = (x + xSize - 1, y + ySize - 1, z + zSize - 1);
    in
        traverseWithin startPoint endPoint brickFn    
    end;

val brickList = [BLUE, INDIGO, VIOLET, ORANGE, DARKRED];

build (32,32,32);

myPutMultibrick (5,2,10) brickList (0,0,0);
putMultibrick   (5,2,10) brickList (6,0,0);

show "implementing putMultibrick";
property_07.png
Figure 7: An implementation of putMultibrick.

Property 8

In this example, a function, called slicedSphere, is defined that slices a solid sphere using a diagonal plane. Note that this artifact is not easily constructed using Level 4 functions.

The body of the slicedSphere function consists of a let-block whose body first creates a sphere, and traverses the virtual space that contains the sphere applying the function brickFn to every cell visited.

The body of brickFn consists of a conditional expression that keeps all the cells of the sphere whose height  (its y-value) is less-than or equal-to its width (its x-value). The contents of all other cells are discarded (i.e., an EMPTY brick is put in cells having discarded locations).  The result is the sliced sphere shown in Figure 8.

open Level_5; 

val d   = 64;
val mid = d div 2;

fun slicedSphere radius brickList (xCenter,yCenter,zCenter) = 
  let
    fun brickFn (x,y,z) = if y <= x then IDENTITY else EMPTY;
        
    val startPoint = (xCenter - radius, yCenter - radius, zCenter - radius)
    val endPoint   = (xCenter + radius, yCenter + radius, zCenter + radius)
  in
    sphere radius brickList (xCenter,yCenter,zCenter);
        
    traverseWithin startPoint endPoint brickFn    
  end;
    
val brickList = [BLUE];
   
build (d,d,d);

slicedSphere 20 brickList (mid,mid,mid);

show "slicing a sphere";
property_08
Figure 8: A sliced sphere.

Property 9

In this example, a function, called slicedSphere, is defined that creates a horizontal and vertical ring. Note that such an artifact can be constructed using the ringX and ringY functions. What is different about the code shown here is that these rings are constructed by first creating a hollow sphere and then slicing away portions of it. What remains is the horizontal and vertical ring artifact shown in Figure 8.

The body of the slicedSphere function consits of a let-block whose body first creates a hollow sphere, and traverses the virtual space that contains the hollow sphere applying the function brickFn to every cell visited.

The main ideas in our construction method occur in the body of brickFn. The body of brickFn is a let-block that declares three variables: cellContents, onHorizontalSlice, and onVerticalSlice. The variable cellContents is bound to an expression consisting of the function call access (x,y,z). Recall that the access function is a Level_5 function that lets you retrieve (and inspect) the contents of a cell in the virtual space. In this example, if cellContents is not equal to EMPTY (i.e., it contains a brick), it implies that the cell is part of the hollow sphere. Knowing this the property satisfied by a horizontal slice can be stated as follows.

A cell belongs to the horizontal slice of a hollow sphere if the cell is not empty and the cell is located on the xz-plane corresponding to the horizontal center of the hollow sphere.

The variable onHorizontalSlice is bound to a Boolean expression that captures the above stated property. A similar property can be used to define a vertical slice.

The body of brickFn’s let-block is a conditional expression that checks if the cell, currently being visited, falls on either the horizontal slice or the vertical slice. If it does, then the IDENTITY brick is returned, other wise the EMPTY brick is returned. Recall that the IDENTITY brick  is a conceptual brick. Putting an IDENTITY brick at a given location has no effect – the contents of the cell is left unaltered. So what the conditional expression is really saying is the following.

If the current cell belongs to one of our slices, then left its contents alone; otherwise erase the contents of the cell.

open Level_5; 

val d   = 64;
val mid = d div 2;

fun slicedSphere radius brickList (xCenter,yCenter,zCenter) = 
  let
    fun brickFn (x,y,z) = 
      let
        val cellContents      = access (x,y,z);
        val onHorizontalSlice = cellContents <> EMPTY andalso y = yCenter;
        val onVerticalSlice   = cellContents <> EMPTY andalso x = xCenter;
      in
        if onHorizontalSlice orelse onVerticalSlice then IDENTITY else EMPTY
      end
        
    val startPoint = (xCenter - radius, yCenter - radius, zCenter - radius)
    val endPoint   = (xCenter + radius, yCenter + radius, zCenter + radius)
  in
    hollowSphere radius 10 brickList (xCenter,yCenter,zCenter);
        
    traverseWithin startPoint endPoint brickFn    
  end;
    
val brickList = [BLUE, INDIGO, VIOLET, ORANGE, DARKRED];
   
build (d,d,d);

slicedSphere 20 brickList (mid,mid,mid);

show "slicing a sphere";
property_09
Figure 9: Slices from a sphere.