The Selectable trait enables enhanced structural typing with dynamic member access and structural subtyping in Scala 3.
trait Selectable extends AnyMarker trait for objects that support structural selection via selectDynamic and applyDynamic methods.
Classes implementing Selectable should define these methods:
def selectDynamic(name: String): Any
def applyDynamic(name: String)(args: Any*): AnyAlternative signature for applyDynamic with class parameters:
def applyDynamic(name: String, paramClasses: Class[_]*)(args: Any*): Anytrait WithoutPreciseParameterTypes extends SelectableMarker trait indicating that precise parameter types are not needed for method dispatch, allowing more relaxed subtyping rules.
// 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.0import 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"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}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"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'"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")// 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 infoStructural types and the Selectable trait provide powerful dynamic programming capabilities while maintaining type safety through structural subtyping and compile-time verification of method existence.