Lab: Dynamic Typing

Dear Computer

Chapter 3: Types

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:

Define an enum type named tag_t that has members INT, FLOAT, and DOUBLE. Use typedef to give the type its name.
Define a union type named value_t that can hold an int, a float, or a double. Use typedef to give the type its name.
Define a struct type named 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.
Write three “constructors” for each of the different numeric primitive types. Name them 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.
Write function 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.
Write function 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.
Write a 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:

Ruby
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:

Ruby
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:

Ruby
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.

← Lecture: Settle, Part 3