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:
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.
Next week, 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 Exam 1. (self-verifying-numbers.rkt is also part of the .zip file available for this session from the Session Summaries page.)
Once 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 procedure:
> (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
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 this 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 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 procedures 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 USPS money orders, you will have to code the same algorithm yet again!
But 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 procedure 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!
(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 procedure 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?
We can write a procedure that produces a procedure as its value. Let's change validate into a procedure that returns a validating procedure:
(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 procedure factory. It creates a new, customized validation procedure based on the values of its parameters. make-validator takes two arguments: a procedure and an integer. It returns as its answer a validation procedure that takes one argument, a list of digits. This validator procedure 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 procedure by evaluating a call to it:
> (make-validator ISBN-f 11) #<procedure>
... and that the generated procedure 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 procedure as data -- works.