Data
A data element in Scalqa is an opaque type often based on Java primitive, which is also provided with custom specialized framework of containers for efficient unboxed data processing.
Defining data elements is quite easy, one just needs to select one of the following data roots and follow certain conventions:
- Boolean.Opaque.Data
- Byte.Opaque.Data
- Char.Opaque.Data
- Short.Opaque.Data
- Int.Opaque.Data
- Long.Opaque.Data
- Float.Opaque.Data
- Double.Opaque.Data
- AnyRef.Opaque.Data
- String.Opaque.Data
Define
Let's create an example of data element 'Price', which would be based on a Float, behave like a Float, but be distinct from a Float.
Here is the definition part (available in samples):
type Price = Price.TYPE.DEF
extension (inline x: Double) inline def Dollars : Price = Price(x.toFloat)
object Price extends Float.Opaque.Data.Numerical[Price]("Price"):
inline def apply(inline v: Float): Price = v.toOpaque
override def value_tag(v:Price) : String = "$"+v.roundTo(0.01.Dollars).toString
extension(x: Price)
inline def discount(inline p: Percent): Price = (x.real - p(x.real)).toOpaque
inline def isNotExpensive : Boolean = x < 100
object TYPE:
opaque type DEF <: Float.Opaque = Float.Opaque
Let's explain the above line by line.
type Price = Price.TYPE.DEF
The Price
alias is the public name to be used. This definition can be next to the 'object Price
' or can be in some other more accessible place, like a root package (it is for developers to decide).
extension (inline x: Double) inline def Dollars : Price = Price(x.toFloat)
The extension creates a Double based Price constructor. This is an optional convenience pattern, but it is widely used in Scalqa. Double constructor will cover all primitives.
object Price extends Float.Opaque.Data.Numerical[Price]("Price"):
Object Price extends not only Float.Opaque.Data, which would be sufficient to create a data element. Price extends Float.Opaque.Data.Numerical, which adds Float like behaviour with a set of default methods provided. For example:
var p: Price = 19.99.Dollars
if(p > 10 && p <= 100.0) () // Like Float, Price can be compared to primitives
p = p / 2 * 1.5F + 10 - 2 // Like Float, Price supports arithmetics with primitives
p = 12.0F // FAILS to compile, Price is NOT a Float
All the above operations are inlined and are as efficient, as if they were performed on Float value.
inline def apply(inline v: Float): Price = v.toOpaque
This is a standard constructor, which is inlined and does not have any overhead. v.toOpaque
is a zero cost method call, available on base type within Data definition scope.
override def value_tag(v:Price) : String = "$"+v.roundTo(0.01.Dollars).toString
The value_tag
is the new .toString
. Here we define what the universal .tag
method on type Price will return. If this method is not overridden, the default behavior would convert Float toString.
extension (x: Price)
The extension section is where custom Price methods are defined.
inline def discount(inline p: Percent): Price = (x.real - p(x.real)).toOpaque
Calling method ".real" on a standard opaque value is a zero cost call, which returns base (in this case Float) value.
Use
The Price object already contains specialized collections. To see what they are, look at Float, the 'Containers' section has the same collections (but parameterized for Float)
Here are some usage examples:
val s: Price.Stream = (1 <> 10).stream.map(_.Dollars)
val o: Price.Opt = stream.readRawOpt
val p: Price.Pack = stream.pack
val i: Price.Idx = pack
val c: Price.Collection = pack
Let's find out how efficient those specialized containers are. To do so, we will benchmark them to Scala non-specialized collections:
J.Benchmark(
("Scala ", () => (1 to 1000).iterator.map(v => (v % 200 + 0.99).Dollars).filter(_.isNotExpensive).map(_.discount(5.Percent)).sum),
("Scalqa", () => (1 <> 1000).stream .map(v => (v % 200 + 0.99).Dollars).filter(_.isNotExpensive).map(_.discount(5.Percent)).sum),
("Scalqa Heavy",() => (1 <> 1000).stream .MAP(v => (v % 200 + 0.99).Dollars).FILTER(_.isNotExpensive).MAP(_.discount(5.Percent)).sum),
)
// Output
Final Result. Total length is about 12 secs
--- ------------ ------- --- ------ --- -----------------
Num Name Ops/Sec % Memory % Last Value
--- ------------ ------- --- ------ --- -----------------
1 Scala 32.6k 11 59.1kB 100 23982.74999999997
2 Scalqa 77.1k 27 251B 0 23982.74999999997
3 Scalqa Heavy 279.2k 100 171B 0 23982.74999999997
--- ------------ ------- --- ------ --- -----------------
The conclusion is: boxing in Scala is quite efficient, it is only few times slower. The biggest difference is the memory consumption, which on heavy calculations could slow down the entire JVM.