Option
Value option (type Opt) is the second most important Scalqa feature.
Opt either contains a value or is empty, thus it naturally solves the problem of 'null' pointer exception. The large processing library attached to Opt allows to define complex value manipulations without the need to check if value is present, because this is constantly done by the library.
Opt has implementation with zero runtime costs. It creates no object: Opt is an opaque type of the value itself. Moreover, the option methods are fully inlined, so the resulting Java code will simply manipulate value without any mention of option type.
Consider a method which optionally takes a String, and triples it by simple concatenation:
def triple(v: Opt[String]): Opt[String] = v.map(s => s + s + s)
If we do the same without option, we will have to check for null:
def triple(v: String): String = {
var r: String = null
if(v != null)
r = s + s + s
r
}
and here is the first program de-compiled into Java, doing the same thing:
public Object triple(Object v) {
Object o = ZZ.None;
if (v != ZZ.None) {
String v = (String)v;
o = v + v + v;
}
return o;
}
Basicly, Opt greatly reduces boilerplate null checking code with almost no execution cost.
The use of Opt in Scalqa is ubiquitous, there are very few program units without options. Most notably Opt is the key mechanism for reading stream values.
Specialized for Primitives
Regular Opt with its zero cost processing is extremely fast and sufficient for most usage scenarios. But since Scalqa is focused on ultimate performance, each primitive type has a generic option implementation: Byte.G.Opt, Int.G.Opt, Double.G.Opt, etc, where option is an opaque primitive value.
Let's benchmark option processing for regular, specialized and Scala options:
val CNT = 100000
val array: Array[Int] = (0 <> CNT).stream.toArray
J.Benchmark(
("Opt[Int]", () => { var sum=0.Percent; for(i <- 0<>CNT){ val o: Opt[Int] = array(i); o.filter(_ % 2 == 0).map(_.Percent).foreach(sum += _)}; sum}),
("Int.Opt", () => { var sum=0.Percent; for(i <- 0<>CNT){ val o: Int.Opt = array(i); o.filter(_ % 2 == 0).map(_.Percent).foreach(sum += _)}; sum}),
("scala.Option[Int]", () => { var sum=0.Percent; for(i <- 0<>CNT){ val o: Option[Int] = Some(array(i)); o.filter(_ % 2 == 0).map(_.Percent).foreach(sum += _)}; sum}),
)
// Output
Final Result. Total length is about 12 secs
--- ----------------- ------- --- ------ --- -------------
Num Name Ops/Sec % Memory % Last Value
--- ----------------- ------- --- ------ --- -------------
2 Opt[Int] 1.5k 9 1.5mB 14 2.499984464E9
1 Int.Opt 15.5k 100 66B 0 2.499984464E9
3 scala.Option[Int] 276 1 10.7mB 100 2.50005E9
--- ----------------- ------- --- ------ --- -------------
Note.
Unlike streams, where specialized version extends regular, specialized options are not directly related to non-specialized. This is due to their opaque nature limitations. To make consistent experience specialized options can be assigned to generalized (with the help of implicit conversions or explicit method ".ref"). Generalized options have to be explicitly converted to specialized:
var generalOpt: Opt[Int] = VOID
var specialOpt: Int.Opt = VOID
// generalize
generalOpt = specialOpt
// or
generalOpt = specialOpt.ref
// specialize
specialOpt = generalOpt.raw