CSC 517 1 Regular expressions Ruby was invented as “a better Perl,” and hence has excellent built-in support for regular expressions. This is why it is useful as a scripting language. Here’s an example: "faculty".sub(/c/, "") >> "cheese".gsub(/e/, "o") >> There are many facets to regular expressions. A Ruby regular expression is an object, and can be created by a call to new. e = Regexp.new('\d\w*') It can also be specified like this: e = /\d\w*/ The \d and \w are character-class abbreviations. We can, of course, use regular-expression objects in pattern matching: 'a0a0a0'.sub(e, "") Any PCRE (Perl-compatible regular expression) can go between the two slashes. Making substitutions couldn't be easier than this: str = str.gsub(/Steel/,'Titanium') puts str >>Titanium is the toughest material We can constrain matching to occur at the start (^) or end ($) of a string. "cheese".sub(/^e/, "o") >> "cheese" Lecture 6 Object-Oriented Languages and Systems 1 A character class is a set of characters between brackets. Any character within the class matches a character class. "anecdote".sub(/[aeio]/, "u") >> A hyphen may be used to signify a range: "now is the time".sub(/[a-y]/, "z") >> Repetition can be specified by several special characters. "choose".sub(/o+/, "e") >> "choose".sub(/o?/, "e") >> Modules and Mixins Modules in Ruby are a way to group together methods, classes and constants. They are similar to namespaces in languages such as C++. Of course, whenever you mix namespaces, you have the possibility of name clashes. Say you have a graphics library that contains classes for Window and (window.rb) and Border (border.rb).1 Both window.rb and border.rb have a top method, which gives the position of the top of the Window and the top of the Border, respectively. 1 A considerably cornier example can be found in the Module chapter of Programming Ruby. CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 2 CSC 517 3 An application programmer wants to lay out windows with borders, and thus loads both window.rb and border.rb into the program. What’s the problem here? Let’s consider how we might “solve” this. Take a minute, and write down all the possibilities you can think of. Ruby’s answer is the module mechanism. Modules define a namespace. What’s a namespace? The window functions can go into one module module Window def Window.top # .. end def Window.bottom # .. end end and the border functions can go into another module Border def Border.top # ... end def Border.width # .. end end Look at the names of module constants and methods. What do they remind you of? Class constants and methods! Same conventions. Lecture 6 Object-Oriented Languages and Systems 3 When a program needs to use these modules, it can simply load the two files using the Ruby require statement, and reference the qualified names. require 'window' require 'border' trueTop = Window.top + Border.Top Some differences between require and include: include makes features available, but does not execute the code. require loads and executes the code one time (like a C #include). load loads and executes the code every time it is encountered. However, only include will work to create mixins (see below). Mixins The most interesting use of modules is to define mixins. When you include a module within a class, all its functionality becomes available to the class. Not only can modules contain class methods; they can also contain instance methods. Let’s contrast this with #include in (what languages?) How are mixins different? #include files define methods that can be called (like class methods) but not applied to objects (like instance methods). Let’s contrast this with multiple inheritance in (what languages?). How are mixins different? The same methods can be added to any number of different classes, regardless of where they are in the inheritance hierarchy. So, instead of implementing interfaces as in Java, one uses mixins in Ruby when one wants to simulate multiple inheritance. CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 4 CSC 517 5 Consider the following code: module Introspect def kind puts "#{self.class.name}" end end class Animal include Introspect def initialize(name) @name = name end end class Car include Introspect def initialize(model) @model = model end end d = Animal.new("Cat") c = Car.new("Ferrari") d.kind # kind method is available through … c.kind # .. the mixin Introspect >>Animal >>Car Lecture 6 Object-Oriented Languages and Systems 5 Exercise: Take a look at the TeachingAssistant example defined here and answer here. What happens if you remove the different to_s methods? What does this tell you about the way that Ruby resolves conflicts among names inherited from mixins? Comparable A good example of the power of modules is Comparable, from the Ruby library. To use comparable in a class, the class needs to define a method called <=> (sometimes called “rocket”). Once we define this method, we get a lot of useful comparison functions, such as <, >, <=, >=, == and the method between? for free. What is this like in Java? Here is an example. Suppose that we have a Line class: class Line def initialize(x1, y1, x2, y2) @x1, @y1, @x2, @y2 = x1, y1, x2, y2 end end We compare two lines on the basis of their lengths. We add the Comparable mixin as follows: class Line include Comparable def length_squared (@x2-@x1) * (@x2-@x1) + (@y2-@y1) * (@y2-@y1) end CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 6 CSC 517 7 def <=>(other) self.length_squared <=> other.length_squared end end <=> returns 1,0, or –1, depending on whether the first argument is greater than, equal to, or less than the second argument. We delegate the call to <=> of the Fixnum class, which compares the squares of the lengths. Now we can use the Comparable methods on Line objects: l1 = Line.new(1, 0, 4, 3) l2 = Line.new(0, 0, 10, 10) puts l1.length_squared if l1 < l2 puts "Line 1 is shorter than Line 2" else if l1 > l2 puts "Line 1 is longer than Line 2" else puts "Line 1 is just as long as Line 2" end end >>Line 1 is shorter than Line 2 Composing Modules Enumerable (p. 120 of Pickaxe book) is a standard mixin, which can be included in any class. It has a very useful method inject, which can be used to repeatedly apply an operation to adjacent elements in a set: [1, 2, 3, 4, 5].inject {|v, n| v+n } >>15 Lecture 6 Object-Oriented Languages and Systems 7 Many built-in classes include Enumerable, including Array and Range. (‘a’ .. ‘z’).inject {|v, n| v+n } Exercise: Use inject to define a factorial method. Let’s define a VowelFinder class that includes Enumerable. It will have an each method for returning successive vowels from a string. class VowelFinder include Enumerable def initialize(string) @string = string end def each @string.scan(/[aeiou]/) do |vowel| yield vowel end end end Here’s an example of its use: VowelFinder.new("abacadabra").inject {|v, n| v+n} >>aaaaa However, would be nice not to have to type out the “inject” each time, if we just want to work on a different collection. “+” does different things for different classes. When used with numbers, it When used with strings, it CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 8 CSC 517 9 Let’s define a module that encapsulates the call to inject and +. module Reducible def sum_reduce inject {|v, n| v+n} end end class Array include Reducible end class Range include Reducible end class VowelFinder include Reducible end [1, 2, 3, 4, 5].sum_reduce ('a' .. 'z').sum_reduce vf = VowelFinder.new ("The quick brown fox jumped over the lazy dog.") puts vf.sum_reduce Exercise: Define a method that returns a string containing the vowels, so that the above can be done with only one method call. Simulating Multiple Inheritance Modules can be used to simulate multiple inheritance in Ruby. Lecture 6 Object-Oriented Languages and Systems 9 Suppose one wants to add tags to Strings. Then one can define a Taggable module and include it into the class. require 'set' # A collection of unordered values with no duplicates # Include this module to make your class taggable. The names of the # instance variable and the setup method are prefixed with "taggable_" # to reduce the risk of namespace collision. You must call # taggable_setup before you can use any of this module's methods. module Taggable attr_accessor :tags def taggable_setup @tags = Set.new end def add_tag(tag) @tags << tag end def remove_tag(tag) @tags.delete(tag) end end class TaggableString < String include Taggable def initialize(*args) super taggable_setup end end CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 10 CSC 517 s = TaggableString.new('It was the best of times, it was the worst of times.') s.add_tag 'dickens' s.add_tag 'quotation' s.tags # => #<Set: {"dickens", "quotation"}> What happens if we execute puts s ? Exercise: Add a to_s method to the taggable-string example. Then submit it here. Is Multiple Inheritance Good? Multiple inheritance is generally used in one of four ways. • Multiple independent protocols. A class is created by combining completely different superclasses. For example, in Eiffel, the library class WINDOW is a subclass of SCREENMAN, RECTANGLE, and TWO_WAY_TREE. • Mix and match. Several classes are created specially for subsequent combination. Mixins are an example of this. • Submodularity. While creating a situation, modularity of subparts is noticed and factored out. For example, in a class representing mortgages, one might factor out FIXED_RATE and ADJUSTABLE mortgages. • Separation of interface and implementation. An abstract class is created to define the interface. A second class is created to encapsulate the implementation details. E.g., a Stack class could be created as a subclass of StackInterface and StackImplementation. Lecture 6 Object-Oriented Languages and Systems 11 11 Problems posed by multiple inheritance: Multiple inheritance can increase reusability and consistency in a system. However, with its power also comes complexity and ambiguity. The following problems arise: • Name collision. Two features with the same name are inherited from different superclasses. The parent classes may be correct and consistent. But there is a conflict when a descendant class inherits a method with the same name (e.g., initialize) from each. • Repeated inheritance. Because there may be more than one path from a class to its ancestor classes, a class can inherit from the same superclass more than once. We will consider this problem shortly. • Method combination. An object may need to execute methods with the same name defined in different superclasses (e.g., intialize). • Implementation difficulties. Because classes may be combined in various ways, it is difficult to represent objects and find methods without a lot of search or indirection. • Misuse. Because a class has the ability to inherit from as many other classes as necessary, there is a tendency to use inheritance more often than it should be used. Consider declaring an ApplePie class to inherit from Apple and Cinnamon. Why is this not good? ° ° ApplePie should simply be a client of Apple and Cinnamon CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 12 CSC 517 Resolution of naming conflicts: Suppose class C inherits two different methods for copy, from two different superclasses. In Ruby, compound selectors are used (Window.top, etc). Renaming in Eiffel: In Eiffel, no name conflicts are permitted between inherited features. There is a rename clause to remove them: class C inherit A rename x as x1, y as y1; B rename x as x2, y as y2; feature … This inherit clause would be illegal without renaming. Sometimes renaming can be used to give more appropriate names to features. The TREE class defines a routine called insert_subtree. For class WINDOW, this is not an appropriate name; add_subwindow is much better. Renaming allows the better name to be used. Extending Specific Objects There are two common methods for specifying objects: set based or prototype based. Within a set-based language: We describe a class that abstracts what we know to be true of the object we are attempting to specify. We create instances of the object from the class to perform the actual work. Just as classes describe objects, classes are described by metaclasses. Each object instance is unique and holds its own internal values. Lecture 6 Object-Oriented Languages and Systems 13 13 We can create more specific subclasses of a given class that either modify the properties of the parent or add additional properties. When an object receives a message it does not understand, it consults its ancestors, asking each of them to handle the message. Within a prototype-based language: We create an object that is a concrete representation of the object we are attempting to specify, called a prototype. We copy, or clone, the prototype to obtain multiple instances of the object. Each instance holds only what is unique to itself and a reference to its prototype, called an extension. There is no true distinction between an instance and a prototype. Any instance can be cloned, becoming the prototypical object for its duplicates. When an object receives a message it does not understand, it delegates the message to its prototypes, asking each of them to grant it the ability to handle the message. We can quickly see the philosophical differences between sets and prototypes. In a set-based object system, we define objects top down by specifying the abstract and using that specification to create the specific. For example, to describe a Cat object we collect properties that we believe to be common to all Cats and create a Cat class. Then we use that class to create unique Cats. In a prototype object system, we define objects bottom up by specifying a specific instance and modifying it as necessary to represent other instances. For example, to describe a Cat Escher we create an object that contains her properties (female, black fur, annoying). CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 14 CSC 517 If we later discover a Cat Berlioz, we specify what we know about Berlioz (male, orange fur) and assume that the remainder is identical to the prototypical Cat Escher. This system naturally evolves the assumption that all Cats are annoying. Some researchers believe that prototype object systems make better usage of the human ability to generalize from their experience with concrete situations. How does this apply to Ruby? Ruby can act as a set-based language: Using include augments a class definition, and hence adds functionality to all objects of a class. But it can also act as a prototype-based language: There is a way to add functionality to just specific instances of a class, by using the Object#extend method. Let us suppose that there is a mild-mannered Person class. class Person attr_reader :name, :age, :occupation def initialize(name, age, occupation) @name, @age, @occupation = name, age, occupation end def mild_mannered? true end end jimmy = Person.new('Jimmy Olsen', 21, 'cub reporter') clark = Person.new('Clark Kent', 35, 'reporter') jimmy.mild_mannered? # => true clark.mild_mannered? # => true Lecture 6 Object-Oriented Languages and Systems 15 15 But it happens that some Person objects are not as mild-mannered as they might appear. Some of them have super powers. module SuperPowers def fly 'Flying!' end def leap(what) "Leaping #{what} in a single bound!" end def mild_mannered? false end def superhero_name 'Superman' end end If we use include to mix the SuperPowers module into the Person class, it will give every person super powers. Some people are bound to misuse such power. Instead, we'll use extend to give super powers only to certain people: clark.extend(SuperPowers) puts clark.superhero_name puts clark.fly puts clark.mild_mannered? puts jimmy.mild_mannered? >> >> >> >> CSC/ECE 517 Lecture Notes © 2010 Edward F. Gehringer 16