Lab: Dynamic Typing
In today's lab, you'll explore dynamic typing in two different contexts. First, you'll add dynamic typing via tags to C to build a polymorphic math library. Second, you'll add a reflection-based unit test runner to Ruby.
Dynamic Typing in C
C is a statically-typed language, but it's powerful enough to let you build your own dynamic typing system using tags. In this exercise, you build your own library of math functions that accept numbers of arbitrary types. This arbitrary number is a struct with two fields: a union that can hold a value of any of C's three most common number types, and a tag that indicates which type is active. Build this type and two arithmetic operations by following these steps:
tag_t
that has members INT
, FLOAT
, and DOUBLE
. Use typedef
to give the type its name.value_t
that can hold an int
, a float
, or a double
. Use typedef
to give the type its name.number_t
that pairs a tag and a value. Use typedef
to give the type its name. A value of this type represents a generic number. It could be a floating-point number. It could be an integer. Nobody knows—until they look at the tag.create_int
, create_float
, and create_double
. Accept a parameter of the appropriate type and return a number_t
whose tag and value are appropriately set. Do not dynamically allocate the struct with malloc
. The pain of managing memory is not worth it for this task.negate
that accepts a number_t
parameter and returns a new number_t
holding the negated value. If given an integer, the returned number_t
holds the negated integer. Do likewise for floats and doubles. Do not modify the parameter.add
that accepts two number_t
parameters and returns a new number_t
of the sum. Maintain the type. Allow no coercion; assert that the parameters have the same numeric type. If not, print an error message and exit with a non-zero status. Do not modify the parameter.main
function that tests your negation and summation functions.Some dynamically-typed languages use tags like this to ask an object what its type is.
Test Runner
Most of the dynamically-typed languages that you use don't use tags. Instead they use duck typing. The interpreter typechecks by asking a value if it supports the requested operation. For duck typing to work, your language needs reflection, which is the ability to dynamically ask questions of your code. In this exercise, you'll use reflection to create a test runner.
Consider this assert function that prints an error message if two values aren't the same:
def assert_equal(expected, actual, message)
if expected != actual
STDERR.puts message
STDERR.puts " Expected: #{expected}"
STDERR.puts " Actual: #{actual}"
STDERR.puts
end
end
Also consider this class with several unit tests:
class MathTester
def test_round
assert_equal(6, 5.6.round, "I tried rounding a float, but I didn't get the value I expected.")
end
def helper
raise 'You shouldn\'t see this message because this method should not be automatically called by the test runner.'
end
def test_max
assert_equal(3.9, [-1.1, 3.9, 3.1].max, "I tried finding the max of some values, but I didn't get the max I expected.")
end
def test_plus
assert_equal(13, 4 + 9, "I tried adding two values, but I didn't get the sum I expected.")
end
def test_pi
# This test fails because it's a bad test.
assert_equal(3, Math::PI, "I tried testing pi, but it wasn't the value I expected.")
end
# ...
end
Each method in MathTester
whose name has the prefix test_
is a unit test. To run these tests, you could make an instance of MathTester
and call all of these methods manually, like this:
tester = MathTester.new
tester.test_round
tester.test_max
tester.test_plus
tester.test_pi
If you have a lot of unit tests, this is too much labor. Instead, write a universal test runner method named run_tests
that accepts an arbitrary class as its only parameter, creates an instance of the class, queries the instance for its test methods, and runs each test method on the instance by calling send
. You can get a list of all methods using reflection. Define run_tests
at the top level, not inside any class. Add more test_
methods to MathTester
and ensure that they are run by run_tests
.
A similar test runner is built into the JUnit testing framework for Java. When you're in Eclipse or IntelliJ and run a class with methods annotated with @Test
, the runner finds these methods using reflection and calls them.
Submit
To receive credit for this lab, you must submit your .c
and .rb
source files on Canvas by Monday noon. Late labs or forgot-to-submits are not accepted because Monday at noon is when your instructor has time to grade.