dragonfly-ai / narr   0.105

GitHub

Scala.js abstraction for a common core of features shared by Array[T], js.Array[T], and the JavaScript TypedArray family of Arrays..

Scala versions: 3.x
Scala.js versions: 1.x
Scala Native versions: 0.4

NArr

pronounced: (ˈnär, as in gnarly) stands for: Native Array

   This library provides Scala.js cross projects with an abstraction over features common to scala.Array, as well as js.Array and the most relevant subset of the JavaScript TypedArray family: Int8Array, Int16Array, Int32Array, Float32Array, Float64Array. It also includes TypeClasses and extension methods to polyfill native JavaScript Arrays with features like: indices, tabulate and fill.  It also provides extensions for a growing subset of ArrayOps methods.  Using NArray[T] instead of Array[T] or js.Array[T] ensures that a project will always use the native Array type of the platform it compiles to.


   Why? Because: "Arrays are really good! As good as you think Arrays are, they are better, uhm, they are just super, super, good!" - Daniel Spiewak

Click here to compare NArr features to those built into Scala JVM/Native and Scala.js.

Advantages:

  • Performance!

       When a Scala.js cross project uses the NArray type internally it implicitly takes advantage of each platform's most optimized data structures. While JVM Arrays inherit the speed and compactness of C/C++ style Arrays, JavaScript Arrays are a special case of JavaScript's Object type: the only data structure it has. Likewise, making use of native Array types eliminates all performance penalties associated with conversions and wrappers at the boundary between a Scala.js library and native code that makes use of it.

  • Native Interoperability without Boilerplate

      When writing a cross compiled Scala.js library with accessibility from native languages, developers have to pay attention to @JSExport annotations and make sure that every method or field of type Array has an analogous js.Array exposed in the JavaScript build. By simply replacing all references to js.Array or Array with NArray, the same methods and fields will interoperate seamlessly with code in either run time environment.

Caveats:
   Although the TypedArray family of data structures avoids the following issues, they pertain to the more ubiquitous js.Array.

  • Type Safety Concerns.

       Because the JVM Array has a reference to the type of its elements and the JavaScript Array has no concept of type at all, careless or malicious JavaScript developers could spike a js.Array with unexpected or dangerous values before passing it to an unsuspecting Scala.js method that takes parameters of type NArray[T]. Security concerns aside, if abused, these type system distinctions could cause unwanted run time differences between the behavior of JVM and JavaScript versions of a program. However, using js.Array[T] explicitly doesn't solve these issues, either. These concerns argue against js.Array in general as much as they argue against NArray.

  • Size Inconsistency and Mutability Concerns.

       The JVM Array has a fixed, immutable length while the length of a JavaScript Array can change dynamically. To mitigate the usual dangers of shared mutable state, users of NArr had better treat all instances of NArray like JVM arrays, ideally by restricting all interaction with instances of NArray[T] to only three operations:

    // assume: val a:NArray[String]
    
    a(0)              // read element
    a(0) = "Gnarly!"  // write element
    a.length          // read length
  • Output from toString() differs by platform. For example:

    println(new NArray[Int](3))

    yields different results depending on the platform on which it runs.

    • Scala.js prints the contents of the array: 0,0,0

    • On the JVM, it prints something like: [I@176c3251

    • In Scala Native, it prints something like: scala.scalanative.runtime.IntArray@24da9104

  • In Scala.js, accessing an index of newly allocated Array yields: undefined and, on the JVM and Native, yields null.

  • Sorting in JavaScript environments.
       Every native JavaScript array type has a builtin sort(compareFn) method where compareFn indicates an optional comparison lambda. For the TypedArray family, default sorting behaves like default sorting in Scala, however, the default behavior of js.Array[T <: AnyRef] sorts its elements alphabetically by their toString() representations. From Scala development perspectives, we almost never want that, so NArr provides extension methods that connect Scala Ordering[T] objects to native JavaScript sorting methods. In general, you can assure consistent sorting behavior across platforms by explicitly providing the desired sort order. The most illuminating example comes from trying to sort NArray[Long]:

    // Sorting a NArray[Long]:
    val sla: NArray[Long] = NArray[Long](555L, 4444L, 9L, 11111L, 88L)
    sla.sort() // default JavaScript sorting calls toString() on each element.
    // yields: NArray[Long](11111, 4444, 555, 88, 9) sorted in alphabetical order.  :(
    sla.sort(Ordering.Long) // Better pass the ordering explicitly!
    // yields: NArray[Long](9, 88, 555, 4444, 11111) sorted in alphabetical order.  :)

When to use NArr:

Array Dependency Convenience Performance Increase Notes
JS JVM Native
No Arrays ☆☆☆ ☆☆☆ ☆☆☆ ☆☆☆ NArr offers no utility for projects that do not use Array[T] types.
js.Array[T] ☆☆☆ ☆☆☆ ☆☆☆ ☆☆☆ js.Array[T] will suffice
Array[T] ☆☆☆ ★☆☆ ☆☆☆ ☆☆☆ Array[T] might not perform as optimally as the native js.Array[T] in JavaScript environments.
js.Array[T] and Array[T] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Int8Array or Array[Byte] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Int16Array or Array[Short] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Int32Array or Array[Int] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Float32Array or Array[Float] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Float64Array or Array[Double] ★★★ ★★★ ☆☆☆ ☆☆☆ Seamless optimized interop with native code on both platforms without any conversions or wrappers.
Other JavaScript Typed Arrays ☆☆☆ ☆☆☆ ☆☆☆ ☆☆☆ Good JVM analogues do not exist. NArr can't help, but what can?

NArr has no impact on JVM or Native performance, but it can dramatically speed up JavaScript by making use of natively optimized data structures and eliminating conversions that tend to have O(n) run time complexities. It also adds convenience methods for js.Array[T] such as fill and tabulate, but mainly eliminates the need for specially crafted and maintained @JSExport methods and fields for JavaScript interop.

To use this library with SBT:

libraryDependencies += "ai.dragonfly" %%% "NArr" % "<LATEST_VERSION>"

How to use NArr:

import NArray

// constructor call
val a1:NArray[String] = new NArray[String](5)  // in JavaScript this resolves to js.Array[String]

// literal
val a2:NArray[Int] = NArray[Int](2, 4, 8, 16)  // in JavaScript this resolves to Int32Array

// fill
val a3:NArray[Double] = NArray.fill[Double](10)(42)  // in JavaScript this resolves to Float64Array

// tabulate
val a4:NArray[Double] = NArray.tabulate[Double](42)(
  (i:Int) => i * Math.random()
)

// multi dimensional
val a2d:NArray[NArray[Double]] = NArray[NArray[Double]]( 
  NArray.tabulate[Double](5)( (i:Int) => i * Math.random() ),
  NArray.tabulate[Double](5)( (i:Int) => i * Math.random() ),
  NArray.tabulate[Double](5)( (i:Int) => i * Math.random() ),
  NArray.tabulate[Double](5)( (i:Int) => i * Math.random() )
) // in JavaScript this resolves to js.Array[Float64Array]

// enjoy Scala semantics
a2d.foreach((d:Double) => println(d))

// conveniently expose Scala.js libraries to native JavaScript and Java developers without any need for wrappers or conversions.
@JSExportTopLevel("copy")
def copy(a0:NArray[Double]): NArray[Double] = NArray.tabulate[Double](a0.length)(
  (i:Int) => a0(i)
)

Compare ArrayOps feature coverage between Scala JVM/Native, Scala.js, and NArr!.


Projects that rely on NArr:

https://github.com/dragonfly-ai/bitfrost

https://github.com/dragonfly-ai/vector

https://github.com/dragonfly-ai/matrix

https://github.com/dragonfly-ai/color

https://github.com/dragonfly-ai/spatial

https://github.com/dragonfly-ai/img