Code Along
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, you will learn the geometric algorithm for constructing a basic lace pattern which involves an initial seed shape and a stamping pattern. The seed shape we will use is a bit-brick (i.e., a 1×1 brick), and the stamping pattern we will use resembles the reverse of the letter L.
This code-along will also use a Bricklayer function, called generateRandomBrickFn, to generate random bricks within our lace artifact. This will enable us to add color to lace artifacts in interesting ways.
Step 1
In this step we will write a program that creates a lace using (1) a 1×1 brick as its seed shape, and (2) one iteration of the stamping pattern that is shown in the table below.
* | |
* | * |
In the code below, information about the seed shape is stored in the variables seedShapeSide and theDimensions. This allows us to change the size of the seed shape from a 1×1 brick to an nxn brick by simply chaning the value to which the variable seedShapeSide is bound. Also note, that the variable theDimensions will be bound (at run-time) to a value that is a tuple consisting of two integer values.
The seedShape function is a function that when called creates a square of size theDimensions whose bricks are of the kind b positioned at the coordinate p. Note that it is perfectly acceptable to use a single variable p as a formal parameter to denote a coordinate.
The function lace0 implements a lace pattern by placing seed shapes at the locations specified by the reverse-L stamping pattern in the table above. The body of lace0 is a let-block in which a val-declaration is used to locally declare the variable delta, which will be bound to the value of the seedShapeSide at run-time. In lace0, delta and seedShapeSide are equivalent, so there is no compelling reason for introducing delta. However, as we interate the stamping pattern to create larger and larger reverse-L lace artifacts, the value of delta and seedShapeSide will not be equivalent. More specifically, the value of delta will be a power of 2 times the value of the seedShapeSide.
open Level_3; val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; fun lace0 brick (x,z) = let val delta = seedShapeSide; in seedShape brick (x ,z ); seedShape brick (x+delta,z ); seedShape brick (x+delta,z+delta) end; build2D (256,256); lace0 BLUE (0,0); show2D "reverse-L";
Step 2
This example creates a reverse-L lace using 6 iterations of the stamping pattern. Notice that this example makes use of a function, called pow, to compute integer powers of 2. We have not covered how to write these kinds of functions so it is not necessary to understand the inner workings of such function declarations. At this time, the only thing you need to understand about the pow function is (1) what it does, and (2) how to call it. Let v and n denote two integers. The function call pow v n will compute vn.
In the code below, the bodies of the lace0 – lace5 functions differ from one another in only two places: (1) the second argument given to the function pow, and (2) the function that is called in the body of the let-block.
open Level_3; val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; fun pow v 0 = 1 | pow v n = v * pow v (n-1); fun lace0 brick (x,z) = let val delta = (pow 2 0) * seedShapeSide; in seedShape brick (x ,z ); seedShape brick (x+delta,z ); seedShape brick (x+delta,z+delta) end; fun lace1 brick (x,z) = let val delta = (pow 2 1) * seedShapeSide; in lace0 brick (x ,z ); lace0 brick (x+delta,z ); lace0 brick (x+delta,z+delta) end; fun lace2 brick (x,z) = let val delta = (pow 2 2) * seedShapeSide; in lace1 brick (x ,z ); lace1 brick (x+delta,z ); lace1 brick (x+delta,z+delta) end; fun lace3 brick (x,z) = let val delta = (pow 2 3) * seedShapeSide; in lace2 brick (x ,z ); lace2 brick (x+delta,z ); lace2 brick (x+delta,z+delta) end; fun lace4 brick (x,z) = let val delta = (pow 2 4) * seedShapeSide; in lace3 brick (x ,z ); lace3 brick (x+delta,z ); lace3 brick (x+delta,z+delta) end; fun lace5 brick (x,z) = let val delta = (pow 2 5) * seedShapeSide; in lace4 brick (x ,z ); lace4 brick (x+delta,z ); lace4 brick (x+delta,z+delta) end; build2D (256,256); lace5 BLUE (0,0); show2D "reverse-L";
Step 3
This step explores a way to introduce color into the lace artifact by associating a hard-coded brick value with a given lace function call. In this example, hard-coding will involve replacing instances of the variable brick with actual brick values (e.g., BLUE).
Note that all of the lace function declarations have brick as a formal parameter. It is through these parameters that brick colors “flow” from a lace call all the way down to the seedShape function calls which create the lace. From this perspective, the variable brick, which is a formal parameter of all the lace functions creates a kind of plumming that transports values introduced at a source location (e.g., the initial call to the function lace5) with a destination location (i.e., the seedShape function) where these values are actually used.
With this metaphor in mind, consider the following function call:
lace3 YELLOW (0,0);
The lace artifact created by this function call is the composition of three laces created by calls to the function lace2. In turn, the lace artifact created by a function call to lace2 is the composition of three laces created by calls to the function lace1, and so on.
When constructing a lace artifact using a lace3 function call, consider what would happen if each of the three calls to lace2 was made using a different hard-coded brick value. For example, suppose the first call to lace2 used RED, the second call to lace2 used GREEN, and the third call to lace2 used BLUE. In this case, the lace artifact created using the lace3 function would be the composition of a RED, a GREEN, and a BLUE lace each of which created by corresponding calls to the function lace2.
Note that in this case, the actual parameter YELLOW, which is used in the lace3 function call is completely disregarded. One way to understand what is happening here is that the flow of brick values coming into the function lace3 is disregarded and a new hard-coded flow of brick values is introduced. It is also worth mentioning that the choice of interrupting the flow in the lace3 function is completely arbitrary. The way the code is written, it is easy to introduce hard-coded values at any level of the artifact construction.
open Level_3; val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; fun pow v 0 = 1 | pow v n = v * pow v (n-1); fun lace0 brick (x,z) = let val delta = (pow 2 0) * seedShapeSide; in seedShape brick (x ,z ); seedShape brick (x+delta,z ); seedShape brick (x+delta,z+delta) end; fun lace1 brick (x,z) = let val delta = (pow 2 1) * seedShapeSide; in lace0 brick (x ,z ); lace0 brick (x+delta,z ); lace0 brick (x+delta,z+delta) end; fun lace2 brick (x,z) = let val delta = (pow 2 2) * seedShapeSide; in lace1 brick (x ,z ); lace1 brick (x+delta,z ); lace1 brick (x+delta,z+delta) end; fun lace3 brick (x,z) = let val delta = (pow 2 3) * seedShapeSide; in lace2 RED (x ,z ); lace2 GREEN (x+delta,z ); lace2 BLUE (x+delta,z+delta) end; fun lace4 brick (x,z) = let val delta = (pow 2 4) * seedShapeSide; in lace3 brick (x ,z ); lace3 brick (x+delta,z ); lace3 brick (x+delta,z+delta) end; fun lace5 brick (x,z) = let val delta = (pow 2 5) * seedShapeSide; in lace4 brick (x ,z ); lace4 brick (x+delta,z ); lace4 brick (x+delta,z+delta) end; build2D (256,256); lace5 BLUE (0,0); show2D "reverse-L";
Step 4
This version of the code replaces the hard-coded values that were introduced in the body of the lace3 function in the previous example with randomly generated values. Specifically, Bricklayer’s generateRandomBrickFn is used to create the nullary function randomBrickFn that, when called, generates a brick randomly selected from the brick list called blueScale, which is provided by Bricklayer. Note that Bricklayer provides a number of such predefined brick lists which can be used in place of the blueScale list used in this example. An incomplete set of lists is shown below.
- grayScale
- greenScale
- blueScale (used in this example)
- purpleScale
- redScale
- warmScale
- brownScale
- clearScale
- allOneBitBricks
Note that the execution of a lace3 function call will result in 3 distinct calls to the lace2 function. Similarly, the execution of a lace4 function call will result in 3×3 = 9 distinct calls to the lace2 function. The code is written in such a way that each time the lace2 function is called it is given a randomly generated brick.
There are many points in the program flow where randomly generated bricks can be introduced. Also, more than one random generator can be used to generate bricks. For example, one random generator can be used to select from the blueScale bricks and another can be used to select from the redScale bricks.
open Level_3; val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; fun pow v 0 = 1 | pow v n = v * pow v (n-1); val new = generateRandomBrickFn blueScale; fun lace0 brick (x,z) = let val delta = (pow 2 0) * seedShapeSide; in seedShape brick (x ,z ); seedShape brick (x+delta,z ); seedShape brick (x+delta,z+delta) end; fun lace1 brick (x,z) = let val delta = (pow 2 1) * seedShapeSide; in lace0 brick (x ,z ); lace0 brick (x+delta,z ); lace0 brick (x+delta,z+delta) end; fun lace2 brick (x,z) = let val delta = (pow 2 2) * seedShapeSide; in lace1 brick (x ,z ); lace1 brick (x+delta,z ); lace1 brick (x+delta,z+delta) end; fun lace3 brick (x,z) = let val delta = (pow 2 3) * seedShapeSide; val brick1 = new (); val brick2 = new (); val brick3 = new (); in lace2 brick1 (x ,z ); lace2 brick2 (x+delta,z ); lace2 brick3 (x+delta,z+delta) end; fun lace4 brick (x,z) = let val delta = (pow 2 4) * seedShapeSide; in lace3 brick (x ,z ); lace3 brick (x+delta,z ); lace3 brick (x+delta,z+delta) end; fun lace5 brick (x,z) = let val delta = (pow 2 5) * seedShapeSide; in lace4 brick (x ,z ); lace4 brick (x+delta,z ); lace4 brick (x+delta,z+delta) end; build2D (256,256); lace5 BLUE (0,0); show2D "reverse-L";
Step 5
This version of the code slightly improves the previous code by moving the location where the new (i.e., randomly selected) brick is introduced into the code flow. In the previous implementation, 3 randomly selected bricks were introduced in the body of the lace3 function. Each of these bricks was then passed to the function lace2. Computationally, this is equivalent to introducing a single randomly selected brick in the body of the lace2 function. In other words, rather than introducing a randomly selected brick at the “point of call” to the function lace2, we introduce a randomly selected brick at the “point of entry” to the function body of lace2.
open Level_3; val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; fun pow v 0 = 1 | pow v n = v * pow v (n-1); val new = generateRandomBrickFn blueScale; fun lace0 brick (x,z) = let val delta = (pow 2 0) * seedShapeSide; in seedShape brick (x ,z ); seedShape brick (x+delta,z ); seedShape brick (x+delta,z+delta) end; fun lace1 brick (x,z) = let val delta = (pow 2 1) * seedShapeSide; in lace0 brick (x ,z ); lace0 brick (x+delta,z ); lace0 brick (x+delta,z+delta) end; fun lace2 brick (x,z) = let val delta = (pow 2 2) * seedShapeSide; val brick1 = new (); in lace1 brick1 (x ,z ); lace1 brick1 (x+delta,z ); lace1 brick1 (x+delta,z+delta) end; fun lace3 brick (x,z) = let val delta = (pow 2 3) * seedShapeSide; in lace2 brick (x ,z ); lace2 brick (x+delta,z ); lace2 brick (x+delta,z+delta) end; fun lace4 brick (x,z) = let val delta = (pow 2 4) * seedShapeSide; in lace3 brick (x ,z ); lace3 brick (x+delta,z ); lace3 brick (x+delta,z+delta) end; fun lace5 brick (x,z) = let val delta = (pow 2 5) * seedShapeSide; in lace4 brick (x ,z ); lace4 brick (x+delta,z ); lace4 brick (x+delta,z+delta) end; build2D (256,256); lace5 BLUE (0,0); show2D "reverse-L";
Step 6
This example involves restructuring the code so that all lace sizes are created using an applyLace function. The applyLace function takes as input a stamp function, a brick, a shift value delta, and a coordinate. Its purpose is to perform the reverse-L stamping pattern and nothing more. The lace0 – lace5 functions are defined in terms of applyLace. For example, the lace0 function directs the applyLace function to perform its stamping pattern using seedShape as its stamp. Similarly, the lace1 function directs the applyLace function to perform its stamping pattern using lace0 as its stamp, and so on.
open Level_3; fun pow v 0 = 1 | pow v n = v * pow v (n-1); fun applyLace stamp brick delta (x,z) = ( stamp brick (x ,z ); stamp brick (x+delta,z ); stamp brick (x+delta,z+delta) ); fun lace p = let val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; val new = generateRandomBrickFn blueScale; val delta0 = pow 2 0 * seedShapeSide; val delta1 = pow 2 1 * seedShapeSide; val delta2 = pow 2 2 * seedShapeSide; val delta3 = pow 2 3 * seedShapeSide; val delta4 = pow 2 4 * seedShapeSide; val delta5 = pow 2 5 * seedShapeSide; fun lace0 brick p = applyLace seedShape brick delta0 p; fun lace1 brick p = applyLace lace0 brick delta1 p; fun lace2 brick p = applyLace lace1 (new ()) delta2 p; fun lace3 brick p = applyLace lace2 brick delta3 p; fun lace4 brick p = applyLace lace3 brick delta4 p; fun lace5 brick p = applyLace lace4 brick delta5 p; in lace5 BLUE p end; build2D (256,256); lace5 BLUE (0,0); show2D "reverse-L";
Step 6 – Some final polish.
This last example involves some minor code polishing. Specifically, a function is introduced, called computeDelta, whose purpose is to compute the appropriate delta for a given power of 2.
open Level_3; fun pow v 0 = 1 | pow v n = v * pow v (n-1); fun applyLace stamp brick delta (x,z) = ( stamp brick (x ,z ); stamp brick (x+delta,z ); stamp brick (x+delta,z+delta) ); fun lace p = let val seedShapeSide = 1; val theDimensions = (seedShapeSide,seedShapeSide); fun seedShape b p = put2D theDimensions b p; val new = generateRandomBrickFn blueScale; fun computeDelta n = pow 2 n * seedShapeSide val delta0 = computeDelta 0; val delta1 = computeDelta 1; val delta2 = computeDelta 2; val delta3 = computeDelta 3; val delta4 = computeDelta 4; val delta5 = computeDelta 5; fun lace0 brick p = applyLace seedShape brick delta0 p; fun lace1 brick p = applyLace lace0 brick delta1 p; fun lace2 brick p = applyLace lace1 (new ()) delta2 p; fun lace3 brick p = applyLace lace2 brick delta3 p; fun lace4 brick p = applyLace lace3 brick delta4 p; fun lace5 brick p = applyLace lace4 brick delta5 p; in lace5 BLUE p end; build2D (256,256); lace (0,0); show2D "reverse-L";