Ruby, Scala and Me ● ~7 years working (mostly) with Ruby ● Increasingly using Scala whenever I can ● Why? – Ruby-like syntax and lack of ceremony – On the JVM – Best bits of Functional and OO paradigms – Powerful statically checked type system
What I'll cover ● Introduction to Scala ● Types and Type systems How do we develop differently in Ruby and Scala? ● What are the pros and cons of each? ● What are the advantages and disadvantages of types and tests for proving correctness of our programs? ● How can types make us better at Ruby?
What is a type? ● A set of related values: Bool = {True, False} Int = {… -2, -1, 0, 1, 2 …} Char = {'a'..'z', 'A'..'Z' etc} List[X] = Empty or X + List[X] String = List[Char]
What is a type? ● A set of related operations or behaviours: Bool - {!, &&, ||} Int - { +, -, *, div} String – {upcase, format} and: List – {join, map, filter, inject}
A type-system bestiary ● Strong or weak? – Presence of implicit coercion – A scale rather than a binary choice Javascript: Weak Ruby: Strong Scala: Strong(-ish)
A type-system bestiary ● Static vs. dynamic type-checking – When do type-checks happen? ● Runtime (Dynamic) – Blows up in your user's face ● Compile time (Static) – Blows up in your own face – Much like your test suite
A type-system bestiary ● Ruby: Dynamic type-checking def square(x) x * x end square("5") $ ruby square.rb TypeError: can't convert String into Integer * at org/jruby/RubyString.java:1135 square at square.rb:2 (root) at square.rb:4
A type-system bestiary ● Scala: Static type-checking $ scalac test.scala test.scala:3: error: type mismatch; found : java.lang.String("5") required: Int square("5") ^ one error found object Example extends App { def square(x : Int) : Int = x * x square("5") }
A type-system bestiary ● Dispatch: when is the implementation of a method resolved? – Static: At compile type – Dynamic: At runtime ● “Dynamic” or “Late” binding ● Enables polymorphism
A type-system bestiary ● Ruby: Dynamic dispatch while line = gets n = line.to_i announcer = if n % 2 == 0 EvenAnnouncer.new(n) else OddAnnouncer.new(n) end puts announcer.announcement end
A type-system bestiary Scala: Dynamic dispatch! import io.Source object NumberWang extends App { val input = Source.fromInputStream(System.in).getLines for(line <- input) { val n = line.toInt val announcer : _ <: NumberAnnouncer = n % 2 == 0 match { case true => new EvenAnnouncer(n) case false => new OddAnnouncer(n) } println(announcer.announcement) } }
A type-system bestiary Scala: Dynamic dispatch! import io.Source object NumberWang extends App { val input = Source.fromInputStream(System.in).getLines for(line <- input) { val n = line.toInt val announcer : _ <: NumberAnnouncer = n % 2 == 0 match { case true => new EvenAnnouncer(n) case false => new OddAnnouncer(n) } println(announcer.announcement) } }
Tests for free! class Transaction < Struct(:amount); end class BankAccount def initialize(opening_balance) @opening_balance = opening_balance @transactions = [] end def add_transaction(transaction) @transactions << transaction end def balance @transactions.inject(opening_balance) {|b,t| b + t.amount } end end
Tests for free! class Transaction < Struct(:amount); end class BankAccount def initialize(opening_balance) @opening_balance = opening_balance @transactions = [] end def add_transaction(transaction) @transactions << transaction end def balance @transactions.inject(opening_balance) {|b,t| b + t.amount } end end
Tests for free! class Transaction < Struct(:amount, :currency); end class BankAccount def initialize(opening_balance) @opening_balance = opening_balance @transactions = [] end def add_transaction(transaction) @transactions << transaction end def balance @transactions.inject(opening_balance) {|b,t| b + t.amount } end end
Tests for free! class Transaction < Struct(:amount, :currency); end class BankAccount def initialize(opening_balance, currency) @opening_balance = opening_balance @currency = currency @transactions = [] end def add_transaction(transaction) @transactions << transaction end def balance @transactions.inject(opening_balance) {|b,t| b + t.amount } end end
Tests for free! object Currencies { type GBP type USD } class Transaction[C](val amount : Int) class Account[C](val opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction[C]) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } } val account = new Account[Currencies.USD]
Tests for free! object Currencies { type GBP type USD } class Transaction[C](val amount : Int) class Account[C](val opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction[C]) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } } val account = new Account[Currencies.USD]
Tests for free! object Currencies { type GBP type USD } class Transaction[C](val amount : Int) class Account[C](val opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction[C]) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } } val account = new Account[Currencies.USD]
Tests for free! object Currencies { type GBP type USD } class Transaction[C](val amount : Int) class Account[C](val opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction[C]) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } } val account = new Account[Currencies.USD]
But why? ● Types do the same job as tests – Tests are statements in logic – Types are also statements in logic (Curry- Howard correspondence, 1934) – So why choose one over the other?
Type vs. Tests ● Tests are necessarily existentially qualified logical statements: – “For this specific input, we observe this output” ● Types are universally qualified logical statements: – “For all possible inputs, we observe this (type of) output” (modulo non-termination) – Types can make assertions about the call -site of your methods! –
Type vs. Tests ● However... – Types don't encode our test criteria Can drive design in the small – (But not in the large) – Can document contracts and interfaces – (But not the overall behaviour of a system) The type system complements, rather than replaces your test suite!
Cool Ruby tools DiamondBack (by Michael Furr) – Optional, static type checker for Ruby Rantly (By Howard Yeh) – Quickcheck style fuzz-testing describe "Squaring a number" do context "For all integer inputs" do it "returns a positive integer output" do Rantly { (integer ** 2).should be_greater_than_or_equal_to(0) } end end end
Cool Ruby tools Ourselves! – Encapsulate everything, well-defined interfaces between components – Think carefully about how well tests cover possible inputs and states – Fail fast with clear error messages –
Encapsulation class Address < Struct.new(:line_1, :line_2, :postcode); end class LatLongService Class << self def lat_long_for_postcode(postcode) end end end LatLongService.lat_long_for_postcode(address.postcode)
Encapsulation class Address < Struct.new(:line_1, :line_2, :postcode) def lat_long LatLongService.lat_long_for_postcode(postcode) end end address.lat_long