or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

annotations.mdcompile-time.mdgeneric-programming.mdimmutable-arrays.mdindex.mdmacros.mdstructural-types.mdtuples.mdtype-safe-equality.md
tile.json

structural-types.mddocs/

Structural Types and Selection

The Selectable trait enables enhanced structural typing with dynamic member access and structural subtyping in Scala 3.

Core API

Selectable Trait

trait Selectable extends Any

Marker trait for objects that support structural selection via selectDynamic and applyDynamic methods.

Required Implementation Methods

Classes implementing Selectable should define these methods:

def selectDynamic(name: String): Any
def applyDynamic(name: String)(args: Any*): Any

Alternative signature for applyDynamic with class parameters:

def applyDynamic(name: String, paramClasses: Class[_]*)(args: Any*): Any

WithoutPreciseParameterTypes

trait WithoutPreciseParameterTypes extends Selectable

Marker trait indicating that precise parameter types are not needed for method dispatch, allowing more relaxed subtyping rules.

Usage Examples

Basic Structural Types

// Structural type definition
type HasLength = { def length: Int }
type HasArea = { def area: Double }

def getLength(obj: HasLength): Int = obj.length
def getArea(obj: HasArea): Double = obj.area

// Works with any object that has the required methods
val str = "hello"
val list = List(1, 2, 3, 4, 5)
val array = Array(1, 2, 3)

getLength(str)   // 5
getLength(list)  // 5
getLength(array) // 3

// Custom class with area
class Rectangle(width: Double, height: Double):
  def area: Double = width * height
  def perimeter: Double = 2 * (width + height)

val rect = Rectangle(5.0, 3.0)
getArea(rect)  // 15.0

Implementing Selectable

import scala.collection.mutable

class DynamicRecord extends Selectable:
  private val fields = mutable.Map[String, Any]()
  
  def selectDynamic(name: String): Any = 
    fields.getOrElse(name, throw new NoSuchFieldException(name))
  
  def updateDynamic(name: String)(value: Any): Unit =
    fields(name) = value
  
  def applyDynamic(name: String)(args: Any*): Any = 
    name match
      case "set" if args.length == 2 => 
        fields(args(0).toString) = args(1)
      case "get" if args.length == 1 => 
        fields.getOrElse(args(0).toString, null)
      case _ => 
        throw new NoSuchMethodException(s"$name with ${args.length} arguments")

// Usage
val record = DynamicRecord()
record.updateDynamic("name")("Alice")
record.updateDynamic("age")(30)

val name = record.selectDynamic("name")  // "Alice"
val age = record.selectDynamic("age")    // 30

record.applyDynamic("set")("email", "alice@example.com")
val email = record.applyDynamic("get")("email")  // "alice@example.com"

JSON-like Dynamic Object

import scala.util.{Try, Success, Failure}
import scala.collection.mutable

class JsonObject extends Selectable:
  private val data = mutable.Map[String, Any]()
  
  def selectDynamic(name: String): Any = 
    data.get(name) match
      case Some(value) => value
      case None => JsonNull
  
  def updateDynamic(name: String)(value: Any): Unit =
    data(name) = value
  
  def applyDynamic(name: String)(args: Any*): Any = 
    name match
      case "apply" if args.length == 1 => selectDynamic(args(0).toString)
      case "update" if args.length == 2 => updateDynamic(args(0).toString)(args(1))
      case "contains" if args.length == 1 => data.contains(args(0).toString)
      case "remove" if args.length == 1 => data.remove(args(0).toString).getOrElse(JsonNull)
      case _ => throw new NoSuchMethodException(s"$name${args.toList}")
  
  override def toString: String = 
    data.map { case (k, v) => s""""$k": ${formatValue(v)}""" }.mkString("{", ", ", "}")
  
  private def formatValue(value: Any): String = value match
    case s: String => s""""$s""""
    case n: Number => n.toString
    case b: Boolean => b.toString
    case JsonNull => "null"
    case obj: JsonObject => obj.toString
    case _ => s""""$value""""

object JsonNull

// Usage
val json = JsonObject()
json.updateDynamic("name")("Bob")
json.updateDynamic("age")(25)
json.updateDynamic("active")(true)

// Dynamic access
val name = json.selectDynamic("name")           // "Bob"
val age = json.selectDynamic("age")             // 25
val missing = json.selectDynamic("email")       // JsonNull

// Method calls
val hasAge = json.applyDynamic("contains")("age")       // true
val removed = json.applyDynamic("remove")("active")     // true
val stillThere = json.applyDynamic("contains")("active") // false

println(json)  // {"name": "Bob", "age": 25}

Configuration Object

import scala.collection.mutable

class Config extends Selectable:
  private val settings = mutable.Map[String, String]()
  
  def load(properties: Map[String, String]): Unit =
    settings ++= properties
  
  def selectDynamic(name: String): String = 
    settings.getOrElse(name, "")
  
  def applyDynamic(name: String)(args: Any*): Any = 
    name match
      case "getInt" if args.isEmpty => 
        Try(selectDynamic("defaultKey").toInt).getOrElse(0)
      case "getInt" if args.length == 1 => 
        Try(selectDynamic(args(0).toString).toInt).getOrElse(0)
      case "getBool" if args.length == 1 => 
        selectDynamic(args(0).toString).toLowerCase == "true"
      case "getDouble" if args.length == 1 =>
        Try(selectDynamic(args(0).toString).toDouble).getOrElse(0.0)
      case "set" if args.length == 2 =>
        settings(args(0).toString) = args(1).toString
      case _ => ""

// Usage
val config = Config()
config.load(Map(
  "host" -> "localhost",
  "port" -> "8080",
  "debug" -> "true",
  "timeout" -> "30.5"
))

val host = config.selectDynamic("host")              // "localhost"
val port = config.applyDynamic("getInt")("port")     // 8080
val debug = config.applyDynamic("getBool")("debug")  // true
val timeout = config.applyDynamic("getDouble")("timeout") // 30.5

config.applyDynamic("set")("newSetting", "value")
val newSetting = config.selectDynamic("newSetting")  // "value"

Database-like Query Object

case class Person(name: String, age: Int, city: String)

class QueryBuilder extends Selectable:
  private var table: String = ""
  private var conditions: List[String] = List.empty
  private var selectedFields: List[String] = List.empty
  
  def selectDynamic(name: String): QueryBuilder = 
    name match
      case fieldName => 
        selectedFields = selectedFields :+ fieldName
        this
  
  def applyDynamic(name: String)(args: Any*): QueryBuilder = 
    name match
      case "from" if args.length == 1 => 
        table = args(0).toString
        this
      case "where" if args.length == 1 => 
        conditions = conditions :+ args(0).toString
        this
      case "equals" if args.length == 1 =>
        val lastField = selectedFields.lastOption.getOrElse("unknown")
        conditions = conditions :+ s"$lastField = '${args(0)}'"
        this
      case "greaterThan" if args.length == 1 =>
        val lastField = selectedFields.lastOption.getOrElse("unknown")
        conditions = conditions :+ s"$lastField > ${args(0)}"
        this
      case _ => this
  
  def build(): String = 
    val select = if selectedFields.isEmpty then "*" else selectedFields.mkString(", ")
    val where = if conditions.isEmpty then "" else s" WHERE ${conditions.mkString(" AND ")}"
    s"SELECT $select FROM $table$where"

// Usage with method chaining
val query = QueryBuilder()
val sql = query
  .selectDynamic("name")
  .selectDynamic("age")
  .applyDynamic("from")("persons")
  .applyDynamic("where")("age > 18")
  .build()

// Result: "SELECT name, age FROM persons WHERE age > 18"

// More fluent API
val query2 = QueryBuilder()
val sql2 = query2
  .selectDynamic("name").applyDynamic("equals")("Alice")
  .applyDynamic("from")("users")
  .build()

// Result: "SELECT name FROM users WHERE name = 'Alice'"

Type-Safe Dynamic Access

import scala.reflect.ClassTag

class TypedRecord extends Selectable:
  private val data = mutable.Map[String, Any]()
  
  def selectDynamic(name: String): Any = 
    data.getOrElse(name, null)
  
  def updateDynamic(name: String)(value: Any): Unit =
    data(name) = value
  
  def applyDynamic(name: String)(args: Any*): Any = 
    name match
      case "get" if args.length == 1 => 
        data.get(args(0).toString)
      case "getAs" if args.length == 2 =>
        data.get(args(0).toString) match
          case Some(value) => 
            Try(value.asInstanceOf[args(1).asInstanceOf[ClassTag[_]].runtimeClass]).toOption
          case None => None
      case "put" if args.length == 2 => 
        data(args(0).toString) = args(1)
      case "size" => data.size
      case "keys" => data.keys.toList
      case "clear" => data.clear()
      case _ => null

// Usage
val record = TypedRecord()
record.updateDynamic("name")("Charlie")
record.updateDynamic("age")(35)
record.updateDynamic("scores")(List(95, 87, 92))

val name = record.selectDynamic("name")                    // "Charlie"
val age = record.applyDynamic("get")("age")               // Some(35)
val scores = record.applyDynamic("get")("scores")         // Some(List(95, 87, 92))
val missing = record.applyDynamic("get")("email")         // None

val size = record.applyDynamic("size")()                  // 3
val keys = record.applyDynamic("keys")()                  // List("name", "age", "scores")

Advanced Structural Typing with Refinements

// Complex structural type with multiple methods
type Drawable = { 
  def draw(): Unit
  def area: Double
  def perimeter: Double
}

type Movable = {
  def move(dx: Double, dy: Double): Unit
  def position: (Double, Double)
}

type DrawableMovable = Drawable & Movable

class Circle(var x: Double, var y: Double, val radius: Double) extends Selectable:
  def draw(): Unit = println(s"Drawing circle at ($x, $y) with radius $radius")
  def area: Double = math.Pi * radius * radius
  def perimeter: Double = 2 * math.Pi * radius
  def move(dx: Double, dy: Double): Unit = 
    x += dx
    y += dy
  def position: (Double, Double) = (x, y)
  
  def selectDynamic(name: String): Any = name match
    case "area" => area
    case "perimeter" => perimeter
    case "position" => position
    case "x" => x
    case "y" => y
    case "radius" => radius
    case _ => null
  
  def applyDynamic(name: String)(args: Any*): Any = name match
    case "draw" => draw()
    case "move" if args.length == 2 => 
      move(args(0).asInstanceOf[Double], args(1).asInstanceOf[Double])
    case _ => null

def processDrawable(d: Drawable): String =
  d.draw()
  s"Area: ${d.area}, Perimeter: ${d.perimeter}"

def processMovable(m: Movable): String = 
  val (x, y) = m.position
  m.move(1.0, 1.0)
  val (newX, newY) = m.position
  s"Moved from ($x, $y) to ($newX, $newY)"

def processDrawableMovable(dm: DrawableMovable): String =
  processDrawable(dm) + "; " + processMovable(dm)

val circle = Circle(0.0, 0.0, 5.0)
val result = processDrawableMovable(circle)
// Draws circle and returns area/perimeter info plus movement info

Structural types and the Selectable trait provide powerful dynamic programming capabilities while maintaining type safety through structural subtyping and compile-time verification of method existence.