Scala 3: transparent inline with Dynamic
динамическая типизация внутри статической
В скале есть интерфейс Dynamic. От него можно унаследоваться и получить динамическую типизацию - при отсутствии поля в классе компилятор подставит вызов специального метода.
import scala
import scala.language.dynamics
class MyClass extends Dynamic
val a = MyClass
a.x
// a.selectDynamic("x")
a.x = 2
// a.updateDynamic("x")(2)Класс MyClass может быть любым, например таким:
class MyClass extends Dynamic {
private var x: Int = 0
def selectDynamic(fieldName: String): Int = {
fieldName match
case "x" => return x
}
def updateDynamic(fieldName: String)(value: Int): Unit = {
fieldName match
case "x" => x = value
}
}Конкретно такой класс получился не особо полезным - по поведению он похож на
class MyClass(){var x: Int = 0}только хуже - во время компиляции мы не увидим ошибок, если обратимся к несуществующему полю, и код упадёт в рантайме. Впрочем, этот код нужен для иллюстрации возожностей языка.
Что, если нам захочется сделать класс с полями разных типов?
class MyClass() {
var x: Int = 0
var b: Boolean = false
}Если попробуем написать альтернативу этому классу с помощью Dynamic, в качестве принимаемых и возвращаемых типов придётся указывать Any и кастовать к какому-то типу.
Впрочем, у нас тут Scala 3, в которой появились Union types, можно указать в качестве типа поля Int | Boolean и что-то иное типа строки компилятор не даст присвоить.
class MyClass extends Dynamic {
private var x: Int = 0
private var b: Boolean = false
def selectDynamic(field: String): Int | Boolean = {
field match
case "x" => x
case "b" => b
}
def updateDynamic(field: String)(value: Int | Boolean): Unit = {
field match
case "x" =>
value match
case v: Int => x = v
case "y" =>
value match
case v: Boolean => b = v
}
}Но появилась ещё проблема - мы всё равно можем попробовать присвоить Boolean в x или Int в b, а потом упадём в рантайме. Такова участь динамической типизации, ничего не поделать.
И тут c ноги врывается transparent inline! Перепишем метод selectDynamic:
import scala.compiletime.error
MyClass {
...
transparent inline def selectDynamic(inline field: String): Int | Boolean = {
inline field match
case "x" => x
case "b" => b
case _ => error("unknown field in MyClass")
}
}Что будет дальше? В месте вызова
a.x
компилятор подставит вызов функции selectDynamic, пойдёт внутрь, в inline match, найдёт там подходящую строку "x" и заменит всё-всё-всё на простое обращение к полю x с типом Int.
Если быть точным и посмотреть байткод - у MyClass появится метод - геттер
def inline$x(): Int
который будет вызываться напрямую.
Ну либо на это можно смотреть так
val x: Int = a.selectDynamic("x")
val b: Boolean = a.selectDynamic("b")Возвращаемый тип зависит от аргумента!
Кроме того, если обратиться к несуществующему полю, прямо во время компиляции подставится compiletime.error и компилятор покажет ошибку.
Поздравляю, мы снова вернулись к статической типизации. Какие выводы можно сделать?
- transparent inline - очень мощный инструмент, позволяющий гибко работать с типами. В scala 2.0 такое невозможно.
- Нет строго дуализма между runtime и compiletime преобразованиями, гибкость scala позволяет в какой-то мере заменить первое на второе. В С++ похожая ситуация.
Для полноты картины покажу, как можно переписать второй метод:
transparent inline def updateDynamic(inline field: String)(inline value: Int | Boolean): Unit = {
inline field match
case "x" =>
inline value match
case v: Int => x = v
case _ => error("should be int")
case "y" =>
inline value match
case v: Boolean => b = v
case _ => error("should be boolean")
case _ => error("unknown field")
}Посмотрите на переход от динамической типизации обратно к статической при помощи более "умного" компилятора! Что-то подобное можно попробовать в динамических языках типа Python, чтобы ловить ошибки как можно раньше. Не все, но какую-то часть.
Пример кода выше не очень практичен - можно сделать обычный класс с полями x: Int, b:Boolean и он будет прекрасно работать. В классе c Dynamic уже известные поля можно заменить на реальные поля и это тоже будет прекрасно работать, компилятор будет обращаться напрямую к ним вместо вызова selectDynamic:
class MyDynamic extends Dynamic{
var x: Double = 0
var b: Boolean = 0
def selectDynamic(...) ...
}В общем, код выше дублирует базовые возможности языка, бесполезен сам по себе, но, надеюсь, полезен для иллюстрации.
Для чего использовать transparent inline в реальном коде - не знаю. Люди как-то жили без этой возможности и прекрасно решали свои задачи. Но я точно уверен, что часть этих задач теперь можно решить более гибко и красиво.