On this page:
unclear/  c
clear/  c
fulfilled/  c
advance/  c
unlike-asset/  c
2.1 Imperative API Reference
unlike-compiler%
clarify
delegate
compile!
lookup
add!
2.2 Responding to Change
2.2.1 Overview
2.2.2 Ripple Procedures
ripple/  c
2.2.3 Step-by-Step Example
2.2.4 Design Implications
2.3 Command Line Interface
2.4 Examples
2.4.1 Basic use of clarify and delegate
7.7

2 Imperative Model

The imperative model uses Stephen Chang’s graph library in a simple object model to track dependencies between assets and build them in order. This model is good for those who are used to classical object-oriented programming and/or anyone who wants to work on the underlying graph of their project as a whole.

To use this model, you subclass unlike-compiler% and override two methods called clarify and delegate.

clarify translates an string like styles.css into an unambiguous identifier like an absolute path or full URL. What counts as a clear or unclear name is up to you.

delegate takes a clear name and maps it to the first procedure that must run to start building the asset of that name. That procedure can identify dependencies of your asset and add them to the graph. So if you process an HTML file, then you can find dependencies in that file and queue them up for further processing.

If an asset changes, you can also control how that change propagates to other assets.

Let’s rehash the fundamentals as contracts set by the context of this model.

When an asset value (as per unlike-asset/c) is fulfilled, it’s not a procedure and its value is final. Otherwise, it is an advance/c procedure that returns a new asset that is fulfilled or unfulfilled.

An instance of unlike-compiler% must clarify these strings and delegate work to procedures that can fulfill the assets under the clear/c names. An unlike-compiler% instance can also mark changes on an asset’s value and control how that change ripples to dependencies.

2.1 Imperative API Reference

class

unlike-compiler% : class?

  superclass: object%

An abstract class that coordinates asset fulfillment.
Depending on your requirements and the complexity of your project, you may need to use custodians, threads, engines, places, or other constructs to coordinate different instances of this class.

method

(send an-unlike-compiler clarify unclear)  clear/c

  unclear : unclear/c

Override this method to deterministically map an unclear string to a clear name for an asset. By default, clarify is the identity function.

Once assets have clear names we need to decide what to do with them by delegating work out to appropriate procedures.

method

(send an-unlike-compiler delegate clear)  unlike-asset/c

  clear : clear/c
Override this abstract method to deterministically return the first value to represent an asset of name clear.

If you want your terminal value to be a procedure, wrap it in a box, list, etc.

If delegate returns an advance/c procedure, that procedure must accept the same clear name and the instance of the compiler as arguments, and either return the next advance/c procedure to pass on responsibility, or a terminal value that isn’t a procedure at all.

Any procedure in the implied chain of fulfillment can (and should) add! dependencies to the compiler as they are discovered. If this occurs, the subsequent procedure will not be called until those dependencies are fulfilled.

Once clarified names can be used to delegate work to procedures, you can compile!

method

(send an-unlike-compiler compile! [#:changed changed 
  #:removed removed 
  #:strict? strict?]) 
  (hash/c clear/c fulfilled/c)
  changed : (listof clear/c) = null
  removed : (listof clear/c) = null
  strict? : any/c = #t
Fulfills all assets on the current thread and returns a hash mapping clear names to the final value associated with each asset. Will raise exn:fail if a call to compile! is already running for the instance.

Side-effects:

  • The encapsulated model will record all asset activity.

  • Events are sent to unlike-asset-logger.

If changed or removed are not empty, then the compiler will first modify the underlying model to reflect changed or removed assets according to Responding to Change.

method

(send an-unlike-compiler lookup clear)  unlike-asset/c

  clear : clear/c
Return the current value associated with a clear name in the compiler. Will raise exn:fail if no asset is found.

method

(send an-unlike-compiler add! clear    
  [dependent-clear    
  ripple])  void?
  clear : clear/c
  dependent-clear : (or/c clear/c boolean?) = #f
  ripple : (or/c ripple/c boolean?) = #f
Adds a clear asset name to the compiler. If dependent-clear is a clear name, then the compiler will understand that clear is a dependency of dependent-clear. Will raise exn:fail if a circular dependency forms.

ripple controls how a change in clear’s asset propagates to dependent-clear’s asset. By default, the dependent asset will be rebuilt. Otherwise the change will produce an asset value from a provided ripple/c procedure.

For information on the change model, see Responding to Change.

2.2 Responding to Change

With some work, an unlike-compiler% instance can support ongoing builds that perform only the work thats relevant to reported changes.

2.2.1 Overview

Once an unlike-compiler% instance finishes a call to compile!, it will remember the history of each asset value as it advanced to a fulfilled state.

If you call compile! again, it will simply return the same output it already prepared unless you report that there were changes.

(send compiler compile!
  #:strict? #t
  #:changed (clarify/multi compiler "/etc/config")
  #:removed (clarify/multi compiler
                           "leaked-xmas-photo.png"
                           "only-production-database-backup.sql"))

Unlike add!, which can be called at any time, #:changed and #:removed assets can only be declared after a prior successful call to compile!. Each following call to compile! will adapt an underlying graph to reflect any assets that were since changed or removed. Depending on the activity, dependent assets will regress to a value they once had in an attempt to incorporate changes while minimizing rework.

First, #:removed assets are deleted from the graph, as well as any edges connecting that that asset to others. Dependent assets are unconditionally rebuilt, but changes ripple from there according to Ripple Procedures. Unless strict? is #f, this process will raise exn:fail if any of the names marked as removed are already absent from the system.

#:changed assets then regress to a value returned from delegate. This is functionally equivalent to marking an asset to rebuild from scratch. Changes will ripple to dependents according to Ripple Procedures. Unless strict? is #f, this process will raise exn:fail if any #:changed assets that were not marked as removed are absent from the system.

2.2.2 Ripple Procedures

Dependent asset values regress according to a related ripple/c procedure provided to add!.

Returns a regressed asset value due to a change in a dependency.

The first argument is the clear name of the asset that has been changed.

The second argument is the clear name of the dependent asset.

The third argument is the history of the the dependent such that the element at index N is an unlike-asset/c returned by the advance/c element at index N+1.

The ripple procedure must return an element from this list. Otherwise the compiler will ignore the value, issue a <warning, and rebuild the dependent asset from scratch.

Internally, the compiler will only propogate a change further if the regressed value is not eq? to its previous value. The first element of the asset value’s history is always eq? to its current value, so having a ripple/c procedure return the first element of the history will both maintain the current value of the asset and prevent change from propogating further.

2.2.3 Step-by-Step Example
(define (replace-link-node) '...)
(define (immune changed/clear dependent/clear dependent/history) (car dependent-history))
(define (rebuild changed/clear dependent/clear dependent/history) (last dependent-history))
(define (partial changed/clear dependent/clear dependent/history) replace-link-node)
 
(send compiler add! "index.html")
(send compiler add! "styles.css")
(send compiler add! "about.html" "index.html" rebuild)
(send compiler add! "contact.html" "about.html" immune)
 
(send compiler add! "styles.css" "about.html" partial)
(send compiler add! "styles.css" "contact.html" partial)
 
(send compiler compile!)

In this example we compile a website of interdependent pages and walk through the behavior of each subsequent build.

(send compiler compile! #:changed '("about.html"))

Compile assuming that "about.html" has changed.

Rebuilds both "about.html" and "index.html".

(send compiler compile! #:changed '("contact.html"))

The website is rebuilt assuming only "contact.html" has changed. The relationship between "contact.html" and "about.html" is such that the latter will not change when the former changes. Not only that, "index.html" will also not change because propagation stopped at "about.html". In this scenario, only one webpage is fully rebuilt.

(send compiler compile! #:changed '("styles.css"))

The website is rebuilt assuming only "styles.css" has changed. In this scenario, "about.html" and "contact.html" are partially rebuilt assuming that replace-link-node is in their respective histories. But "index.html" is still fully rebuilt becase "about.html" propagates change in that way.

(send compiler compile! #:removed '("about.html"))

The website is rebuilt assuming only "about.html" was removed. "about.html" sat between "index.html" and "contact.html", which means that "contact.html" is no longer a dependency for any HTML asset. The compiler will still attempt to advance all assets, but does not guarentee that the output will function as expected. This state will likely result in a broken or missing link on a web page.

"index.html" is marked to build from scratch, but not because of its declared relationship with "about.html" this time around. Asset removal causes a full rebuild of dependents. If "index.html" had dependents, the change implied by this rebuild would ripple normally.

2.2.4 Design Implications

2.3 Command Line Interface

Use the provided raco command to build unlike assets from the terminal. Use raco unlike-assets:build -h flag to see options.

  $ raco unlike-assets:build my-compiler.rkt news/index.php blog/index.md shop/index.html

The first argument is the Racket module that provides an instance of an unlike-compiler% subclass as (provide compiler). With the above command, the instance will be loaded using (dynamic-require "my-compiler.rkt" 'compiler).

Each other argument will be clarified and registered to the compiler using add!.

CAUTION: The assets you name for compilation are considered unclear and will be arguments to clarify. If your implementation of clarify assumes that all relative paths are relative to the same directory, then a relative path prepared via tab completion might surprise you.

  $ raco unlike-assets:build my-compiler.rkt ./project/assets/index.md # whoops

  Cannot clarify ./assets/index.md

    Path not readable: /home/sage/project/assets/project/assets/index.md

    ...

This source of confusion is part of the trade-off in providing consistent behavior when representing dependencies in the abstract.

It helps to remember that as the user, you are the first unlike asset and your dependencies are therefore subject to clarification.

2.4 Examples

2.4.1 Basic use of clarify and delegate

This example shows an implementation of delegate and clarify that together build a message in terms of one set of dependencies.

(require unlike-assets)
 
(define dependencies
  '(complexity only maddens
    people ingesting lengthy
    elusive documentation))
 
(define/contract (build-message clear compiler) advance/c
  (string-upcase
    (apply string (map (λ (unclear) (send compiler lookup unclear))
                   dependencies))))
 
(define/contract (resolve-dependency clear compiler) advance/c
  (string-ref clear 0))
 
(define/contract (main clear compiler) advance/c
  (define clear-names (map (λ (unclear) (send compiler clarify unclear))
                           dependencies))
 
  (for ([dependency clear-names])
    (send compiler add! dependency clear))
 
  build-message)
 
(define compiler (new (class* unlike-compiler% () (super-new)
                        (define/override (clarify unclear)
                          (symbol->string unclear))
 
                        (define/override (delegate clear)
                          (if (string=? clear "start")
                              main
                              resolve-dependency)))))
 
(send compiler add! "start")
(hash-ref (send compiler compile!) "start")
  1. The compiler is informed of a new asset by an unclear name: 'start.

  2. clarify maps the unclear name to "start".

  3. delegate associates "start" to main.

  4. main runs, adding dependencies as a side-effect. build-message is returned as the designated next step to fulfill "start". It will not be called yet.

  5. All dependencies are circulated through clarify and delegate as well, and each end up fulfilled by resolve-dependency as the first letter of their clear names.

  6. build-message runs, joining the first letter of each dependency in the declared order. The output is "COMPILED"