Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Ruby - the power of dynamic programming Niclas Nilsson - http://niclasnilsson.se Copyright 2006 Activa Sweden AB About Niclas Software developer and educator Founder of the consultancy Activa, http://www.activa.se Blogs at http://niclasnilsson.se Who are you? Introduction Focus on the dynamic part of Ruby Java as a (more static) comparison language Objects everywhere # Everything is an object. # There are no 'primitive' types. puts -12.abs puts 42.zero? puts 19.class # # # # output: 12 false Fixnum Objects everywhere # Everything is an object. # There are no 'primitive' types. 3.times do puts 'Hello!' end # # # # output: Hello Hello Hello Exactly everything is an object n = nil n.class n.equal?("foo") n.nil? n.capitalize! # # # # => => => => NilClass false true throws NoMethodError Syntactic sugar puts 1020 puts 100_000_000_000_000 A simple class in two languages // A Person class in Java package se.activa.dynlang; public class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } } public String toString() { return "[" + name + ", " + age + "]"; } class Person attr :name attr :age def initialize(name, age) @name = name @age = age end def to_s "[#{@name}, #{@age}]" end end Containers // List filtering in Java package se.activa.dynlang; import java.util.*; public class FilterList { public static void main(String[] args) { String[] names = { "Adrian Smith", "Robert Johnson", "Eric Clapton", "Steve Vai", "John Scofield", "Eric Johnson", "Steve Cropper" }; List<String> guitarists = Arrays.asList(names); List<String> steves = new ArrayList<String>(); for (Iterator<String> i = guitarists.iterator(); i.hasNext();) { String name = i.next(); if (name.contains("Steve")) { steves.add(name); } } } } System.out.println(steves); # List filtering in Ruby guitarists = ["Adrian Smith", "Robert Johnson", "Eric Clapton", "Steve Vai", "John Scofield", "Eric Johnson", "Steve Cropper"] steves = guitarists.select do |name| name =~ /Steve/ end puts steves Type safety Dynamic languages have no static compile time type checks number = "42"; # Runtime error: sum = number + 3; (Many dynamic language implementations does actually not even have a compile step at all) Type safety in Java package se.activa.dynlang; public class TypeError { public static void main(String[] args) { String number = "42"; } } // Compile error: // int sum = number + 3; Java Collections <= ver 1.4 package se.activa.dynlang; import java.util.*; public class CastException { public static void main(String[] args) { Vector names = new Vector(); names.add("Eric Johnson"); names.add(new Integer(14)); names.add("Steve Cropper"); } } for (Iterator iter=names.iterator(); iter.hasNext();) { String name = (String) iter.next(); name = name + " - guitar player"; } Java >= ver 1.5 package se.activa.dynlang; import java.util.*; public class Generics { public static void main(String[] args) { Vector<String> names = new Vector<String>(); names.add("Eric Johnson"); // Compile error: // names.add(new Integer(14)); names.add("Steve Cropper"); } } for (Iterator<String> iter = names.iterator(); iter.hasNext();) { String name = iter.next(); name = name + " - guitar player"; } A problem? Less of a problem than expected A lot of ClassCastExceptions before JDK 1.5? Quite rare that code gets surprised about content in a collection. No Intellisense / Code Completions in most editors Back to reading the documentation Solution? TDD Test-Driven Development Unit tests and integration tests Tests type usage ...and the code behavior Non dynamic types package se.activa.dynlang; public class Bignum { public static void main(String[] args) { Integer i = 0; System.out.println(i); // 0 System.out.println(i.getClass()); // java.lang.Integer i = 100000000; System.out.println(i); // 100000000 System.out.println(i.getClass()); // java.lang.Integer } } i = i * i; System.out.println(i); // 1874919424 System.out.println(i.getClass()); // java.lang.Integer Dynamic types sum = 0 puts sum puts sum.class # 0 # Fixnum sum = 100_000_000 puts sum # 100000000 puts sum.class # Fixnum sum = sum * sum puts sum puts sum.class # 10000000000000000 # Bignum Passing arguments Is it harder to know argument types? Sometimes it is. The documentation often makes it clear by giving good names and/or descriptions Dynamic typing - arguments Instead of overloading not unusual for a method to accept different types for one argument Example Net::IMAP.copy(set, mailbox) The set parameter is either a number an array of numbers a Range object. Interfaces No 'normal' interfaces Duck typing instead Duck typing is what dynamic typing is called in many languages. 'If it walks like a duck and quacks like a duck, it must be a duck.' Note! Not weak types Simply checked in runtime instead of at compile time // In a language without duck typing interface FlyingObject { public void fly(); } class Duck implements FlyingObject { public void fly() { System.out.println("I'm a duck and I'm flying"); } } class Airplane implements FlyingObject { public void fly() { System.out.println("I'm an airplane and I'm flying"); } } public class NoDuckTyping { public static void main(String[] args) { FlyingObject[] flyers = { new Duck(), new Airplane() }; for (int i = 0; i < flyers.length; i++) { flyers[i].fly(); } } } # Duck typing example class Duck def fly puts "I'm a duck and I'm flying" end end class Airplane def fly puts "I'm an airplane and I'm flying" end end flyers = [ Duck.new(), Airplane.new() ] flyers.each do |flyer| flyer.fly end Duck typing The question is not: Do you confirm to this interface? The question is: Can you handle the messages I send you? Blocks Blocks of code that can be passed to method Common in functional languages Blocks and closures are (more or less) synonyms Blocks can access local variables in the defining method Block-like things in static languages C and C++ has function pointers Java has anonymous inner classes C# 2.0 has closures! (anonymous methods) // Without duck typing: An implementation of the Comparator // interface used for sorting Person objetcs. class PersonComparator implements Comparator<Person> { public int compare(Person person1, Person person2) { return person1.getAge() - person2.getAge(); } } public class SortingPersons { public static void main(String[] args) { Person james = new Person("James", 40); Person karen = new Person("Karen", 45); Person hanna = new Person("Hanna", 18); List<Person> persons = new ArrayList<Person>(); persons.add(james); persons.add(karen); persons.add(hanna); } } PersonComparator pc = new PersonComparator(); Collections.sort(persons, pc); System.out.println(persons); // Without duck typing - alternative solution: // Sort the collection by creating an anonymous inner // class that implements the Comparator interface public class SortingPersonsAnonymous { public static void main(String[] args) { Person james = new Person("James", 40); Person karen = new Person("Karen", 45); Person hanna = new Person("Hanna", 18); List<Person> persons = new ArrayList<Person>(); persons.add(james); persons.add(karen); persons.add(hanna); Collections.sort(persons, new Comparator<Person>() { public int compare(Person person1, Person person2) { return person1.getAge() - person2.getAge(); } }); } } System.out.println(persons); # With block james = Person.new("James", 40) karen = Person.new("Karen", 45) hanna = Person.new("Hanna", 18) persons = [james, karen, hanna] persons.sort! do |person1, person2| person1.age - person2.age end puts persons The closest thing in Java public class PersonPrinter { void print(Person person) { System.out.println(person); } } public class BlockExample { public void printAllSeniors(List<Person> persons, PersonPrinter printer) { Iterator iter = persons.iterator(); while (iter.hasNext()) { Person person = (Person) iter.next(); if (person.getAge() >= 65) { printer.print(person); } } } } public static void main(String[] args) { // Creating and adding persons (removed from example) BlockExample be = new BlockExample(); be.printAllSeniors(persons, new PersonPrinter()); } public class BlockExample2 { public void printAllSeniors(List<Person> persons, PersonPrinter printer) { Iterator iter = persons.iterator(); while (iter.hasNext()) { Person person = (Person) iter.next(); if (person.getAge() >= 65) { printer.print(person); } } } } public static void main(String[] args) { // Creating and adding persons (removed from example) BlockExample be = new BlockExample(); be.printAllSeniors(persons, new PersonPrinter() { void print(Person person) { System.out.println("P: " + person); } }); } Blocks examples require 'Person' def each_senior(persons, &block) persons.each do |person| yield person if person.age >= 65 end end harold = Person.new("Harold", 75) james = Person.new("James", 40) karen = Person.new("Karen", 45) hanna = Person.new("Hanna", 18) ruth = Person.new("Ruth", 65) persons = [harold, james, karen, hanna, ruth] each_senior(persons) do |senior| puts senior end # output: # [Harold, 75] # [Ruth, 65] require 'Person' def each_senior(persons) persons.each do |person| yield person if person.age >= 65 end end harold = Person.new("Harold", 75) james = Person.new("James", 40) karen = Person.new("Karen", 45) hanna = Person.new("Hanna", 18) ruth = Person.new("Ruth", 65) persons = [harold, james, karen, hanna, ruth] each_senior(persons) do |senior| puts senior end # output: # [Harold, 75] # [Ruth, 65] require 'Person' def each_senior(persons) persons.each do |person| yield person if person.age >= 65 end end harold = Person.new("Harold", 75) james = Person.new("James", 40) karen = Person.new("Karen", 45) hanna = Person.new("Hanna", 18) ruth = Person.new("Ruth", 65) persons = [harold, james, karen, hanna, ruth] each_senior(persons) # # # # # output: LocalJumpError: no block given method each_senior in block_example2.rb at line 5 method each_senior in block_example2.rb at line 4 at top level in block_example2.rb at line 21 require 'Person' def each_senior(persons) persons.each do |person| yield person if person.age >= 65 if (block_given?) end end harold = Person.new("Harold", 75) james = Person.new("James", 40) karen = Person.new("Karen", 45) hanna = Person.new("Hanna", 18) ruth = Person.new("Ruth", 65) persons = [harold, james, karen, hanna, ruth] each_senior(persons) do |senior| puts senior end each_senior(persons) # output: # [Harold, 75] # [Ruth, 65] Using blocks for IO # Passing a block to File.open automatically # closes the file when block is done File.open("primes.txt", "r") do |file| file.each_line { |line| puts line } end Closures Closures are blocks that have been bound to local variables Used to defer calculations Used as arguments to higher order functions (functions that creates functions from functions...) # From the Ruby documentation def gen_times(factor) lambda {|n| n*factor } end times3 = gen_times(3) times5 = gen_times(5) puts times3.call(12) puts times5.call(5) puts times3.call(times5.call(4)) #=> 36 #=> 25 #=> 60 Mixins module Logger def log_info(message) puts "INFO: #{Time.now} #{message}" end end class User include Logger # ... end ph = User.new ph.log_info("some message") Extends module Logger def log_info(message) puts "INFO: #{Time.now} #{message}" end end class User end user = User.new # user.log_info("some message") # NoMethodError user.extend(Logger) user.log_info("another message") Reflecton Exists in many static languages, but not all C++ has very limited reflection capabilities C has no reflection capabilities Are you of this type? puts 3.instance_of?(Fixnum) puts 3.instance_of?(Numeric) puts 3.kind_of?(Numeric) # # # # output: true false true Do you respond to this? package se.activa.dynlang; import java.lang.reflect.*; public class RespondTo { public static void main(String[] args) { Object o = "foobar"; } } try { Method m = o.getClass().getMethod("length", null); // Yes, it had a method called length (with 0 args) } catch (NoSuchMethodException e) { // ... } Do you respond to this? str = "foo" puts str.respond_to?("upcase") puts 3.respond_to?("upcase") # output: # true # false Sending messages in static languages 1. Look up method in virtual table 2. Call the method Sending messages in dynamic languages 1. Send a message to an object 2. The object responds (or not) to the message 'Sending messages' package se.activa.dynlang; import java.lang.reflect.*; public class ReflectiveCalls { public static void main(String[] args) { String str = "foobar"; try { Method m = str.getClass().getMethod("length", null); Integer result = (Integer) m.invoke(str, null); // ... } catch (NoSuchMethodException e) { // ... } catch (InvocationTargetException e) { // ... } catch (IllegalAccessException e) { // ... 'Sending messages' package se.activa.dynlang; import java.lang.reflect.*; public class ReflectiveCalls2 { } public static void main(String[] args) { String str = "foobar"; try { Method m = str.getClass().getMethod("length", null); Integer result = (Integer) m.invoke(str, null); // ... } catch (Exception e) { throw new RuntimeException(e); } } Sending messages str = "foobar" puts str.send("size") Method missing class PersonList < Array # ... end persons persons persons persons = PersonList.new << Person.new("John Smith", 75) << Person.new("Andy Taylor", 50) << Person.new("John Smith", 45) puts persons.find_by_name("John Smith") puts persons.find_by_age(50) puts persons.find_by_occupation("programmer") Output: [John Smith, 75] [John Smith, 45] [Andy Taylor, 50] class PersonList < Array def method_missing(method, *args, &block) # parse the method name method_name = method.id2name if method_name =~ /find_by_.*/ property = method_name.sub(/find_by_/, "") end matches = select do |person| if person.respond_to?(property) person.send(property) == args[0] else false end end matches end end Null Object implementation class NullObject def method_missing(method, *args, &block) self end end null_obj = NullObject.new a = null_obj.do_something_very_interesting b = null_obj.zzzzzzz puts "a equals b" if a == b Eval execute a string as code code is data, data is code Eval class Model def self.finder(name) eval %{ def #{name} # ... # ... end } end end class User < Model finder :find_by_name finder :find_by_name_and_password end u = User.new u.find_by_name # f.find_by_address # will not execute Code generation on the fly! In Java BCEL (Byte Code Engineering Library) and CGLIB (Code Generation Library) Operated on binary level (.class-files) In dynamic languages eval Recognized that pattern? class User < Model finder :find_by_name finder :find_by_name_and_password end Person again class Person attr :name attr :age def initialize(name, age) @name = name @age = age end def to_s "[#{@name}, #{@age}]" end end Attributes revised class Foo attr :size, true end foo = Foo.new foo.size = 2 puts "size is #{foo.size}" Attributes explicit class Foo def size @size end def size=(val) @size = val end end foo = Foo.new foo.size = 2 puts "size is #{foo.size}" Rolling you own # Adding behavior to class Object class Object def attr_java_style(name) name = name.id2name eval %{ def get#{name.capitalize} @#{name} end } end end def set#{name.capitalize}(#{name}) @#{name} = #{name} end Rolling you own # Using the new behavious in inherited # from class Object class Person attr_java_style :name attr_java_style :age def initialize(name, age) @name = name @age = age end def to_s "[#{@name}, #{@age}]" end end person = Person.new("James", 42) puts "#{person.getName} is #{person.getAge} years old." No closed classes Add methods can also add methods to objects not only to classes Adding methods to objects foo = "foo" bar = "bar" def foo.capitalized? self[0, 1] == self[0, 1].to_s.upcase end puts foo.capitalized? puts bar.capitalized? # false # NoMethodError Aliasing class Sheep def initialize(name) @name = name end def speak puts "Baaaah!" end end Aliasing # Sometime later, someone wants to log when # a sheep is speaking class Sheep alias_method :speak_original, :speak def speak puts "At #{Time.now} the sheep called #{@name} says" speak_original() end end mary = Sheep.new('Mary') mary.speak() # output: # At Sat Aug 19 19:22:55 CEST 2006 the sheep called Mary says # Baaaah! Changing visibility class ShoppingCart def initialize @total = 0 end # A lots of methods excluded... def total @total - @total * discount_in_percent(@total) end private def discount_in_percent discount = case when @total > 100 : 0.05 when @total > 500 : 0.10 end end end Changing visibility require "test/unit" class ShoppingCart public :discount_in_percent end class TestShoppingCart < Test::Unit::TestCase def test_discount_in_percent cart = ShoppingCart.new # setup things to test # Testing a private method! cart.discount_in_percent end end Questions? Final words That's all folks!