Scala for Java Developers - Intro

Post on 14-Jun-2015

4.338 views 3 download

description

Slides of a talk I gave at work on Scala. It is geared toward Java developers. Some of the examples are in my company's domain, which is analyzing energy usage (i.e. a "read" is an electric meter read).

Transcript of Scala for Java Developers - Intro

ScalaWTF is it? The new hotness or yet another functional fad?

1

Scalable LanguageThat’s why it’s pronounced Ska-Lah ?!??!

2

Statically Typed

3

Runs on the JVM

4

Hybrid Language:Object-Oriented + Functional

5

Why?

6

Make Java more DRY

7

Allow easy creation of DSLs

8

Conciseness of Ruby +Safety of Static Typing

9

No meta-programming but...very liberal syntax

10

15

Type Inference

17

List<String> oyVey = new ArrayList<String>();

18

List<String> oyVey = new ArrayList<String>(); var hellsYeah = new ArrayList[String];

19

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Int

20

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Intval doh = booyah / 3; // not a Float!!

21

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Intval doh = booyah / 3; // not a Float!!val better:Float = booyah / 3;

22

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Intval doh = booyah / 3; // not a Float!!val better:Float = booyah / 3;

// return type is java.lang.Stringdef fullName = { first + “,” + last; }

23

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Intval doh = booyah / 3; // not a Float!!val better:Float = booyah / 3;

// return type is java.lang.Stringdef fullName = { first + “,” + last; }

If the type seems obvious to you...

24

List<String> oyVey = new ArrayList<String>();var hellsYeah = new ArrayList[String]; val booyah = 45; // it’s an Intval doh = booyah / 3; // not a Float!!val better:Float = booyah / 3;

// return type is java.lang.Stringdef fullName = { first + “,” + last; }

If the type seems obvious to you...Scala can usually figure it out, too

25

Liberal Syntax

26

val semicolons = “don’t need ‘em”val but = “if it’s not clear”; val you = “need them”

27

val semicolons = “don’t need ‘em”val but = “if it’s not clear”; val you = “need them”

def braces(b:Boolean) = if (b) “needed for multiple expressions” else “you don’t need them”

28

val semicolons = “don’t need ‘em”val but = “if it’s not clear”; val you = “need them”

def braces(b:Boolean) = if (b) “needed for multiple expressions” else “you don’t need them”

• if/else is a singular expression

29

class Read(d:Date, val usage:Int) { def +(other:Int) = new Read(d,usage + other) override def toString = d + “:” + u

}

30

class Read(d:Date, val usage:Int) { def +(other:Int) = new Read(d,usage + other) override def toString = d + “:” + u

}

val f = new Read(new Date,10)val g = f + 20

31

class Read(d:Date, val usage:Int) { def +(other:Int) = new Read(d,usage + other) override def toString = d + “:” + u

}

val f = new Read(new Date,10)val g = f + 20val gee = f.+(20) // yuck, but legal

32

class Read(d:Date, val usage:Int) { def +(other:Int) = new Read(d,usage + other) override def toString = d + “:” + u def later(ms:Int) = new Read(d + ms,usage)}

val f = new Read(new Date,10)val g = f + 20val gee = f.+(20) // yuck, but legalval l = f later (3600 * 1000)

33

Mixins (Traits)

34

abstract class SaneCompare[T] {  def <(other:T):Boolean  def >(other:T) = !(this < other)  def <=(other:T) = this == other || this < other  def >=(other:T) = this == other || this > other}class Read(d:Date,u:Int) extends SaneCompare[Read] {  val usage = u    def <(other:Read) = usage < other.usage }if (someRead >= someOtherRead) println(”finally!”)

35

abstract class SaneCompare[T] {  def <(other:T):Boolean  def >(other:T) = !(this < other)  def <=(other:T) = this == other || this < other  def >=(other:T) = this == other || this > other}class Read(d:Date,u:Int) extends AbstractEntity {  val usage = u    def <(other:Read) = usage < other.usage }// hmmm....what now?

36

abstract class SaneCompare[T] {  def <(other:T):Boolean  def >(other:T) = !(this < other)  def <=(other:T) = this == other || this < other  def >=(other:T) = this == other || this > other}class Read(d:Date,u:Int) extends AbstractEntity {  val usage = u    def <(other:Read) = usage < other.usage }// AbstractEntity extends SaneCompare?// God class above AbstractEntity subsumes it?

37

abstract class SaneCompare[T] {  def <(other:T):Boolean  def >(other:T) = !(this < other)  def <=(other:T) = this == other || this < other  def >=(other:T) = this == other || this > other}class Read(d:Date,u:Int) extends AbstractEntity {  val usage = u    def <(other:Read) = usage < other.usage }// AbstractEntity extends SaneCompare?// God class above AbstractEntity subsumes it?// these are different concepts entirely

38

trait SaneCompare[T] {  def <(other:T):Boolean  def >(other:T) = !(this < other)  def <=(other:T) = this == other || this < other  def >=(other:T) = this == other || this > other}class Read(d:Date,u:Int) extends AbstractEntity with SaneCompare[Read] {  val usage = u    def <(other:Read) = usage < other.usage }// now we have both!

39

Traits

•Separate Concerns

40

Traits

•Separate Concerns

•Precedence is based on declaration order

41

Traits

•Separate Concerns

•Precedence is based on declaration order

•All abstract – just like Java interface

•None abstract – multiple inheritance

42

Functions

43

val reads = getSomeElectricReads // List[Read]

reads.sort( (a,b) => a.date.compareTo(b.date) < 0 )

44

val reads = getSomeElectricReads // List[Read]

reads.sort( (a,b) => a.date.compareTo(b.date) < 0 )

45

val reads = getSomeElectricReads // List[Read]

reads.sort( (a,b) => a.date.compareTo(b.date) < 0 )

def sort(compFunc:(Read,Read) => Boolean)

46

val reads = getSomeElectricReads // List[Read]

reads.sort( (a,b) => a.date.compareTo(b.date) < 0 )

def sort(compFunc:(Read,Read) => Boolean)

Function2[Read,Read,Boolean]

47

val reads = getSomeElectricReads // List[Read]

reads.sort( (a,b) => a.date.compareTo(b.date) < 0 )

def sort(compFunc:(Read,Read) => Boolean)

Function2[Read,Read,Boolean]

def sortReads(a:Read,b:Read) = a.date.compareTo(b.date) < 0

reads.sort(sortReads) // could also use a method // just keep in mind...

48

Methods are not Functions

49

Methods are not Functionsbut can be passed as Functions

50

List Processing

51

class State(val code:String,val desc:String)val states = getAllStates

// returns a List[String] with the codesstates.map( (state) => state.code )

// returns true if any state has a code of “DC”states.exists( (state) => state.code == “DC” )

// returns the state with the desc of “Hawaii”states.find( (state) => state.desc == “Hawaii” )

// returns a List[State] if states with descs matchingstates.filter( (state) => state.desc.startsWith(”V”) )

// Tons more

52

Complete Access to JDK and Java libraries

53

val s = new SingletonMetadataAwareAspectInstanceFactory()val foo = s.getOrderForAspectClass(classOf[FooBar])

54

import java.util.Observerimport java.util.Observable

class AwesomeObserver extends Observer { def update(o:Observable, arg:Any) = if (o hasChanged) println(arg.asInstanceOf[MeterRead].date)}

55

import java.util.Observerimport java.util.Observable

class AwesomeObserver extends Observer { def update(o:Observable, arg:Any) = if (o hasChanged) println(arg.asInstanceOf[MeterRead].date)}

56

import java.util.Observerimport java.util.Observable

class AwesomeObserver extends Observer { def update(o:Observable, arg:Any) = if (o hasChanged) println(arg.asInstanceOf[MeterRead].date)}

57

import java.util.Observerimport java.util.Observable

class AwesomeObserver extends Observer { def update(o:Observable, arg:Any) = if (o hasChanged) println(arg.asInstanceOf[MeterRead].date)}

58

Goodbye Java’s Baggage

59

No primitives

60

Proper F’ing Propertiesclass ServicePoint(val id:String,var name:String)

val sp = new ServicePoint(”foo”,”The Foo House”)println(sp.id) // get, but no setprintln(sp.name)sp.name = “Thy Foo Haüs”

61

Proper F’ing Propertiesclass ServicePoint(val id:String,private var _name:String) { def name = _name.toUpperCase def name_=(newName:String) = _name = newName}

val sp = new ServicePoint(”foo”,”The Foo House”)sp.name = “Thy Foo Haüs”println(sp.name) // prints THY FOO HAÜS

62

¡Adiós Checked Exceptions!def readFile(f:File) = { val is = new FileInputStream(f) var ch = f.read while (ch != -1) { print(ch) ch = f.read }} // Wow, that was clean!

63

¡Adiós Checked Exceptions!def readFile(f:File) = { try { val is = new FileInputStream(f) var ch = f.read() while (ch != -1) { print(ch) ch = f.read } } catch { case fnfe:FileNotFoundException => println(f + ” not found, dude: ” + fnfe) } // All others bubble out, even if checked in Java}

64

Can I get a closure?class Logger(level:Int) { def debug(message: => String) = log(20,message) def info(message: => String) = log(10,message) def log(logLevel:Int, message: => String) = { if (level >= logLevel) println(message) }}val log = new Logger(10)log.debug(“Got read for “ + read.date + “ with usage “ + read.usage)read.usage = 44log.info(if read.usage < 10 “low read” else “high read”)

65

Can I get a closure?class Logger(level:Int) { def debug(message: => String) = log(20,message) def info(message: => String) = log(10,message) def log(logLevel:Int, message: => String) = { if (level >= logLevel) println(message) }}val log = new Logger(10)log.debug(“Got read for “ + read.date + “ with usage “ + read.usage)read.usage = 44log.info(if read.usage < 10 “low read” else “high read”)

66

Can I get a closure?class Logger(level:Int) { def debug(message: => String) = log(20,message) def info(message: => String) = log(10,message) def log(logLevel:Int, message: => String) = { if (level >= logLevel) println(message) }}val log = new Logger(10)log.debug(“Got read for “ + read.date + “ with usage “ + read.usage)read.usage = 44log.info(if read.usage < 10 “low read” else “high read”)

67

Can I get a closure?class Logger(level:Int) { def debug(message: => String) = log(20,message) def info(message: => String) = log(10,message) def log(logLevel:Int, message: => String) = { if (level >= logLevel) println(message) }}val log = new Logger(10)log.debug(“Got read for “ + read.date + “ with usage “ + read.usage)read.usage = 44log.info(if read.usage < 10 “low read” else “high read”)

68

Can I get a closure?class Logger(level:Int) { def debug(message: => String) = log(20,message) def info(message: => String) = log(10,message) def log(logLevel:Int, message: => String) = { if (level >= logLevel) println(message) }}val log = new Logger(10)log.debug(“Got read for “ + read.date + “ with usage “ + read.usage)read.usage = 44log.info(if read.usage < 10 “low read” else “high read”)

69

“Literals”val triStateArea = List(”MD”,”DC”,”VA”)val theSouth = Map(”MD” -> true,”DC” -> false, ”VA” ->true)val perlCirca96 = (true,”Tuples rule”)val (hasTuples,message) = perlCirca96

70

“Literals”val triStateArea = List(”MD”,”DC”,”VA”)val theSouth = Map(”MD” -> true,”DC” -> false, ”VA” ->true)val perlCirca96 = (true,”Tuples rule”)val (hasTuples,message) = perlCirca96

These are actually API calls

71

“Literals”val triStateArea = List(”MD”,”DC”,”VA”)val theSouth = Map(”MD” -> true,”DC” -> false, ”VA” ->true)val perlCirca96 = (true,”Tuples rule”)val (hasTuples,message) = perlCirca96

This is done by the compilercreates a Tuple2[Boolean, String]

72

“Literals”class Read(val id:Int, val usage:Int, val age:Int)

object Read { def apply(id:Int,usage:Int,age:Int) = new Read(id,usage,age)}

val read = Read(4,10,33)

73

“Literals”class Read(val id:Int, val usage:Int, val age:Int)

object Read { def apply(id:Int,usage:Int,age:Int) = new Read(id,usage,age)}

val read = Read(4,10,33) // shortcut via compilerval read2 = Read.apply(4,10,33)

74

Crazy Awesome - Pattern Matchingdef fromEnglish(string:String) = string match { case “none” => 0 case “one” => 1 case _ => 2}

75

Crazy Awesome - Pattern Matchingdef toUML(obj:Any) = obj match { case 0 => “0” case 1 => “0..1” case n:Int => “0..” + n case true => “1” case false => “0” case “many” => “0..*” case _ => “0..*”}

76

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

77

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

• properties • equals/toString/hashCode• “extractor” • no need for “new”

78

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

79

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

80

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

81

Crazy Awesome - Pattern Matchingsealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

def proRate(read:Read) = read match { case AMIRead(d,usage,duration) => usage / duration case BillingRead(d,usage,days,c) => usage / days case CorrectedRead(BillingRead(d,oldUsage,days,c),usage) => (oldUsage + usage) / days}

82

Crazy Awesome - Implicitssealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

implicit def readToSeconds(r:Read):Int = r.date.getTime / 1000

def areConsecutive(from:Read, to:Read) = (from - to) <= from.duration

83

Crazy Awesome - Implicitssealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

implicit def readToSeconds(r:Read):Int = r.date.getTime / 1000

def areConsecutive(from:Read, to:Read) = (from - to) <= from.duration

Have a Read, but need an Int

84

Crazy Awesome - Implicitssealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

implicit def readToSeconds(r:Read):Int = r.date.getTime / 1000

def areConsecutive(from:Read, to:Read) = (from - to) > from.duration

Needs a Read and gives an Int

85

Crazy Awesome - Implicitssealed abstract class Readcase class AMIRead(date:Date,usage:Int,duration:Int) extends Readcase class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Readcase class CorrectedRead(read:BillingRead, usage:Int) extends Read

implicit def readToSeconds(r:Read):Int = r.date.getTime / 1000

def areConsecutive(from:Read, to:Read) = (from - to) > from.duration

• Given this and matching, casting is rarely needed

86

Crazy Awesome - XML Literalsval xml = <html> <head> <title>Scala Pronunciation Guide</title> </head> <body> <h1>How to Pronounce It</h1> </body></html>

println(xml)

87

Crazy Awesome - XML Literalsval lang = getLangval title = translate(”scala.title”,lang)val xml = <html lang={lang}> <head> <title>{title}</title> </head> <body> <h1>{title.toUpperCase}</h1> </body></html>

println(xml)

88

Crazy Awesome - XML Literalsval lang = getLangval title = translate(”scala.title”,lang)val xml = <html lang={lang}> <head> <title>{title}</title> </head> <body> <h1>{title.toUpperCase}</h1> </body></html>

println(xml)

89

Crazy Awesome - XML Literalsval states = List(”DC”,”MD”,”VA”)val xml = <html> <body> <h1>States</h1> <ul> { states.map( (state) => <li>{state}</li> ) } </ul> </body></html>

println(xml)

90

Concurrency

91

Message-passing

92

Message-passingimmutable objects

93

Message PassingImmutable objects

“actors” with “mailboxes”

94

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

95

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

96

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

97

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

98

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

99

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

100

case class Accumulate(amount: Int)case class Resetcase class Total

object Accumulator extends Actor { def act = { var sum = 0 loop { react { case Accumulate(n) => sum += n case Reset => sum = 0 case Total => reply(sum); exit } } }}

object Accumulators extends Application { Accumulator.start for(i <- (1 to 100)) { Accumulator ! Accumulate(i) } Accumulator !? Total match { case result: Int => println(result) }}

101

There’s a lot more to it

102

Demo

103

DemoFill Gaps in Electric Meter Reads

104

6/15 kwh

6/27 kwh

6/43 kwh

6/55 kwh

6/88 kwh

105

6/15 kwh

6/27 kwh

6/43 kwh

6/55 kwh

6/88 kwh

6/3??

6/6??

6/7??

106

for all consecutive reads r1 and r2 if r2 - r1 > one day fill gaps for (r1,r2)

107

fill: for all reads (first,second,List(rest)) if gap(first,second) fill_gap(first,second) + fill(second + rest) else first + fill(second + rest)

108

fill: for all reads (first,second,List(rest)) if !first || !second reads else if gap(first,second) fill_gap(first,second) + fill(second + rest) else first + fill(second + rest)

109

def fillReads( strategy: (MeterRead,MeterRead) => Seq[MeterRead], reads:List[MeterRead]):List[MeterRead] =

reads match { case List() => List() case first :: List() => List(first) case first :: second :: rest if gap(first, second) => first :: strategy(x,y).toList ::: fillReads(strategy, second :: rest) case first :: rest => first :: fillReads(strategy,rest) }

110

def fillReads( strategy: (MeterRead,MeterRead) => Seq[MeterRead], reads:List[MeterRead]):List[MeterRead] =

reads match { case List() => List() case first :: List() => List(first) case first :: second :: rest if gap(first, second) => first :: strategy(x,y).toList ::: fillReads(strategy, second :: rest) case first :: rest => first :: fillReads(strategy,rest) }

111

def fillReads( strategy: (MeterRead,MeterRead) => Seq[MeterRead], reads:List[MeterRead]):List[MeterRead] =

reads match { case List() => List() case first :: List() => List(first) case first :: second :: rest if gap(first, second) => first :: strategy(x,y).toList ::: fillReads(strategy, second :: rest) case first :: rest => first :: fillReads(strategy,rest) }

112

(demo with code)

113

It’s all happy flowers and meadows?

114

It’s all happy flowers and meadows?not quite; a few stumbling blocks

115

Generics, type-variance, etc. can get really confusing at times

116

Library docs not as extensive as Java’s

117

Still have to compile

118

Symbol Soup sometimes

119

Symbol Soup sometimes...but it gets easier

120

Static typing sometimes paints you into a (very dark and confusing) corner

121

sometimes you just need method_missing

122

sometimes you just needmonkey patching

123

Where is Scala now?

124

scala-lang.org

125

mailing lists, irc, a few blogs

126

One good book, a few others

127

Online docs improving, but scattered

128

Spec is surprisingly readable

129

Java is on the decline

130

Java is on the declineand Ruby and Python aren’t the

only options

131

questions?

132