An Application: Self-Verifying Numbers
Program One: UPC Codes
Suppose that your boss came to you with a new client, a grocer. This client would like you to write a program that verifies the UPC barcodes read by his scanner. It turns out that all legal UPC barcodes possess a distinctive arithmetic property used to catch errors.
If we call the ith digit of the barcode
d[i], then there is a function f such that
the sum of f(d[i],i) for all the digits in a legal
barcode is evenly divisible by 10. The function f(d[i],i) is
defined as:
- d[i], if i is odd
- 3 * d[i], if i is even
UPC barcodes are 0-based; that is, the first digit is considered the 0th digit.
No problem, you say. You write the following procedures to verify UPC barcode digits:
(define f
(lambda (digit pos)
(if (zero? (modulo pos 2))
(* 3 digit)
digit)))
We'd like to apply f to each digit in a UPC and
then add up the results to see if the sum is divisible by 10.
This sounds like a great application for
the map function
— except that map takes
a one-argument function. This f operates
on both the digit and the digit's position in
the sequence. We need a version of map that works
with procedures of two arguments, the digit and its position in
the list.
In a few sessions, we'll begin to study recursion in some
detail, and you'll learn some powerful techniques that will
help us implement such a function. For now, I'll do what any
programmer would do: I'll define a procedure named
counted-map that does just this for us.
The definition of counted-map is given in
the source file
that contains all of the code from this reading. Feel free
to study this procedure definition now if you'd like, but you
are free to save it for after Quiz 1.
self-verifying-numbers.rkt is also part of
the .zip file
available for Session 5, linked on the course home page.
Now that we have counted-map, writing a procedure
to verify UPC barcodes is pretty straightforward. We use
counted-map to apply f individually
to each digit in the list:
(counted-map f list-of-digits)
We then use apply to add up the list of values
returned by counted-map:
(apply + (counted-map f list-of-digits))
Then we need to find the remainder of dividing this value
by 10, using Racket's primitive procedure
modulo...
(modulo (apply + (counted-map f list-of-digits))
10)
... and verify that the number is evenly divisible by 10, by
checking this result with zero?
(zero? (modulo (apply + (counted-map f list-of-digits))
10))
Note: The (zero? (modulo ... 10)) code is
a Racket idiom that resembles a similar idiom in Java and C:
(x % 10) == 0.
That expression is the body of our function:
> (define valid-UPC?
(lambda (list-of-digits)
(zero? (modulo (apply + (counted-map f list-of-digits))
10))))
> (valid-UPC? '(2 7 3 5 9 1 9 3 0 5))
#t
> (valid-UPC? '(2 9 3 7 9 0 8 4 9 5))
#f
> (valid-UPC? '(3 9 2 0 8 9 0 0 0 3))
#t
Success.
Program Two: ISBN Codes
Time passes. Your boss is so proud of your concise solution that she shows it to another client, a bookstore employee who handles inventory. This client asks your boss to ask you to write a similar bit of code to verify ISBN numbers prior to processing special-request orders from his own clients.
While studying the topic further, you learn that ISBN numbers
also have a distinctive arithmetic property used to catch
errors: if we call the ith digit of the number d[i],
then the sum of f(d[i],i) for all the digits is
evenly divisible by 11. For ISBN numbers, the function
f(d[i],i) is defined as d[i] * i,
with 1 being the position of the first digit.
The only differences between a UPC code and an ISBN number
in thus regard is that they use a different function
f and a different modulus m!
You learn that there is an entire area of work dealing with
self-verifying numbers that share this property, with
only the function f and the modulus m
differing. Other numbers of this type are credit card numbers,
VINs on cars and other vehicles, and the serial numbers on US
Postal Service money orders.
When you re-examine your valid-UPC? procedure, you
realize now that you have hard-coded references to specific
values for f and m into your code.
So, you could do the same thing for ISBN numbers and write a
procedure that works in the same way:
> (define ISBN-f
(lambda (digit pos)
(* digit (+ pos 1))))
> (define valid-ISBN?
(lambda (list-of-digits)
(zero? (modulo (apply + (counted-map ISBN-f list-of-digits))
11))))
> (valid-ISBN? '(2 7 3 5 9 1 9 3 0 5))
#f
> (valid-ISBN? '(2 9 3 7 9 0 8 4 9 5))
#f
> (valid-ISBN? '(0 8 9 8 1 5 4 6 4 2))
#t
Your functions work fine, but they violate a basic principle
of programming: "Say everything once and only once."
If you have made a mistake in your code for the modular sum
algorithm, or if the algorithm changes for some reason, then
you will have to make the change in verify-UPC
and verify-ISBN. Even more important,
when your boss later asks you to implement solutions for
validating credit card numbers and US Postal Service money
orders, you will have to code the same algorithm yet again!
The Next Step: A Higher-Order Function
If both of these algorithms do exactly the same thing, only
with different values for f and m,
why not make f and m
parameters to a function that does the common work?
If both of the arguments were numbers or some other basic type, you would not think twice about doing this. It would be the normal thing to do!
Racket makes this step the normal thing to do for functions, too.
(define validate
(lambda (f m list-of-digits)
(zero? (modulo (apply + (counted-map f list-of-digits))
m))))
(define valid-UPC?
(lambda (list-of-digits)
(validate UPC-f 10 list-of-digits)))
(define valid-ISBN?
(lambda (list-of-digits)
(validate ISBN-f 11 list-of-digits)))
This allows us to validate numbers, at the cost of an extra function call. In other languages, though, you might not be able to do more than this. Functions in most languages can return many kinds of values, but not procedures. But in Racket, a function is like any other value. Can we do better?
Yes. We can write a function that
produces a function as its value.
Let's change validate into a function that
returns a validating function:
(define make-validator
(lambda (f m)
; -------------------------------------------------------
(lambda (list-of-digits)
(zero? (modulo (apply + (counted-map f list-of-digits))
m)))
; -------------------------------------------------------
))
You might think of make-validator as a
function factory. It creates a new, customized
validation function based on the values of its parameters.
make-validator takes two arguments: a function
and an integer. It returns as its answer a validation
function that takes one argument, a list of digits. This
validator function determines whether the list of digits form
a valid number, according to its values for f and
m.
You can see for yourself that make-validator
returns a function by evaluating a call to it:
> (make-validator ISBN-f 11) #<procedure>
... and that the generated functions works just like the one we defined above:
> ((make-validator ISBN-f 11) '(2 7 3 5 9 1 9 3 0 5)) #f > ((make-validator ISBN-f 11) '(2 7 3 5 9 1 9 2 1 9)) #f > ((make-validator ISBN-f 11) '(0 8 9 8 1 5 4 6 4 2)) #t
Now, we can define valid-ISBN? as a verifier that
uses the required values for f and m:
> (define valid-ISBN?
(make-validator ISBN-f 11))
> (valid-ISBN? '(2 7 3 5 9 1 9 2 1 9))
#f
> (valid-ISBN? '(0 8 9 8 1 5 4 6 4 2))
#t
We can also define valid-UPC? using
make-validator:
> (define valid-UPC?
(make-validator UPC-f 10))
> (valid-UPC? '(2 7 3 5 9 1 9 3 0 5))
#t
> (valid-UPC? '(2 7 3 5 9 1 9 3 2 7))
#f
> (valid-UPC? '(3 9 2 0 8 9 0 0 0 3))
#t
You will now be able to generate code for clients with similar problems quite easily! A productive programmer is a happy programmer :-) — and she can also make her boss and clients happy!
Please download the self-verifying number code, and tinker with it as a way to understand how it — and the idea of function as data — works.