Suppose Emerald de Gassy, the owner of the Oxford-based catering business Deli-Gate, hires us to design a domain-specific language (DSL) for describing sandwich platters. The DSL scripts will direct Deli-Gate's robotic kitchen appliance SueChef (Sandwich and Utility Electronic Chef) to assemble platters of sandwiches.
In discussing the problem with Emerald and the Deli-Gate staff, we discover the following:
Let's define this as an internal DSL--in particular, by using a relatively deep embedding.
What is a sandwich? ... Basically, it is a stack of ingredients.
Should we require the sandwich to have a bread on the bottom? ... Probably. ... On the top? Maybe not, to allow "open-faced" sandwiches. ... What can the SueChef build? ... We don't know at this point, but let's assume it can stack up any ingredients without restriction.
For simplicity and flexibility, let's define a Scala data type Sandwich to model sandwiches. It wraps a possibly empty list of ingredient layers. We assume the head of the list to be the layer at the top of the sandwich.
case class Sandwich(sandwich: List[Layer])
Note: In this case study, we implement Scala algebraic data type
constructors (i.e., product types) as case class
or
case object
entities. We implement union types using a
trait
with subtypes for the variants.
Data type Sandwich gives the specification for a sandwich. When "executed" by the SueChef, it results in the assembly of a sandwich that satisfies the specification.
As defined, the Sandwich data type does not require there to be a bread in the stack of ingredients. However, we add function newSandwich that starts a sandwich with a bread at the bottom and a function addLayer that adds a new ingredient to the top of the sandwich. We leave the implementation of these functions as exercises.
def newSandwich(b: Bread): Sandwich def addLayer(s: Sandwich)(x: Layer): Sandwich
Ingredients are in one of five categories: breads, meats, cheeses,
vegetables, and condiments. Because both the categories and the
specific type of ingredient is important, we choose to represent both
in the type structures and define the following types. A value of type
Layer represents a single ingredient. Type
Layer
has five variants (subtypes) Bread
,
Meat
, Cheese
, Vegetable
, and
Condiment
. Each of these variants itself has several
variants. For example, Bread
has variants (subtypes)
White
, Wheat
, and Rye
.
sealed trait Layer sealed trait Bread extends Layer case object White extends Bread case object Wheat extends Bread case object Rye extends Bread sealed trait Meat extends Layer case object Turkey extends Meat case object Chicken extends Meat case object Ham extends Meat case object RoastBeef extends Meat case object Tofu extends Meat sealed trait Cheese extends Layer case object American extends Cheese case object Swiss extends Cheese case object Jack extends Cheese case object Cheddar extends Cheese sealed trait Vegetable extends Layer case object Tomato extends Vegetable case object Onion extends Vegetable case object Lettuce extends Vegetable case object BellPepper extends Vegetable sealed trait Condiment extends Layer case object Mayo extends Condiment case object Mustard extends Condiment case object Ketchup extends Condiment case object Relish extends Condiment case object Tabasco extends Condiment
We need to be able to compare ingredients for equality and convert them to strings. Because the automatically generated definitions are appropriate, we do not need to do anything further. We will need to provide an appropriate definition of equality for Sandwich because the default element-by-element equality of lists does not seem to be the appropriate equality comparison for sandwiches.
To complete the model, we define type Platter to wrap a list of sandwiches.
case class Platter(platter: List[Sandwich])
We also define functions newPlatter to create a new Platter and addSandwich to add a sandwich to the Platter. We leave the implementation of these functions as exercises.
def newPlatter: Platter def addSandwich(p: Platter)(s: Sandwich): Platter
Please put these functions in a Scala module SandwichDSL. You may use functions defined earlier in the exercises to implement those later in the exercises.
def isBread(x: Layer): Boolean def isMeat(x: Layer): Boolean def isCheese(x: Layer): Boolean def isVegetable(x: Layer): Boolean def isCondiment(x: Layer): Boolean
def inOSO(s: Sandwich): Boolean def intoOSO(s: Sandwich)(defaultbread: Bread): Sandwich
def priceSandwich(s: Sandwich): Int val prices = List( (White,20),(Wheat,30),(Rye,30), (Turkey,100),(Chicken,80),(Ham,120),(RoastBeef,140),(Tofu,50), (American,50),(Swiss,60),(Jack,60),(Cheddar,60), (Tomato,25),(Onion,20),(Lettuce,20),(BellPepper,25), (Mayo,5),(Mustard,4),(Ketchup,4),(Relish,10),(Tabasco,5) )
def eqSandwich(sl: Sandwich)(sr: Sandwich): Boolean
In this section, we look at compiling the Platter and Sandwich descriptions to issue a sequence of commands for the SueChef's controller.
The SueChef supports the special instructions that can be issued in sequence to its controller. The algebraic data type SandwichOp below represents the instructions.
sealed trait SandwichOp case object StartSandwich extends SandwichOp case object FinishSandwich extends SandwichOp case class AddBread(bread: Bread) extends SandwichOp case class AddMeat(meat: Meat) extends SandwichOp case class AddCheese(cheese: Cheese) extends SandwichOp case class AddVegetable(vegetable: Vegetable) extends SandwichOp case class AddCondiment(condiment: Condiment) extends SandwichOp case object StartPlatter extends SandwichOp case object MoveToPlatter extends SandwichOp case object FinishPlatter extends SandwichOp
We also define the type Program to represent the sequence of commands resulting from compilation of a Sandwich or Platter specification.
case class Program(program: List[SandwichOp])
The flow of a program is given by the following pseudocode:
StartPlatter for each sandwich needed StartSandwich for each ingredient needed Add ingredient on top FinishSandwich MoveToPlatter FinishPlatter
Consider a sandwich defined as follows:
Sandwich(List(Bread Rye,Condiment Mayo,Cheese Swiss, Meat Ham,Bread Rye))
The corresponding sequence of SueChef commands would be the following.
List(StartSandwich,AddBread Rye,AddMeat Ham,AddCheeseSwiss, AddCondiment Mayo,AddBread Rye,FinishSandwich,MoveToPlatter)
def compileSandwich(s: Sandwich): List[SandwichOp]
def compile(p: Platter): Program
UP to CSci 555 Lecture Notes root document?