quick note on dotty, graaljs interop: structural typing with Selectable

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

Popular posts from this blog

zio layers and framework integration

zio environment and modules pattern: zio, scala.js, react, query management

typescript ambient declarations, global.d.ts, lib and typeRoot.md