Growing the language¶
So far, we have a few “primitives” for creating pictures and some for transforming pictures. Let’s list them out to recap –
; Shapes
(Disc <radius>)
(Circle <radius> <thickness>)
(Qquare <width>)
(Rectangle <width> <height>)
; Transformations
(Translate <dx> <dy> <picture>)
(Rotate <deg> <picture>)
(Scale <xscale> <yscale> <picture>)
(InvertColour <picture>)
(Opacity <alpha> <picture>)
(Colourize <colour> <picture>)
; Combinations
(Overlay <pictureA> <pictureB>)
(Intersect <pictureA> <pictureB>)
(LayoutHoriz <hstep> <pictureA> <pictureB>)
(LayoutVert <vstep> <pictureA> <pictureB>)
Exercise
Implement some of these operations as ordinary lambda functions to convince yourself that the representation we’ve chosen still serves to model this whole set.
Some of you may have noticed that if we have Rectangle,
we don’t really need Square since a square is just a rectangle
with equal sides. Even in the case of Disc and Circle,
we can see that a Disc or radius \(R\) can be thought of
as a circle of radius \(R/2\) of thickness \(R\).
Those who have your linear algebra course still on top of your minds
will perhaps be able to see that Translate, Rotate
and Scale are all special cases of “affine transforms” which
we can model using a single Affine operator which applies a
matrix operation followed by translation.
(struct Affine (mxx mxy myx myy dx dy pic))
We also see how something like LayoutHoriz can be expressed
in terms of Overlay and Translate like this -
(LayoutHoriz xstep picA picB) = (Overlay picA (Translate xstep 0.0 picB))
So LayoutHoriz is not really “fundamental” or “core” in that sense.
Having a small “core” for our language is valuable because it reduces the
possibilities over which we need to reason in order to be convinced that
our language is good. [1]
It would appear that a language that has a small “core” is at loggerheads
with the goal of growing the language to be able to do more interesting
things with it. To meet this, we’ll still keep the core small, but add a
transformation from a larger set of “sugary” constructs to the core language,
so we can have our cake and eat it too. We’ll refer to this transformation
as “desugaring”. You’ve already seen examples of desugaring in Scheme,
when we talked about how the let construct can be expressed using
lambda and application.
To make our core language terms stand out compared to the “surface” or
“sugar” layer, we’ll add a C suffix to core terms and S suffix to
sugar/syntax terms.
(define (desugar picexprS)
(match picexprS
[(LayoutHorizS xstep picA picB)
(OverlayC (desugar picA) (desugar (TranslateS xstep 0.0 picB)))]
[(TranslateS xstep ystep picA)
(AffineC 1.0 0.0 0.0 1.0 xstep 0.0 (desugar picA))]
; ... other such conversion rules.
; Our contract is that desugar must not produce any
; of the sugar terms. The result expression must only involve
; the core terms.
[_ picexprS]))
It is easy to forget to call desugar recursively. Remember that everywhere
we have a “surface expression”, we’ll need to convert it into a “core expression”
and that’s the purpose of desugar.
Now, when we want to add a new operation that we know can be expressed in terms
of our “core expressions”, we can add it to our desugar function without
touching our core interpreter. So this “surface-vs-core” split helps us grow
the language in one way.
Typed racket (advanced / optional)¶
We stepped out of #lang plai-typed into plain racket because it was
too restrictive for the picture language we’re setting out to build. In perhaps
a later version of this course, we’ll augment the language to help meet this
constraint. For now though, you can use either plain Racket or #lang typed/racket
if you’re brave enough to get similar type checking as #lang plai-typed.
Below is some of our code in typed/racket.
The advantage of using a type system is that the “compilation” step will detect places where you’re being inconsistent and throw up errors so that you don’t face these errors when running your program. Using a type system also helps design your program in the initial stages.
#lang typed/racket
(require typed-racket-datatype)
(require racket/match)
(struct Colour [a : Float] [r : Float] [g : Float] [b : Float])
(define-datatype PicExprC
(DiscC [radius : Float])
(CircleC [radius : Float] [thickness : Float])
(RectangleC [width : Float] [height : Float])
(AffineC [mxx : Float]
[mxy : Float]
[myx : Float]
[myy : Float]
[dx : Float]
[dy : Float]
[pic : PicExprC])
(ColourizeC [colour : Colour] [pic : PicExprC]))
(define-datatype PicExprS
(DiscS [radius : Float])
(CircleS [radius : Float] [thickness : Float])
(RectangleS [width : Float] [height : Float])
(SquareS [width : Float])
(TranslateS [dx : Float] [dy : Float] [pic : PicExprS])
(RotateS [angle : Float] [pic : PicExprS])
(ScaleS [xscale : Float] [yscale : Float] [pic : PicExprS])
(ColourizeS [colour : Colour] [pic : PicExprS]))
(: desugar (-> PicExprS PicExprC))
(define (desugar picexprS)
(match picexprS
[(DiscS radius)
(DiscC radius)]
[(CircleS radius thickness)
(CircleC radius thickness)]
[(RectangleS width height)
(RectangleC width height)]
[(SquareS width)
(RectangleC width width)]
[(TranslateS dx dy picS)
(AffineC 1.0 0.0 0.0 1.0 dx dy (desugar picS))]
[(RotateS angle picS)
(let ([c (cos angle)] [s (sin angle)])
(AffineC c (- s) s c 0.0 0.0 (desugar picS)))]
[(ScaleS xscale yscale picS)
(AffineC xscale 0.0 0.0 yscale 0.0 0.0 (desugar picS))]
[(ColourizeS colour picS)
(ColourizeC colour (desugar picS))]
(_ (raise-argument-error 'desugar "PicExprS" picexprS))))
(define-type Picture (-> Float Float Colour))
(: affine (-> Float Float Float Float Float Float Picture Picture))
(define (affine mxx mxy myx myy dx dy pic)
; Note that we need to apply the inverse of
; the specified Affine transform on the given
; (x y) coordinates to get the coordinates to
; be passed to the given pic. The given transform
; is to apply the matrix followed by the translation,
; so the inverse would be the inverse translation
; followed by the inverse of the matrix.
(let ([det (- (* mxx myy) (* mxy myx))])
(let ([mixx (/ myy det)]
[mixy (- (/ mxy det))]
[miyy (/ mxx det)]
[miyx (- (/ myx det))])
(λ ([x : Float] [y : Float])
(let ([x2 (- x dx)]
[y2 (- y dy)])
(let ([x3 (+ (* mixx x2) (* mixy y2))]
[y3 (+ (* miyx x2) (* miyy y2))])
(pic x3 y3)))))))
; ... and so on.
Exercise
Write the interpreter using typed/racket.