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 map
—
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
a source file
that contains all of the code from this section. 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 this session from the Session Summaries 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.
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
! For ISBN numbers, the function
f(d[i],i)
is defined as d[i] * i
, with 1
being the position of the first digit.
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 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 eturns 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.