Scala for Java Developers - Intro - Presentation Transcript
Scala
WTF is it? The new hotness or yet another functional fad?
1
Scalable Language
That’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
11
12
13
14
15
16
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 Int
val doh = booyah / 3; // not a Float!!
21
List<String> oyVey = new ArrayList<String>();
var hellsYeah = new ArrayList[String];
val booyah = 45; // it’s an Int
val 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 Int
val doh = booyah / 3; // not a Float!!
val better:Float = booyah / 3;
// return type is java.lang.String
def fullName = { first + “,” + last; }
23
List<String> oyVey = new ArrayList<String>();
var hellsYeah = new ArrayList[String];
val booyah = 45; // it’s an Int
val doh = booyah / 3; // not a Float!!
val better:Float = booyah / 3;
// return type is java.lang.String
def 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 Int
val doh = booyah / 3; // not a Float!!
val better:Float = booyah / 3;
// return type is java.lang.String
def 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 + 20
val 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 + 20
val gee = f.+(20) // yuck, but legal
val 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
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 Functions
but 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 codes
states.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 matching
states.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.Observer
import 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.Observer
import 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.Observer
import 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.Observer
import 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 Properties
class ServicePoint(val id:String,var name:String)
val sp = new ServicePoint(”foo”,”The Foo House”)
println(sp.id) // get, but no set
println(sp.name)
sp.name = “Thy Foo Haüs”
61
Proper F’ing Properties
class 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 = 44
log.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 = 44
log.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 = 44
log.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 = 44
log.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 = 44
log.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 compiler
creates 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 compiler
val read2 = Read.apply(4,10,33)
74
Crazy Awesome - Pattern Matching
def fromEnglish(string:String) = string match {
case “none” => 0
case “one” => 1
case _ => 2
}
75
Crazy Awesome - Pattern Matching
def 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Matching
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 - Implicits
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 - Implicits
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 - Implicits
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 - Implicits
sealed abstract class Read
case class AMIRead(date:Date,usage:Int,duration:Int) extends Read
case class BillingRead(toDate:Date,usage:Int,numDays:Int,charges:Int) extends Read
case 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 Literals
val xml = <html>
<head>
<title>Scala Pronunciation Guide</title>
</head>
<body>
<h1>How to Pronounce It</h1>
</body>
</html>
println(xml)
87
Crazy Awesome - XML Literals
val lang = getLang
val 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 Literals
val lang = getLang
val 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 Literals
val 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-passing
immutable objects
93
Message Passing
Immutable objects
“actors” with “mailboxes”
94
case class Accumulate(amount: Int)
case class Reset
case 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 Reset
case 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 Reset
case 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 Reset
case 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 Reset
case 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 Reset
case 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 Reset
case 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
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 need
monkey 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 decline
and Ruby and Python aren’t the
only options
131
Slides of a talk I gave at work on Scala. It is ge more
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). less
0 comments
Post a comment