quick note on dotty, graaljs interop: structural typing with Selectable
This is a quick note on dotty’s structural typing via Structural
and graaljs interop.
graaljs is nodejs on the graal vm with interop to JVM.
You can run dotty as the JVM language quite easily, just like any other JVM language.
Dotty has a new feature for structural typing (https://dotty.epfl.ch/docs/reference/changed-features/structural-types.html) based on the Selectable
trait. It implements a dynamic lookup to values based on application specific metadata.
The example given on the web site is:
case class Record(elems: (String, Any)*) extends Selectable {
def selectDynamic(name: String): Any = elems.find(_._1 == name).get._2
}
type Person = Record {
val name: String
val age: Int
}
val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
println(s"${person.name} is ${person.age} years old.")
// Prints: Emma is 42 years old. }
graaljs allows you to call a JVM function from nodejs javascript easily:
const book = { id: "10", title: "Blah", isbn: "123", pages: 100 }
const resources = { token: "XYZ" }
Packages.example5.Resolvers.description(book, resources)
and on the dotty side:
object Resolvers:
def description(book: Value, resources: Value):
// ...
A Value
object is a facade over the javascript value. You can probe its “capabilities” such as being executable, having “members” or being “indexable” then call functions to access or convert the data to JVM types as needed. There are a bunch of choices. I wanted to use Selectable
:
Here’s a shortened version of code:
type ConvertFunction = Value => Any
type MetadataMapping = Map[String, ConvertFunction]
// This does not do a "conversion" of the entire object.
// We would want to cache results if access performance became an issue.
case class JSObject(o: Value, metadata: MetadataMapping = Map()) extends Selectable:
def selectDynamic(name: String): Any =
val v = o.getMember(name)
metadata.get(name) match
case Some(convert) => convert(v)
case _ =>
if v.isString then v.asString
else if v.isBoolean then v.asBoolean
else v
Using Selectable
with some metadata allows us to figure out the right type if it is not inferrable from the actual value. In the case of the polyglot API and javascript, a number is a number and could be an Int, Double, Float, Long, etc. If we want an Int, we have to know that and call value.asInt
explicitly even though we can only probe for value.isNumber
. There is no value.isInt
. Hence, we need external injected metadata to solve this problem.
Using a few other features of dotty, we can use this to create a Book
type that matches the Book
“type” from javascript:
// our structural type
type Book = JSObject:
val id: String
val title: String
val isbn: String
val pages: Int
// metadata needed to be specific about the Int
val BookMetadata = Map(
"pages" -> ((value: Value) => value.asInt),
)
// syntax extension for explicit conversion, we could use also use a dotty Converter.
def (v: Value).asBook = JSObject(v, BookMetadata).asInstanceOf[Book]
Now we can
def description(book: Value, resources: Value):
// validate its a book...then convert
val jvmBook = book.asBook
val allPages = jvmBook.pages + 10
println(s"title ${jvmBook.title}: $allPages")
// ...
Admitedly, this is actually much easier in scala.js where we can just alias over the object:
@js.native
trait Book extends js.Object {
val id: String
val title: String
val isbn: String
val pages: Int
}
and do a quick cast: someJSValue.asInstanceOf[Book]
. But then we would not be using the JVM for processing.
That’s it!
Comments
Post a Comment