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

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 may hold an int, a float, or a double. Use typedef to give the type its name.
Define a struct type named 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.
Write three “constructors” for the three numeric primitive types. Name them 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.
Write function 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.
Write function 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.
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. 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:

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

← Monkey Patching