Upgrade to Pro — share decks privately, control downloads, hide ads and more …

From Ruby to Scala and back again: Better living through type checking

From Ruby to Scala and back again: Better living through type checking

Presented at Ruby Manor 4.0 in April 2013. An intro to Scala and static type systems, for Rubyists.

Tim Cowlishaw

April 07, 2013
Tweet

Other Decks in Technology

Transcript

  1. From Ruby to Scala and back again: Better living through

    type-checking Tim Cowlishaw Ruby Manor 4.0
  2. 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
  3. 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?
  4. Ruby! def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13]
  5. Scala def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13] def fibonnaciSequence(n : Int) : List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  6. Scala def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13] def fibonnaciSequence(n : Int) : List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  7. Scala def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13] def fibonnaciSequence(n : Int) : List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  8. Scala def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13] def fibonnaciSequence(n : Int) : List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  9. Scala def fibonacciSequence(n) (0..(n-2)).inject([0,1]) do |fibs, n| nxt = fibs[-1]

    + fibs[-2] fibs + [nxt] end end # fibonacciSequence(7) # # => [0,1,1,2,3,5,8,13] def fibonnaciSequence(n : Int) : List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  10. What is a type? • Property of values: 2 :

    Int, “Hello World” : String • …variables… : x : Int, greeting : String • …functions…: sumIsEven : (Int, Int) → Bool • ...and expressions: sumIsEven(2,3) : Bool
  11. 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]
  12. What is a type? • A set of related operations

    or behaviours: Bool - {!, &&, ||} Int - { +, -, *, div} String – {upcase, format} and: List – {join, map, filter, inject}
  13. A type-system bestiary • Strong vs. weak • Static vs.

    dynamic... – Static vs. dynamic checking – Static vs. dynamic dispatch
  14. A type system bestiary • Strong or weak? – Javascript

    – Ruby: irb(main):001:0> 5 * “2” TypeError: String can't be coerced into Fixnum irb(main):002:0>
  15. A type-system bestiary • Strong or weak? – Javascript –

    Ruby: irb(main):001:0> 5 * “2” TypeError: String can't be coerced into Fixnum irb(main):002:0> 5 + “2”
  16. A type-system bestiary • Strong or weak? – Javascript –

    Ruby: irb(main):001:0> 5 * “2” TypeError: String can't be coerced into Fixnum irb(main):002:0> 5 + “2” TypeError: String can't be coerced into Fixnum
  17. A type-system bestiary • Strong or weak? – Javascript –

    Ruby – Scala: scala> 5 * “2” <console>:8: error: overloaded method value * with alternatives: (x: Double)Double <and> (x: Float)Float <and> (x: Long)Long <and> (x: Int)Int <and> (x: Char)Int <and> (x: Short)Int <and> (x: Byte)Int cannot be applied to (java.lang.String) scala>
  18. A type-system bestiary • Strong or weak? – Javascript –

    Ruby – Scala: scala> 5 * “2” <console>:8: error: overloaded method value * with alternatives: (x: Double)Double <and> (x: Float)Float <and> (x: Long)Long <and> (x: Int)Int <and> (x: Char)Int <and> (x: Short)Int <and> (x: Byte)Int cannot be applied to (java.lang.String) scala> 5 + “2”
  19. A type-system bestiary • Strong or weak? – Javascript –

    Ruby – Scala: scala> 5 * “2” <console>:8: error: overloaded method value * with alternatives: (x: Double)Double <and> (x: Float)Float <and> (x: Long)Long <and> (x: Int)Int <and> (x: Char)Int <and> (x: Short)Int <and> (x: Byte)Int cannot be applied to (java.lang.String) scala> 5 + “2” res8: String = 52
  20. 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)
  21. 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
  22. 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
  23. 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") }
  24. 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
  25. A type-system bestiary • Ruby: Dynamic dispatch module NumberAnnouncer def

    initialize(n) @n = n end def announcement raise NotImplementedError end end
  26. A type-system bestiary • Ruby: Dynamic dispatch class EvenAnnouncer include

    NumberAnnouncer def announcement "#{@n} is even!" end end
  27. A type-system bestiary • Ruby: Dynamic dispatch class OddAnnouncer include

    NumberAnnouncer def announcement "#{@n} is odd!" end end
  28. 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
  29. A type-system bestiary • Scala: Dynamic dispatch! trait NumberAnnouncer {

    def announcement : String } class EvenAnnouncer(n : Int) extends NumberAnnouncer { def announcement : String = n + " is even!" } class OddAnnouncer(n : Int) extends NumberAnnouncer { def announcement : String = n + " is odd!" }
  30. 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) } }
  31. 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) } }
  32. A type-system bestiary • Ruby – Strong typing – Dynamic

    checking – Dynamic dispatch • Scala – Strong typing – Static checking – Dynamic dispatch
  33. Why do we care? • Static type checks – Ensure

    that some classes of errors can't happen at runtime. – Give us tests for free.
  34. 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
  35. Tests for free! class Transaction(val amount : Int) class Account(val

    opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } }
  36. Tests for free! class Transaction(val amount : Int) class Account(val

    opening_balance : Int) { val transactions = new MutableList def add_transaction(transaction : Transaction) : Unit = { transactions += transaction } def balance : Int = { transactions.foldLeft(opening_balance) {(b,t) => b + t.amount} } }
  37. 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
  38. 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
  39. 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
  40. 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) raise “Wrong currency!” unless transaction.currency = @currency @transactions << transaction end def balance @transactions.inject(opening_balance) {|b,t| b + t.amount } end end
  41. 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]
  42. 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]
  43. 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]
  44. 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]
  45. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – – – – – – – – – – – – –
  46. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – scala> x.map {(n) => n * 2 } – Some(12) : Option[Int] – – – – – – – – – – –
  47. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – scala> x.map {(n) => n * 2 } – Some(12) : Option[Int] – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – – – – – – – – –
  48. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – scala> x.map {(n) => n * 2 } – Some(12) : Option[Int] – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – – – – – – –
  49. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – scala> x.map {(n) => n * 2 } – Some(12) : Option[Int] – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – scala> x = None – x : Option[Int] = None – – – – –
  50. Tests for free – Generically • The “Option” type: “type-safe

    nulls” – scala> var x : Option[Int] = Some(6) – x : Option[Int] = Some(6) – scala> x.map {(n) => n * 2 } – Some(12) : Option[Int] – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – scala> x.map {(n) => n * 2 }.getOrElse(14) – 12 : Int – scala> x = None – x : Option[Int] = None – scala> x.map {(n) => n * 2 }.getOrElse(14) – 14 : Int •
  51. Verbosity? • Type Inference! • def fibonnaciSequence(n : Int) :

    List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  52. Verbosity? • Type Inference! • def fibonnaciSequence(n : Int) :

    List[Int] = { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next : Int = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  53. Verbosity? • Type Inference! • def fibonnaciSequence(n : Int) =

    { 0.to(n-2).foldLeft(List(0,1)) { (fibs, n) => val next = fibs(fibs.length-1) + fibs(fibs.length-2) fibs ::: List(next) } } // fibonacciSequence(7) // => List[Int] = List(0,1,1,2,3,5,8,13)
  54. Duck-typing? • Structural types! trait SerializableAsJSON { def toJSON :

    String } def sendToTheInternet(thing : SerializableAsJSON) = { var serializedThing = thing.toJSON internet.send(serializedThing) }
  55. Duck-typing? • Structural types! trait SerializableAsJSON { def toJSON :

    String } def sendToTheInternet(thing : SerializableAsJSON) = { var serializedThing = thing.toJSON internet.send(serializedThing) }
  56. Duck-typing? • Structural types! def sendToTheInternet(thing : {def toJSON :

    String}) = { var serializedThing = thing.toJSON internet.send(serializedThing) }
  57. 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?
  58. 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! –
  59. 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!
  60. This is all moot • ...as Ruby doesn't do static

    type- checking. • :-( • But...
  61. Cool Ruby tools DiamondBack (by Michael Furr) – Optional, static

    type checker for Ruby Rantly (by Howard Yeh) – Quickcheck style fuzz-testing
  62. 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
  63. 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 –
  64. 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)