#lang scribble/manual @(require "../vkp-scrbl/unnumbered.rkt" "../vkp-scrbl/utils.rkt") @title{Lecture 12: Abstracting over behavior: Function objects} @para[#:style "boxed"]{ Learn how to represent behavior as an object@linebreak{} Practice abstraction over behavior.@linebreak{}@linebreak{} @hspace[4] @link["ExamplesBooks.java"]{ExamplesBooks.java} @hspace[2]} @section*{Lecture 12: Outline} @itemlist[ @item{The need to abstract over behavior} @item{Basic function objects} @item{Parametrized function objects}] @section{Three Questions: Abstracting over behavior: Function objects} @subsection{Introduction} We will be working with a class of books. Each book has an author, year of publication, and a price. We would like to answer the following questions about various lists of books: @verbatim{ A1: Is there a book in this list that is written by "MF"? B1: Are all the books in this list written by "MF"? C1: Produce a list of all books in this list written by "MF". A2: Is there a book in this list published before 1980? B2: Are all the books in this list published before 1980? C2: Produce a list of all books in this list published before 1980. A3: Is there a book in this list that costs more than $10? B3: Are all the books in this list that cost more than $10? C3: Produce a list of all books in this list that cost more than $10. } We can clearly see several patterns in these questions. All the @emph{A} questions inquire whether there is at least one item with the given property. All the @emph{B} question question whether all items in the list satisfy the given property. Finally, the question @emph{C} is really a request to extract from the list only those items that satisfy the given property. We start with designing the classes to represent books and lists of books. The class diagram is shown here - the code appears at the end, toghether with examples. @verbatim{ +------+ | ALoB |<-------------+ +------+ | +------+ | / \ | --- | | | ---------------- | | | | +-------+ +------------+ | | MTLoB | | ConsLoB | | +-------+ +------------+ | +-------+ +-| Book first | | | | ALoB rest |----+ | +------------+ | v +---------------+ | Book | +---------------+ | String author | | int year | | int price | +---------------+ } We start with examples of books and lists of books. @verbatim{ Book b1 = new Book("MF", 2001, 60); Book b2 = new Book("EX", 1942, 15); Book b3 = new Book("JL", 1900, 8); Book b4 = new Book("MF", 1998, 15); ALoB mtlist = new MTLoB(); ALoB list1 = new ConsLoB(b1, new ConsLoB(b4, mtlist)); ALoB list2 = new ConsLoB(b3, mtlist); ALoB list3 = new ConsLoB(b2, new ConsLoB(b3, mtlist)); ALoB list4 = new ConsLoB(b1, new ConsLoB(b4, new ConsLoB(b3, mtlist))); ALoB list5 = new ConsLoB(b2, new ConsLoB(b3, list1)); } We can make examples for all questions: @verbatim{ A1: true for list1, false for list2, false for mtlist B1: true for list1, false for list5, true for mtlist C1: from list 5 produces list 1 A2: true for list5, false for list1, false for mtlist B2: true for list3, false for list5, true for mtlist C2: from list4 produces list3 A3: true for list1, false for list2, false for mtlist B3: true for list3, false for list4, true for mtlist C3: for list4 produces list1 } We will start with methods for the questions @emph{A1}, @emph{A2}, and @emph{A3}. For the empty list the answer is always 'false'. We can write the purpose statement header for the class @tt{ALoB} and the body for the class @tt{MTLoB}: @verbatim{ // In the class ALoB: // is there a book written by "MF" in this list? abstract boolean anyBookWrittenByMF(); // is there a book published before 1980 in this list? abstract boolean anyBookBefore1980(); // is there a book that costs more than $10 in this list? abstract boolean anyBookMoreThan10(); // In the class MTLoB: boolean anyBookWrittenByMF(){ return false; } boolean anyBookBefore1980(){ return false; } boolean anyBookMoreThan10(){ return false; } } The template for the class ConsLoB contains the following: @verbatim{ ... this.first ... ... this.rest ... ... this.rest.anyBookWrittenByMF() ... ... this.rest.anyBookBefore1980() ... ... this.rest.anyBookMoreThan10() ... } However, to complete the bodies of these three methods, we need to add to the class @tt{Book} the methods that determine whether this book was written by @emph{"MF"}, or whether this book was published before @emph{1980}, or whether this book costs more than @emph{$10}. Following the design recipe we end up with: @verbatim{ // In the class Book: // is this book written by "MF" boolean writtenByMF(){ return this.author.equals("MF"); } // was this book published before 1980 boolean before1980(){ return this.year < 1980; } // does this book cost more than $10 boolean moreThan10(){ return this.price > 10; } } The examples are included in the class @tt{ExamplesBooks}. But that means, we can extend the template by the following: @verbatim{ ... this.first.writtenByMF() ... ... this.first.before1980() ... ... this.first.moreThan10() ... } and finish the bodies of the methods in the class @tt{ConsLoS}: @verbatim{ boolean anyBookWrittenByMF(){ return this.first.writtenByMF() || this.rest.anyBookWrittenByMF(); } boolean anyBookBefore1980(){ return this.first.before1980() || this.rest.anyBookBefore1980(); } boolean anyBookMoreThan10(){ return this.first.moreThan10() || this.rest.anyBookMoreThan10(); } } We see, that if we change the names of the three methods to a common name (e.g. @tt{hasProperty}), the only place where they differ is in determining whether @tt{this.first} book satisfies the predicate. We would like to abstract over this part and pass it to the method @tt{hasProperty} as an argument. There are two problems to overcome. The way the question is written in our body requires that the methods @tt{writtenBy}, @tt{before1980}, and @tt{moreThan10} are defined in the class @tt{Book}. But if the methods are to have the same name, they cannot be defined in the same class. Furthermore, we can only pass an instance of a class as an argument to a method. This leads us to consider to have three different classes, each with the method that determines whether a book satisfies the specific predicate. Here is the method purpose statement and the header: @verbatim{ // determine whether the given book satisfies a condition boolean selectBook(Book b){...} } However, to define an argument for our method @tt{hasProperty}, we need to specify its type. The instances of our three classes need to have some unifying type. One option os for all three classes to extend a common superclass. We will see later why that may not be the best option. The other option is for all three classes to implement a common interface. This option makes it clear that we aim to represent commmon behavior. So, the interface is: @verbatim{ interface ISelectBook{ // determine whether the given book satisfies a condition public boolean selectBook(Book b); } } And the three classes will be: @verbatim{ class WrittenByMF implements ISelectBook{ // was the given book written by "MF" public boolean selectBook(Book b){ return b.author.equals("MF"); } } } @verbatim{ class Before1980 implements ISelectBook{ // was the given book published before 1980 public boolean selectBook(Book b){ return b.year < 1980; } } } @verbatim{ class MoreThan10 implements ISelectBook{ // does the given book cost more than $10 public boolean selectBook(Book b){ return b.price > 10; } } } We can now define the method hasProperty as follows: @verbatim{ // In the class ALoB: // is there a book that satisfies the condition? abstract boolean hasProperty(ISelectBook choose); } @verbatim{ // In the class MTLoB: boolean hasProperty(ISelectBook choose){ return false; } } @verbatim{ // In the class ConsLoB: boolean hasProperty(ISelectBook choose){ return choose.selectBook(this.first) || this.rest.hasProperty(choose); } } Finally, we rerun the original tests using the method @tt{hasProperty} with the appropriate arguments. @subsection{Extensions: Parametrized function objects} The methods we defined for selecting books are rather limited. We canonly select one specific author, one specific publication date limit, or one base price. But this is easily remedied by adding a field to the classes that implement the @tt{selectBook} method as follows: @verbatim{ class WrittenBy implements ISelectBook{ String author; WrittenBy(String author){ this.author = author; } boolean selectBook(Book b){ return b.author.equals(this.author); } } } The test cases will become: @verbatim{ // tests for the method anyBookWrittenBy boolean testAnyBookWrittenBy1a = mtlist.hasProperty(new WrittenBy("MF")) == false; boolean testAnyBookWrittenBy2a = list1.hasProperty(new WrittenBy("MF")) == true; boolean testAnyBookWrittenBy3a = list2.hasProperty(new WrittenBy("MF")) == false; } @bold{Exercise:} Design the classes @tt{Before} and @tt{MoreThan} that allow us to select a book published before a specified date, or a book that costs more than a specified price. Make sure you run again the examples using the new function objects. @subsection{DrRacket loops} Let us step back and think about the method we just designed. It determines whether there is an item in a list that satisfies the given predicate. This corresponds to the @emph{DrRacket} loop @tt{ormap}. Here is the description of @tt{ormap} @verbatim{ ;; ormap: (X -> boolean)[Listof X] -> boolean ;; to determine whether p holds for at least one item on alox ;; that is, (ormap p (list x-1 ... x-n)) = (or (p x-1) (or ... (p x-n))) (define (ormap p alox) ... ) } Our @tt{[Listof X]} is the list of books that invokes the method @tt{hasProperty}, the function @tt{(X -> boolean)} is encapsulated in the function objects that implement @tt{ISelectBook} that we pass to our method, and our result is @tt{boolean}, the same as for the @tt{ormap}. @bold{Exercises:} Figure out which @emph{DrRacket} loops correspond to questions @emph{B} and @emph{C}. Design the methods that solve the problems @emph{B1}, @emph{B2}, and @emph{B3}, and abstract them into the corresponding @emph{DrRacket} loop. Do the same for the methods needed to solve the problems @emph{C1}, @emph{C2}, @emph{C3}. In the last part we need to compare two lists for extensional equality. We will learn in the next lecture how to design such tests.