See the top of this for instructions on installing Scala at home. Remember that this is only to enable you to play with the assignment at home: The final version turned in must work on the ACS Linux machines. While you can use Windows/Mac etc. to begin working with Scala, the code you turn in must be that required for the Linux environment.
The overall objective of this assignment is to introduce you to some of the advanced features of Scala, including case classes, implicits and randomized property checking.
The assignment is spread over a single zip file hw6.zip. When you download and open it with unzip hw6.zip out come tumbling the following files
which contain several skeleton Scala functions, with missing bodies, which currently contain the placeholder text sys.error("TO BE WRITTEN").
Your task is to replace the placeholder text in those files with the appropriate Scala code for each case.
The zip also contains the following helper files that you will not modify
.out filesYour functions/programs must compile and/or run on a ACS Linux machine (e.g. ieng6.ucsd.edu) as this is where your solutions will be checked. While you may develop your code on any system, ensure that your code runs as expected on an ACS machine prior to submission. You should test your code in the directories from which the zip files (see below) will be created, as this will approximate the environment used for grading the assignment.
Most of the points, will be awarded automatically, by evaluating your functions against a given test suite. test.ml contains a very small suite of tests which gives you a flavor of of these tests. At any stage, by typing at the UNIX shell :
> make test | grep "130>>" > logyou will get a report on how your code stacks up against the simple tests.
The last line of the file log must contain the word ``Compiled" otherwise you get a zero for the whole assignment.
If for some problem, you cannot get the code to compile, leave it as is with the sys.error("TO BE DONE") with your partial solution enclosed below as a comment.
The second last line of the log file will contain your overall score, and the other lines will give you a readout for each test. You are encouraged to try to understand the code in Test.scala, but you will not be graded on this.
Alternately, after doing make in the UNIX shell, inside the Scala shell, type:
scala> import Test._
scala> Test()
.
.
130>> Results: ...
130>> Compiledand it should return a pair of integers, reflecting your score and the max possible score on the sample tests. If instead an error message appears, your code will receive a zero.
To turn-in your solution, simply type
$ make turninin the directory in which you are working.
turnin will provide you with a confirmation of the submission process; make sure that the size of the file indicated by turnin matches the size of your zip file. See the ACS Web page on turnin for more information on the operation of the program.
For this problem, you will use Scala’s case-classes to implement an Abstract Set datatype, whose properties are captured by the following trait:
trait AbstractSet[A] extends Iterable[A] {
def contains(n: A): Boolean
def add(n: A): AbstractSet[A]
def remove(n: A): AbstractSet[A]
// For Testing
def execCmds(cmds: List[(Boolean, A)]): AbstractSet[A] = {
cmds.foldLeft(this)((s, cmd) => {
if (cmd._1) { this.add(cmd._2) } else { this.remove(cmd._2) }
})
}
}includes the usual set operations which you will implement, and method that uses add and remove for testing (we’ll see this later.)
The sets will be represented as Binary Search Trees that support efficient addition and removal of elements.
We represent the datatype as:
case class Node[A](elt: A, left: BST[A], right: BST[A]) extends BST[A]
case class Leaf[A]() extends BST[A]
sealed abstract class BST[A] extends AbstractSet[A] (the real code includes the implicits that allow for comparisons.)
Thus, values of type BST[A] are of the form Leaf() or Node(e, l, r) where e is an element of type A and l and r are left and right subtrees of type BST[A].
We will only use trees that are binary search ordered, which is to say, trees t such that t.isOrdered evaluates to true.
Note: Make sure you understand what the isOrdered method is doing!
Fill in the definition of the method BST.build_ that converts the supplied List[A] into a BST[A] by recursively splitting the list up into sub-lists, converting the sub-lists to trees. Make sure that the resulting BST[A] is binary-search-ordered.
When you are done you should get the following behavior:
scala> BstProperties.prop_bso.check
+ OK, passed 100 tests.Next, fill in the definition of the method
def contains(x: A): Boolean so that t.contains(x) returns true iff the element x is in the tree t. When you are done, you should see the following behavior at the REPL
scala> BST.t2
res6: BST[Int] = Node(5, 10, 20, 30)
scala> List(5,6,10,11,20,21,30,31).map(BST.t2 contains _)
res7: List[Boolean] = List(true, false, true, false, true, false, true, false)Writing tests is tedious, instead we will check properties! This one (go read the definition, or else!) states that trees generated by the build method are indeed binary-search ordered.
scala> BstProperties.prop_contains_elt.check
+ OK, passed 100 tests.Now, write a fold method that performs an in-order traversal of the tree, accumulating the results as it goes.
def fold[B](f: (B, A) => B, acc: B): B When you are done, various bonus properties of the tree get unlocked by virtue of the iterator method that we have supplied that uses the above fold. In particular, you should get the following behavior
cala> BST.t2.toList
res11: List[Int] = List(5, 10, 20, 30)
scala> BST.t2.toString
res13: String = BST(5, 10, 20, 30)You should also check the property that the trees you build actually contain the elements from the source list.
scala> BstProperties.prop_contains_elts.check
+ OK, passed 100 tests.OK. Now fill in the definition for
def add(x:A): BST[A]which returns a (new) BST which has the same elements as the original tree plus the new element x. Of course, the new tree should also satisfy the binary-search-ordering invariant. When you are done, you should see the following behavior
scala> BST.t2
res0: BST[Int] = BST(5, 10, 20, 30)
scala> BST.t2.add(12)
res1: BST[Int] = BST(5, 10, 12, 20, 30)
scala> val t2_new = BST.t2.add(12)
t2_new: BST[Int] = BST(5, 10, 12, 20, 30)
scala> t2_new.isOrdered
res2: Boolean = trueWriting tests by hand is boring, so instead we check the property that the added elements are indeed in the new set. (Make sure you understand the properties.)
Scala> BstProperties.prop_add_elt.check
+ OK, passed 100 tests.
scala> BstProperties.prop_add_elts_old.check
+ OK, passed 100 tests.We’ve seen a bunch of properties go sailing through. But consider this:
Scala> BstProperties.prop_multiset.check
! Falsified after 5 passed tests.Uh oh. Lets try to debug the code and property to see why the test fails. In the scala shell, you should see something like this:
scala> BstProperties.prop_multiset.check
! Falsified after 4 passed tests.
> ARG_0: List("1", "1")You might see something different for ARG_0, i.e. some other list, but nevertheless you will see some kind of list. Well, lets see why the property failed, by running the test on the failing input that Scalacheck has automatically found for us!
First, copy that input into a variable,
scala> val xs = List(1, 1)
xs: List[Int] = List(1, 1)(again, your failing input may be slightly different, because Scalacheck finds them by random sampling.) Now, lets run the property on that input. How? Well the property is
forAll((xs: List[Int]) => BST(xs).toList == xs.sorted)That is, for any list xs the result of building a BST from xs and converting it to a list, should be identical to just sorting the list xs. Lets see if that is true, for the particular xs that we have above.
scala> BST(xs).toList == xs.sorted
res2: Boolean = falsehmm, why? well dig further, the left hand side of the equality is
scala> BST(xs).toList
res3: List[Int] = List(1)and the right hand side is
scala> xs.sorted
res4: List[Int] = List(1, 1)of course! The BST does not keep duplicates (no duplicates on the LHS), but vanilla sorting does keep duplicates! So the property is broken, we want to check only that the BST has all the elements from xs without any duplicates.
Fix the property so that it passes. Do this in a meaningful way – don’t just replace it with true.
Note: Use the above strategy to fix your code when there are other failing tests too (its just in this one case that the property itself was “wrong” :)).
We also want to remove elements from the set. As a first step, write a method
def removeMin : (A, BST[A]) that returns a tuple of the minimum element in the tree, and the tree containing all elements except the minimum. (If the tree passed in is empty, so there is no minimum element, use sys.error to indicate the problem.) When you are done you should see this behavior
Scala> BST.t2.removeMin
res7: (Int, BST[Int]) = (5,BST(10, 20, 30))or better, with the properties
Scala> BstProperties.prop_remove_min.check
+ OK, passed 100 tests.Finally, use removeMin to fill in the definition of
def remove(x: A): BST[A]which returns the set containing all elements except x. Of course, the new set should satisfy the binary-search-ordering property. When you are done, you should see the following behavior.
scala> BST.t2.remove(12)
res1: BST[Int] = BST(5, 10, 20, 30)
scala> BST.t2
res2: BST[Int] = BST(5, 10, 20, 30)
scala> BST.t2.remove(20)
res3: BST[Int] = BST(5, 10, 30)and we can stress test with properties
scala> BstProperties.prop_remove_elt.check
+ OK, passed 100 tests.
scala> BstProperties.prop_remove_elt_old.check
+ OK, passed 100 tests.
scala> BstProperties.prop_bst_equiv_gold.check
+ OK, passed 100 tests.In Scala, functions are (also) objects. In this exercise, you will see an example of the decorator design pattern , where we add functionality to objects via wrappers.
In the file Decorators.scala there is an example decorator profiled and stub code for decorators trace and memo that you will fill in. The file DecoratorTest.scala contains several examples of decorated functions. The expected output for all these functions is available in decorators.out.
profiled is an object whose field cm maps strings to an integer counter (initially 0). The apply method
def apply[A, B](name: String)(f: A => B) = new Function1[A, B] {...}takes two arguments, a name string and a function f and returns a new function object whose apply method behaves just like f but at each call, increments the count associated with the name. Thus, to find out how many times a decorated function named name has been called, we can simply call profile.count(name).
$ makeYou should see the following behavior at the REPL
scala> import Decorators._
scala> import DecoratorTest._
scala> profile.count("fac")
res0: Int = 0
scala> fac(5)
res1: Int = 120
scala> profile.count("fac")
res2: Int = 5
scala> fac(10)
res3: Int = 3628800
scala> profile.count("fac")
res4: Int = 15
scala> profile.reset("fac")
scala> profile.count("fac")
res6: Int = 0
scala> fac(4)
res7: Int = 24
scala> profile.count("fac")
res8: Int = 4Complete the definition for the decorator trace. (You may need to add extra private fields to the function object that the trace object’s apply method returns.) When the decorated function is called, the decorator should print out an ASCII art tree of the recursive calls and their return values. The format of the tree should be as follows:
Print a pipe symbol followed by a space (|) for every level of nested function calls,
Print a comma then a minus sign then a space (,-) next,
Print the name of the function being traced followed by a string representation of the argument in parentheses.
Next, increase the nesting level and call the function itself.
At the original nesting level, print a pipe symbol followed by a space (|) for every level of nested function calls,
Print a backquote then a minus sign then a space(\- `)
Finally, print the string representation of the return value.
The return value of the function should be return to the caller after all printing is complete.
When you are done, you should see the following behavior
scala> import Decorators._
scala> import DecoratorTest._
scala> fibT(5)
,- fibT(5)
| ,- fibT(4)
| | ,- fibT(3)
| | | ,- fibT(2)
| | | | ,- fibT(1)
| | | | `- 1
| | | | ,- fibT(0)
| | | | `- 1
| | | `- 2
| | | ,- fibT(1)
| | | `- 1
| | `- 3
| | ,- fibT(2)
| | | ,- fibT(1)
| | | `- 1
| | | ,- fibT(0)
| | | `- 1
| | `- 2
| `- 5
| ,- fibT(3)
| | ,- fibT(2)
| | | ,- fibT(1)
| | | `- 1
| | | ,- fibT(0)
| | | `- 1
| | `- 2
| | ,- fibT(1)
| | `- 1
| `- 3
`- 8
res9: Int = 8
scala> even(5)
,- even(5)
| ,- odd(4)
| | ,- even(3)
| | | ,- odd(2)
| | | | ,- even(1)
| | | | `- false
| | | `- false
| | `- false
| `- false
`- false
res10: Boolean = falseExtend your tracing function to properly handle exceptions. If an exception occurs in the function, the nesting level must be adjusted to the appropriate level where the exception is caught.
When you are done, you should get the following behavior:
scala> changeT(List(5, 3), 6)
,- changeT((List(5, 3),6))
| ,- changeT((List(5, 3),1))
| | ,- changeT((List(3),1))
| | | ,- changeT((List(),1))
| ,- changeT((List(3),6))
| | ,- changeT((List(3),3))
| | | ,- changeT((List(3),0))
| | | `- List()
| | `- List(3)
| `- List(3, 3)
`- List(3, 3)Next, complete the definition of the memo decorator. The purpose of this decorator is to cache the results of calls to a function, and to directly return the cached result on subsequent calls with the same arguments.
To this end, you need to add a private cache to the wrapped function object. In the apply method of the wrapped object, you should check whether the given arguments are already in the cache.
If so, the method should return the value the function returned when it was originally called with the given arguments.
If the function has not been called with the given arguments, then call it and record the return value in the cache, before returning the value.
Once you have implemented the function, you should get the following behavior at the Python prompt:
scala> import DecoratorTest._
import DecoratorTest._
scala> snooze(0)
// 5 second pause
res0: Int = 1
scala> snooze(0)
// practically instantaneous
res1: Int = 1Extend the decorator so that it handles functions that throw exceptions. That is,
extend the cache so that it stores whether the function returned a proper value, or threw an exception.
extend the apply method so that if a call throws an exception then the exception is stored in the cache (instead of the return value.)
extend the apply method to that if function is called with previously cached arguments, it should return the previous value if a proper value was returned, and it should throw the same exception that was thrown previously otherwise.
Hint: Scala reuses Java’s exception mechanism, so an exception is simply an object of type Throwable. See this for a refresher.
Hint: You may want to use the Either to remember whether the function threw an exception or returned a real value.
When you are done you should see the following behavior at the REPL
scala> import DecoratorTest._
import DecoratorTest._
scala> hissy(0)
// 5 second pause
res0: Boolean = true
scala> hissy(1)
// 5 second pause
DecoratorTest$HissyFitException
at DecoratorTest$$anonfun$16.apply$mcZI$sp(Test.scala:123)
at DecoratorTest$$anonfun$16.apply(Test.scala:121)
...
scala> hissy(0)
// practically instantaneous
res1: Boolean = true
scala> hissy(1)
// practically instantaneous
DecoratorTest$HissyFitException
at DecoratorTest$$anonfun$16.apply$mcZI$sp(Test.scala:123)
at DecoratorTest$$anonfun$16.apply(Test.scala:121)
...When you have finished with decorators, you can test your solution as follows:
$ make decorators
$ scala DecoratorTest > out
$ diff out decorators.out
$That is, the output file out should be identical to the supplied, reference output file decorators.out.
For this problem you will write a simple document layout engine, that allows the clean printing of nested documents.
A document is represented by an instance of the class
class Doc(val lines: List[String])Thus, a document is a list of lines, each of which is a string. For example, the document
ruination
whiterascal
fattireis represented as
Doc(List("ruination", "whiterascal", "fattire"))We have also provided implementations of methods for computing
height of a document (the number of lines)width of a document (the maximum number of characters in a line)Fill in the definition of padBegin(xs, n, x), which returns a list of length n with enough copies of x (at the beginning) followed by xs. When you are done, you should see the following behavior at the prompt.
//If length is less than n then output is padding ++ input
scala> Doc.padBegin(List(1,2,3,4,5), 10, 0)
res: List[Int] = List(0,0,0,0,0,1,2,3,4,5)
//If length is greater than n then output is same as input
scala> Doc.padBegin(List(1,2,3,4,5), 3, 0)
res: List[Int] = List(1,2,3,4,5)Fill in the definition of padEnd(xs, n, x), which returns a list of length n with xs followed by enough copies of x (at the end). When you are done, you should see the following behavior at the prompt.
//If length is less than n then output is input ++ padding
scala> Doc.padEnd(List(1,2,3,4,5), 10, 0)
res: List[Int] = List(1,2,3,4,5,0,0,0,0,0)
//If length is greater than n then output is same as input
scala> Doc.padEnd(List(1,2,3,4,5), 3, 0)
res: List[Int] = List(1,2,3,4,5)Fill in the implementation of method vcat which given the receiver (Doc) object and another Doc (named that), returns a new document that is the vertical concatenation of the receiver and that with the documents aligned at the left. When you are done, you should see the following behavior at the prompt.
scala> val d0 = Doc("cat") vcat Doc("mongoose")
d0: Doc =
cat
mongooseNext, fill in the implementation of method hcatT which given the receiver (Doc) object and another Doc (named that), returns a new document that is the horizontally concatenation of the receiver and that with the top lines horizontally aligned. When you are done, you should see the following behavior at the prompt.
scala> val dks = Doc("1: ", "2: ", "3: ")
scala> val dvs = Doc("cat", "doggerel", "moose")
scala> dks hcatT dvs
res: Doc =
1: cat
2: doggerel
3: moose
scala> Doc("{ ") hcatT dks hcatT dvs
{ 1: cat
2: doggerel
3: moose
scala> dvs hcatT Doc(" ") hcatT dks
res3: Doc =
cat 1:
doggerel 2:
moose 3:
scala> dvs hcatT Doc("---> at the top")
res5: Doc =
cat ---> at the top
doggerel
moose Next, fill in the implementation of method hcatB which given the receiver (Doc) object and another Doc (named that), returns a new document that is the horizontally concatenation of the receiver and that with the bottom lines horizontally aligned. When you are done, you should see the following behavior at the prompt.
scala> val dks = Doc("1: ", "2: ", "3: ")
scala> val dvs = Doc("cat", "doggerel", "moose")
scala> dks hcatB dvs
res: Doc =
1: cat
2: doggerel
3: moose
scala> Doc("{ ") hcatB dks hcatB dvs
1: cat
2: doggerel
{ 3: moose
scala> dks hcatB dvs hcatB Doc(" }")
1: cat
2: doggerel
3: moose }
scala> dvs hcatB Doc(" ") hcatB dks
res3: Doc =
cat 1:
doggerel 2:
moose 3:
scala> dvs hcatB Doc("---> at the bottom")
res5: Doc =
cat
doggerel
moose ---> at the bottomWhen both hcat and vcat are working you should get the following behavior at the prompt:
scala> val d0 = Doc("cat") vcat Doc("mongoose")
scala> val d1 = d0 hcatT Doc(" ") hcatT d0 hcatT Doc(" ") hcatT d0
d1: Doc =
cat cat cat
mongoose mongoose mongoose
scala> val d2 = Doc("apple") vcat d1
d2: Doc =
apple
cat cat cat
mongoose mongoose mongoose
scala> val d3 = d2 hcatT Doc(" ") hcatT d2
d3: Doc =
apple apple
cat cat cat cat cat cat
mongoose mongoose mongoose mongoose mongoose mongooseJSON – JavaScript Object Notation is a lightweight data-interchange format, that is human- and machine-readable, and commonly used in so-called AJAX web applications like Gmail, FaceBook etc. to ship data across machines.
We have defined a Scala class JVal that represents Json values:
sealed abstract class JVal
case class JStr(s: String) extends JVal
case class JNum(n: BigDecimal) extends JVal
case class JObj(o: Map[String, JVal]) extends JVal
case class JArr(a: List[JVal]) extends JValInformally, a JVal is one of
String or BigDecimal, or,JVal or,String keys to JValIn this problem, use the document formatter from above to convert JVal to pretty Doc which can be shown as Strings, and then
JValUse the Doc formatting class defined above to fill in the definition of the function JVal.render
def render(jv: JVal) : Doc When you are done, you should see the following behavior at the prompt:
scala> import JsonTest._
import JsonTest._
scala> JVal.render(jvReals(0))
res4: Doc =
{ fst : 1
, snd : "cat" }
scala> JVal.render(jvReals(1))
res5: Doc =
{ fst : { fst : 1
, snd : "cat" }
, snd : { fst : 1
, snd : "cat" }
, thd : { fst : 1
, snd : "cat" } }
scala> JVal.render(jvReals(2))
res6: Doc =
{ fst : 1
, snd : "cat"
, thd : { fst : { fst : 1
, snd : "cat" }
, snd : { fst : 1
, snd : "cat" }
, thd : { fst : 1
, snd : "cat" } } }
scala> JVal.render(jvReals(3))
res10: Doc =
{ fst : 1
, snd : { fst : 2
, snd : { fst : 3
, snd : { fst : 4
, snd : { fst : 5
, snd : "Nil" } } } } }
scala> JVal.render(jvReals(4))
res: Doc =
{ { fst : "one"
, snd : 1 }
, { fst : "two"
, snd : 2 }
, { fst : "three"
, snd : 3 } }
scala> JVal.render(jvReals(5))
res: Doc =
{ 1
, 2
, 3
, 4 }
scala> JVal.render(jvReals(6))
res8: Doc =
{ mon : 10
, tue : 20
, wed : 30 }
scala> JVal.render(jvReals(7))
scala> JVal.render(jvReals(7))
res6: Doc =
{ eng : { { fst : "one"
, snd : 1 }
, { fst : "two"
, snd : 2 }
, { fst : "three"
, snd : 3 } }
, spanish : { { fst : "uno"
, snd : 1 }
, { fst : "dos"
, snd : 2 }
, { fst : "tres"
, snd : 3 } }
, french : { { fst : "un"
, snd : 1 }
, { fst : "deux"
, snd : 2 }
, { fst : "trois"
, snd : 3 } } }Now, it is rather lame to have to write down JVal manually. Next, you will use Scala’s traits and implicits to automatically serialize comples Scala scala values tuples, arrays, maps, and nested combinations thereof, into Json values.
We have defined a trait
trait Json[T] {
def json(t: T): JVal
}which corresponds to a typeclass representing values that can be converted to JVal. The object JsonWriter uses the above trait to define methods that will serialize Scala values:
object JsonWriter {
def write(v: JVal): String =
JVal.render(v).toString
def write[A: Json](x: A): String =
write(Json.toJson(x))
}The write method takes any input of type A as long as A has the Json trait, and converts it a String. To do so, it uses the implicit Just.toJson method to obtain a JVal which is then rendered as a String.
For example, to render some Scala values to Json just do:
scala> import Json._ //puts the implicit views into scope
scala> Json.toJson(1)
res5: JVal = JNum(1)
scala> Json.toJson("dog")
res6: JVal = JStr(dog)
scala> Json.toJson((1,(2,(3,"Null"))))
res9: JVal = JObj(Map(fst -> JNum(1), snd -> JObj(Map(fst -> JNum(2), snd -> JObj(Map(fst -> JNum(3), snd -> JStr(Null)))))))
scala>And assuming your render function is working properly,
scala> JsonWriter.write(1,(2,(3,"Null")))
res10: String =
{ fst : 1
, snd : { fst : 2
, snd : { fst : 3
, snd : "Null" } } }Note that in the last case, we are automatically converting a type for which we didn’t even a Json converter (!) because Scala is composing the implicits for Int, String and (A, B) in a type-directed manner.
Using the definition of tuple2Json for inspiration, fill in the definition of tuple3Json. When you are done, you should see the following behavior at the prompt.
scala> import Json._
scala> Json.toJson(tup1)
res16: JVal = JObj(Map(fst -> JObj(Map(fst -> JNum(1), snd -> JStr(cat))), snd -> JObj(Map(fst -> JNum(1), snd -> JStr(cat))), thd -> JObj(Map(fst -> JNum(1), snd -> JStr(cat)))))
scala> JsonWriter.write(tup1)
res13: String =
{ fst : { fst : 1
, snd : "cat" }
, snd : { fst : 1
, snd : "cat" }
, thd : { fst : 1
, snd : "cat" } }
scala> JsonWriter.write(tup2)
res14: String =
{ fst : 1
, snd : "cat"
, thd : { fst : { fst : 1
, snd : "cat" }
, snd : { fst : 1
, snd : "cat" }
, thd : { fst : 1
, snd : "cat" } } }Next, fill in listJson. When you are done, you should see the following behavior at the prompt.
scala> import Json._
scala> Json.toJson(intl)
res17: JVal = JArr(List(JObj(Map(fst -> JStr(one), snd -> JNum(1))), JObj(Map(fst -> JStr(two), snd -> JNum(2))), JObj(Map(fst -> JStr(three), snd -> JNum(3)))))
scala> JsonWriter.write(intl)
res18: String =
{ { fst : "one"
, snd : 1 }
, { fst : "two"
, snd : 2 }
, { fst : "three"
, snd : 3 } }Next, fill in arrJson . When you are done, you should see the following behavior at the prompt.
scala> import Json._
scala> Json.toJson(ints)
res: JVal = JArr(List(JNum(1), JNum(2), JNum(3), JNum(4)))
scala> JsonWriter.write(ints)
res: String =
{ 1
, 2
, 3
, 4 }Finally, fill in mapJson. When you are done, you should see the following behavior at the prompt.
scala> import Json._
scala> Json.toJson(mm)
res: JVal = JObj(Map(mon -> JNum(10), tue -> JNum(20), wed -> JNum(30)))
scala> JsonWriter.write(mm)
res: String =
{ mon : 10
, tue : 20
, wed : 30 }
scala> JsonWriter.write(lang)
res8: String =
{ eng : { { fst : "one"
, snd : 1 }
, { fst : "two"
, snd : 2 }
, { fst : "three"
, snd : 3 } }
, spanish : { { fst : "uno"
, snd : 1 }
, { fst : "dos"
, snd : 2 }
, { fst : "tres"
, snd : 3 } }
, french : { { fst : "un"
, snd : 1 }
, { fst : "deux"
, snd : 2 }
, { fst : "trois"
, snd : 3 } } }