Enable javascript in your browser for better experience. Need to know to enable it? Go here.

Cómo pueden interactuar Scala y Kotlin para construir DSLs (parte uno)

No es todos los días que tienes que desarrollar software para un telescopio gigante. Sin embargo, cuando lo haces, expande tu comprensión, y apreciación, de cosas que antes dabas por sentado. En resumen, te obliga a experimentar. Descubrimos eso al desarrollar una herramienta de orquestación para el Telescopio de Treinta Metros, un telescopio que está siendo desarrollado por un consorcio de diferentes organizaciones y que se lanzará en 2030. Thoughtworks estaba trabajando con el Instituto Indio de Astronomía (con sede en Bengaluru) y la Oficina del Proyecto TMT (con sede en Pasadena, CA.).

 

La "experimentación" central a la que nos llevó un desafío en particular fue combinar Kotlin y Scala para desarrollar un lenguaje específico del dominio. Entraremos en más detalles sobre cómo lo hicimos en breve, pero antes de llegar a eso, vale la pena describir la naturaleza del desafío.

 

El desafío

 

La herramienta de orquestación es una parte importante del telescopio utilizada por los científicos para coordinar la compleja red de componentes del telescopio de diversas maneras según la observación que tengan la intención de realizar. En términos simples, es un motor de ejecución; los expertos en el dominio (es decir, los científicos) deben ingresar la lógica requerida escribiendo scripts.

 

El desafío fue que, si bien gran parte de la pila de software del telescopio se desarrolló en Scala, los científicos que utilizarían la herramienta de orquestación y escribirían los scripts estaban familiarizados con lenguajes como C++. Usar Scala sería una curva de aprendizaje pronunciada.

 

Si bien la mayoría de ellos tenían conocimientos sobre constructos básicos de programación y experiencia con lenguajes de script como Python, los principales desafíos para ellos eran manejar conceptos y técnicas complejas como la multihilo y la concurrencia. Aunque los científicos carecían de conocimientos sobre ellos, eran críticos para garantizar que la herramienta de orquestación pudiera ejecutar secuencias y configuraciones increíblemente complejas.

 

La solución fue intentar desarrollar un DSL integrado fácil de usar, que tuviera una curva de aprendizaje mínima y que se encargara de la concurrencia y la multihilo.

 

Ahora que tienes el contexto del proyecto, te mostraremos cómo lo hicimos, comenzando con nuestro primer (y finalmente equivocado) intento con Scala.

 

El primer intento: construir un DSL con Scala

 

Inicialmente, elegimos Scala para implementar el DSL integrado y Sequencer como las API de Future y async/await del lenguaje eran efectivas para operaciones asíncronas. Todas las modificaciones de datos dentro del script se realizaron dentro de las API de Future; estas se ejecutaron en un ejecutor de un solo hilo. Esto significaba que el script se ejecutaba de manera concurrente, asegurando que no hubiera posibilidad de acceder a los datos de manera paralela. Esto fue especialmente útil ya que eliminó problemas como las condiciones de carrera y la corrupción del estado debido al acceso paralelo.

 

Un script simplista creado con nuestro DSL de Scala se muestra a continuación. El Script maneja un comando llamado 'observationPrep' programando un comando para un componente 'motor1' en un momento dado.

// TcsSync.scala

class TcsSync(csw: CswServices) extends Script(csw) {

 private val timeKey = KeyType.TAITimeKey.make(name = "time")

 handleSetupCommand("observationPrep") { command =>

   val executeAt = command(timeKey).head

   spawn {                                                // --------------> [ 1 ]

     csw.scheduler.scheduleOnce(executeAt) {

       spawn {                                            // --------------> [ 2 ]

         val moveCommand =

           Setup(tcs.prefix, CommandName("move30Degrees"), command.maybeObsId)

         csw.submit("motor1", moveCommand).await          // --------------> [ 3 ]

       }

     }

     Done

   }

 }

}

Aquí, spawn (en 1,2) es un alias para la función asíncrona y .await (en 3) es un método de extensión para la función await. Scala proporciona las funciones async y await para simplificar el código asíncrono.

 

Las funciones asíncronas marcan un bloque de código asíncrono. El bloque suele contener una o más llamadas await, que marcan un punto donde la computación se suspenderá hasta que el Future esperado esté completo.

 

En esta etapa, nosotros, como grupo de programadores, estábamos bastante satisfechos con el DSL. Sin embargo, no estábamos seguros de qué tan fácil sería de usar para las personas que realmente escribirían scripts para la herramienta de orquestación. Varios interesados también expresaron preocupaciones sobre la complejidad del DSL. Esto fue una clara señal de que necesitábamos una alternativa más fácil de usar.

 

El problema con el DSL de Scala fue que los scripts requerían muchas operaciones asíncronas y las API de Future de Scala exigían que los escritores de scripts ingresaran declaraciones async/await para cada una. Obviamente, esto añadió una complejidad significativa a la tarea del escritor de scripts. Olvidar escribir declaraciones await podría dar lugar a un comportamiento no deseado.

 

El ejemplo a continuación demuestra una instancia donde cada llamada a función es asíncrona.

spawn {
 

 csw.submit("motor1", moveCommand).await

 csw.publish(currentState()).await

 csw.submit("motor2",moveCommand).await

 csw.publish(currentState()).await

}

 

El enfoque de Scala para la programación asíncrona, que es asíncrona por defecto y un comportamiento sincrónico explícito, complica el trabajo de los escritores de scripts. Además, debido a que los scripts tendrían que escribirse como una clase Scala, significaba que los escritores de scripts tendrían que aprender una construcción adicional, lo que podría llevar a casos específicos. Por lo tanto, comenzamos la búsqueda de una mejor opción.

 

En la segunda parte de esta serie de blogs, pasaremos a discutir cómo usamos Kotlin y por qué nos ayudó a resolver este desafío complicado.

Aviso legal: Las declaraciones y opiniones expresadas en este artículo son las del autor/a o autores y no reflejan necesariamente las posiciones de Thoughtworks.

Aprende más sobre nuestro trabajo en el Telescopio de Treinta Metros