here's why I like scala.js
Here’s why I like scala.js.
I write various types of frontends for some of my the AI/machine learning/bigdata/data wrangling/* work.
Typescript is the dominant language for writing frontends although vanilla javascript is still huge. Typescript continues to make great strides in its typing system and adapting to the javascript way of doing things. Unfortunately, it will probably never be quite right as many typings still resolve to any
and hence, I lose the benefits of typing fairly quickly.
Also, to reduce errors in my code, I like to use functional concepts e.g. effects as values. I also like immutable data. There are immutable libraries available of course, but that’s yet another js-alien addition. Effects in typescript are an evolving story. If I want to curry a function, that’s another library. Ouch!
Awhile ago, I wrote a reactjs facade, scalajs-reaction, that I continue to use in production work. It uses the spirit of ReasonML’s react facade to create a scalajs facade–namely, function components exclusively and nothing but hooks. It also uses a super simple, scalajs based, representation since I must interact heavily with existing js libraries and UI frameworks. For example, a component is a simple scalajs js-function. This approach simplifies interop and usage. It also allows you to control rendering performance more easily. Writing UIs and facades for external js libraries is a simple and fast exercise.
But here’s why I think scala and scala.js is a good choice for js transpilation. Of course, it leverages existing scala skills, but there are other reasons.
The example I’ll mention and describe is a hook for browser local storage. There are several local storage hooks available. However, some of them treat null and undefined rather carelessly and you have to be clever with implicits or checks when using them in scala.js. Also, the semantics are wrong.
Local storage is useful for “stringable” data but the needed “commands” are more than just “get” and “put.” Sometimes we want to replace the value only if its different (so no event listeners are fired) or unset the value–in js land that can be null but then undefined is actually a value. Local storage defines a “removeItem” function but most hooks rely on null/undefined to imply a “removal” (sometimes).
What if you want to store null or undefined :-)!?!? And if I want to store a straight scala object vs a non-native, JS trait, I need to be able to stringify the object. All of the hooks use JSON.stringify
which won’t work on pure scala objects. I could always convert to JSON using a scala JSON library, but those can be quite heavy in the browser and there is already a good JSON parser/stringifier that is browser native.
Local storage is not the only game in town. Sometimes you also want to use another storage system, say session storage or indexed storage. The semantics for these storage backends are not always the same as local storage and you do not want confusion around null and undefined. In fact, you could have a “local storage” that uses remote storage :-).
In scala we can define our storage commands:
sealed trait StoredValue[+T] extends Product with Serializable
case class Replace[+T](value: T) extends StoredValue[T]
case class ReplaceIfDifferent[+T](value: T) extends StoredValue[T]
case object Keep extends StoredValue[Nothing]
case object Unset extends StoredValue[Nothing]
Then define the usual typeclass for converting values:
/** Most stores use string on the backend..so we keep it simple. */
trait StorePrep[T] {
def parse(v: String): Option[T]
// communicate that a specific t may not be stringifiable
def stringify(t: T): Option[String]
}
implicit object JSONPrep extends StorePrep[js.Any] {
def parse(v: String) = nonFatalCatch opt js.JSON.parse(v)
def stringify(t: js.Any) = nonFatalCatch opt js.JSON.stringify(t)
}
class AnyValPrep[T <: AnyVal] extends StorePrep[T] {
def parse(v: String) = nonFatalCatch opt js.JSON.parse(v).asInstanceOf[T]
def stringify(t: T) =
nonFatalCatch opt js.JSON.stringify(t.asInstanceOf[js.Any])
}
val intPrep = new AnyValPrep[Int]
val floatProp = new AnyValPrep[Float]
val doubleProp = new AnyValPrep[Double]
val longProp = new AnyValPrep[Long]
implicit object StringPrep extends StorePrep[String] {
def parse(v: String) = Option(v)
def stringify(t: String) = Option(t)
}
We could have done this slightly differently of course and there are some variations such as using Either to report a message. But that’s overkill for what’s needed sometimes.
Then define our hook with very clear semantics:
/** Return a callback to issue command and an error flag if the last op was an error. */
def useLocalStorage[T](key: String, initialValue: StoredValue[T] = Keep)(
implicit prep: StorePrep[T]):
(Option[T], js.Function1[StoredValue[T], Unit], Boolean) = ???
The implementation almost worked on the first compile. It uses “givens” to pull a converter in scope to control value serialization then does the usual push into browser local storage. The approach of using “commands” is not common in js world, but it is not without precedent in some libraries.
Oh, and another reason is that I can define my types like A|Null
then write an extension that allows me to write dataPoint ?? "--"
so that when the data is there I get the value, when its null I get --
. This cuts down on alot of .getOrElse
typing. With a little more effort, you can also chain them without using flatMap/maps/for-comprehensions syntax.
Comments
Post a Comment