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. If you are building a math library in C, you'll need to write several versions of each function: one for int
, one for float
, one for double
, and so on. This is the burden of static typing. One way to alleviate this burden is to add dynamic typing to C using type tags.
In this exercise, you build your own library of polymorphic math functions. Each function has only a single version that accepts a general number. This number is actually 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 may hold an int
, a float
, or a double
. Use typedef
to give the type its name.polynumber_t
that pairs a type 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 polynumber_t
whose tag and value are appropriately set. Do not dynamically allocate the struct with malloc
. Heap memory is not necessary for this task.negate
that accepts a polynumber_t
parameter and returns a new polynumber_t
holding the negated value. If given an integer, the returned polynumber_t
holds the negated integer. Do likewise for floats and doubles. Use your constructors to make the new number. Do not modify the parameter.add
that accepts two polynumber_t
parameters and returns a new polynumber_t
of the sum. Maintain the type. Use your constructors to make the new number. 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. Normally you don't see the tag inspection like you do in this exercise.
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
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
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
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 IDEA and run a class that has 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.