Código fuente println de Scala
Después del artículo del mes pasado, recibí algunas quejas/comentarios de que los ejemplos que he usado en esta serie hasta ahora no abordan ningún tema sustancial. Por supuesto, es razonable utilizar algunos pequeños ejemplos en las primeras etapas del aprendizaje de un nuevo idioma, y es natural que los lectores quieran ver más ejemplos de la vida real para comprender las áreas más profundas, las poderosas funciones y las ventajas del idioma. Entonces, en el artículo de este mes haremos un ejercicio de dos partes para crear un lenguaje de dominio específico (DSL), utilizando un pequeño lenguaje de calculadora como ejemplo.
Acerca de esta serie
Ted Neward hablará en profundidad con usted sobre el lenguaje de programación Scala. En esta nueva serie de desarrolladorWorks, aprenderá más sobre Sacla y lo verá en la práctica. Al comparar las funciones del lenguaje de Scala, el código de Scala y el código de Java aparecen juntos, pero (descubrirá) que en Scala gran parte del contenido es. no está directamente relacionado con nada de lo que se encuentra en la programación Java, ¡y esa es la belleza de Scala! ¿Por qué aprender Scala si puedes hacerlo en código Java?
Lenguajes específicos del dominio
Tal vez no puedas soportar (o no tengas tiempo) la presión del gerente del proyecto, déjame decirte eso. Los lenguajes específicos de dominio no son más que un intento (nuevamente) de poner la funcionalidad de una aplicación en manos del usuario.
Al definir un nuevo lenguaje de texto que los usuarios pueden entender y usar directamente, los programadores han escapado con éxito de la molestia de lidiar constantemente con solicitudes de interfaz de usuario y mejoras de funciones, lo que también permite a los usuarios crear scripts y otras herramientas. comportamientos para las aplicaciones que construyen. Aunque este ejemplo puede ser un poco arriesgado (y puede generar algunos correos electrónicos de queja), todavía quiero decir que el ejemplo más exitoso de DSL es Microsoft. El lenguaje Office Excel se utiliza para expresar varios cálculos y el contenido de las celdas de la hoja de cálculo. Algunos incluso argumentan que SQL en sí es un DSL, pero esta vez es un lenguaje diseñado para interactuar con bases de datos relacionales (imagínese cómo sería para un programador obtener datos de Oracle a través de llamadas API tradicionales de lectura()/escritura()).
El DSL creado aquí es un lenguaje de calculadora simple para obtener y evaluar expresiones matemáticas. De hecho, el objetivo aquí es crear un lenguaje pequeño que permita al usuario ingresar una expresión algebraica relativamente simple, que luego este código pueda evaluar y producir un resultado. Para ser lo más simple y claro posible, este lenguaje no admitirá muchas de las funciones que admite una calculadora con todas las funciones, pero no quiero limitar su uso a la enseñanza de este lenguaje. El lenguaje debe ser lo suficientemente extensible para que los lectores puedan usarlo como el núcleo de un lenguaje más poderoso sin cambiarlo por completo, lo que significa que el lenguaje debe ser fácilmente extensible y tratar de permanecer encapsulado sin barreras.
Más información sobre DSL
El tema de DSL abarca una amplia gama de temas, y su riqueza y amplitud no se pueden describir en un párrafo de este artículo. Los lectores que quieran aprender más sobre DSL pueden consultar los libros en curso de Martin Fowler que se enumeran al final de este artículo, prestando especial atención a la discusión sobre DSL internos y externos. Scala, con su sintaxis flexible y potentes funciones, es el lenguaje más potente para crear DSL internos y externos.
En otras palabras, el objetivo (final) es permitir a los clientes escribir código que haga lo siguiente.
Objetivo DSL de la Calculadora de Inventario
//Esto es Java usando la cadena de calculadora s = ((*)+); resultado doble = evaluación de la calculadora tedneward calcdsl println(obtuvimos; +resultado); //debería ser
No terminaremos toda la discusión en un artículo, pero podemos aprender algunas cosas en este artículo y en el siguiente. Completar todo el contenido.
Desde una perspectiva de implementación y diseño, podemos comenzar con la construcción de un analizador basado en cadenas, construyendo un analizador que pueda seleccionar cada carácter y calcularlo dinámicamente. Esto es realmente tentador, pero solo funciona con lenguajes relativamente simples y no se escala muy bien. Si el objetivo del lenguaje es lograr una escalabilidad simple, antes de profundizar en la implementación, dediquemos un tiempo a pensar en cómo diseñar el lenguaje.
De acuerdo con la esencia de la teoría básica de la compilación, se puede saber que el funcionamiento básico de un procesador de lenguaje (incluidos el intérprete y el compilador) consta de al menos dos etapas.
●?El analizador se utiliza para tomar el texto de entrada y convertirlo en un árbol de sintaxis abstracta (AST)●? Se usa un generador de código (en el caso de un compilador) para tomar el AST y generar el código de bytes requerido a partir de él, o se usa un evaluador (en el caso de un intérprete) para tomar el AST y calcular lo que encuentra en el AST. .
Tener un AST puede optimizar el árbol de resultados hasta cierto punto. Si nos damos cuenta de esto, las razones de las diferencias anteriores se volverán más obvias. Para una calculadora, es posible que deseemos volver a verificar la expresión para encontrar dónde se pueden truncar partes enteras de la expresión, como dónde están los operandos en una expresión de multiplicación (lo que significa que no importa cuántos otros operandos haya, el resultado será ).
Lo primero que debes hacer es definir un AST para el lenguaje de la calculadora. Afortunadamente, Scala tiene una clase de caso que proporciona datos enriquecidos y utiliza paquetes muy delgados. Algunas de sus propiedades los hacen muy adecuados para construir AST.
Clase de caso
Antes de entrar en la definición de AST, permítanme describir brevemente qué es una clase de caso. Las clases de casos son un mecanismo conveniente para que los programadores de Scala creen una clase con algunos valores predeterminados asumidos, por ejemplo, al escribir lo siguiente.
La lista utiliza la clase de caso persona.
Case class Person(first:String last:String age:Int){ }
El compilador de Scala no solo genera el constructor esperado según nuestras expectativas: el compilador de Scala también implementa Se pueden generar equals() toString() y hashCode() en el sentido convencional. De hecho, esta clase de caso es común (es decir, no tiene otros miembros), por lo que el contenido de las llaves después de la declaración de clase de caso es opcional.
Enumera la lista de clases más corta del mundo
Case class Person(first:String last:String age:Int)
Esto se resuelve fácilmente con nuestro Old verificación javap de amigo.
¡Lista Generador de Código Sagrado Batman!
c:\Projects\Exploration\Scala>javap Persona compilada a partir del caso scala clase pública Persona extiende java lang Objeto implementa Scala Objeto Scala Producto Scala Java io Serializable {? Persona pública (Java idioma String Java idioma String int);? elemento de producto de objeto de idioma java público (int);? public int productArity();? prefijo de producto de cadena de lenguaje público java();? booleano público es igual (objeto de idioma java);? idioma público Java String toString();? público int hashCode();? público int $etiqueta();? edad int pública();? idioma Java público String last();? public Java lang String first();}
Como puede ver, suceden muchas cosas con las clases de casos que generalmente no son causadas por clases tradicionales. Esto se debe a que las clases de casos se usan junto con la coincidencia de patrones de Scala (analizada brevemente en Tipos de colección).
El uso de clases de casos es algo diferente del uso de clases tradicionales, porque generalmente no se construyen con la nueva sintaxis tradicional. De hecho, normalmente se crean mediante un método de fábrica con el mismo nombre que la clase.
¿La lista no utiliza la nueva sintaxis?
Aplicación de objeto {? def main(args:Array[String]):Unidad =? {?val ted = Persona(Ted Neward)? }}
Las clases de casos en sí mismas pueden no ser tan interesantes o diferentes como las clases tradicionales, pero hay una diferencia importante al usarlas. El código generado por la clase case prefiere la ecuación biise a la ecuación de referencia, por lo que el siguiente código tiene algunas sorpresas interesantes para los programadores de Java.
Lista que esta no es la clase anterior.
Aplicación de objeto {? def main(args:Array[String]):Unidad =? {?val ted = Persona(Ted Neward)? val ted = Persona(Ted Neward)? val amanda = Persona(Amanda Raucher)? Salida del sistema println( ted == amanda: +?(if (ted == amanda) Sí, más No)? Salida del sistema println( ted == ted: +?(if (ted == ted) Sí, más No)? Salida del sistema println( ted == ted : +?(if (ted == ted ) Sí, ¿no?} }/* C:\Projects\Exploration\Scala>Scala Appted == Amanda:Notó == ted:Sí ted = = ted:Sí */
El valor real de las clases de casos radica en la coincidencia de patrones. Los lectores de esta serie pueden revisar la coincidencia de patrones (consulte el segundo artículo de esta serie sobre varias estructuras de control en Scala). al switch/Case de Java solo de una manera más técnica y funcional. La coincidencia de patrones no solo puede verificar el valor de la estructura coincidente para realizar la coincidencia de valores, sino también hacer coincidir valores basados en comodines locales (similar a la protección contra valores predeterminados locales). coincidencias de prueba. Los valores de los criterios de coincidencia también se pueden vincular a variables locales, e incluso los tipos que satisfacen los criterios de coincidencia pueden coincidir entre sí.
Con la coincidencia de patrones de clases de casos, tiene funciones más potentes, como se muestra en el listado.
Este no es un interruptor antiguo.
case class Persona(primero:Cadena último:Cadena edad:Int); def main(args:Array[String]):Unidad =? {?val ted = Persona(Ted Neward)? val amanda = Persona(Amanda Raucher)? ¿Salida del sistema println(proceso(ted))? ¿Salida del sistema println(proceso(amanda))? }?def proceso(p: Persona) =? {?proceso+p+revelar+? si edad años& gt= & gt? ¿primero+último+es+edad en años+años? ¿No sé qué hacer con esta persona })?}/* C:\Projects\Exploration\Scala& gt; Guy (Ted Neward) reveló que definitivamente son procesadores viejos (Amanda Laucher) reveló que Amanda Laucher tiene 30 años */
Hay muchas operaciones en la lista. Entendamos lentamente lo que está pasando y luego volvamos a la calculadora y veamos cómo aplicarlos.
Primero, toda la expresión coincidente está entre paréntesis, lo cual no es requerido por la sintaxis de coincidencia de patrones, pero esto se debe a que estoy concatenando los resultados de la expresión de coincidencia de patrones según el prefijo anterior (recuerde el función Todo en un lenguaje de fórmulas es una expresión)
En segundo lugar, hay dos comodines en la expresión del primer caso (el carácter subrayado es un comodín), lo que significa que la coincidencia obtendrá una coincidencia para Persona Cualquiera. valor para el campo, pero introduce una variable local a la que se vinculará el valor en la página de la aplicación. Esta situación sólo puede ocurrir si también se proporciona una expresión de protección (seguida de una expresión if). Sólo la primera persona tendrá éxito, no la segunda. La segunda expresión de caso usa comodines en la parte del nombre de Persona, pero usa la cadena constante Neward para hacer coincidir la parte del apellido y comodines en la parte de la edad.
Debido a que la primera persona ya coincide con el caso anterior y la segunda persona no tiene el apellido Neward, no se activará ninguna coincidencia para nadie (pero la Persona (Michael Neward) irá al segundo caso. , Porque la cláusula de guarda falla en el primer caso).
El tercer ejemplo muestra un uso común de coincidencia de patrones, a veces llamado extracción. Durante este proceso de extracción, los valores del objeto coincidente P se extraen en variables locales (primero, último y edad) para su uso en el bloque de casos. La última expresión de caso es el valor predeterminado del caso ordinario y solo se activará si otras expresiones de caso no tienen éxito.
Después de una breve comprensión de las clases de casos y la coincidencia de patrones, volvamos a la tarea de crear la calculadora AST
Calculadora AST
Primero, la calculadora AST debe tener un tipo base común porque las expresiones matemáticas generalmente se componen de subexpresiones, lo cual es fácil de ver a través de +(*). En este ejemplo, la subexpresión (*) será el operando derecho de la operación +.
De hecho, esta expresión proporciona tres tipos de AST.
●?Expresión sucia●? ¿Tipos numéricos que llevan valores constantes ●? Operadores binarios que llevan operaciones y dos operandos
Piénselo. En aritmética, el operador unario también puede usarse como operador negativo (signo menos) para convertir un valor numérico de positivo a negativo, por lo que podemos introducir el siguiente AST básico.
Lista de calculadora AST (src/calc escalar)
paquete tedneward calcdsl {? clase abstracta privada [calcdsl] Expr? privado[calcdsl]? El número de clase de caso (valor: Doble) extiende Expr? privado[calcdsl]? clase de caso UnaryOp (operador: String arg: Expr) extiende Expr? privado[calcdsl]? case class BinaryOp(operator:String left:Expr right:Expr) extends Expr}
Tenga en cuenta que la declaración del paquete coloca todo esto en un solo paquete (TENDNEARD CalcDSL), con el modificador de acceso delante de cada clase. La declaración indica que otros miembros o subpaquetes del paquete pueden acceder al paquete. La razón para centrarse en esto es que es necesario que haya un conjunto de clientes reales de la calculadora de pruebas JUnit que puedan probar este código y que no necesariamente vean el AST, por lo que las pruebas unitarias deben escribirse como un subpaquete de TENDNEARD. CalcDSL.
Prueba de calculadora de inventario (testsrc/calctest scala)
Paquete tedneward calcdsl test {? ¿Prueba de cálculo de clase? {?import JUnit_Assert_@Test def ast Test =? {?val n = Número()? afirmarEquals(nvalor)? } @Test def prueba de igualdad =? {?val binop = BinaryOp(+Number()Number())afirmar es igual a(Number()binop left)? afirmar es igual (Número()binop ¿verdad)? afirmarEquals (+ operador binop)? }?}}
Hasta ahora, todo bien. Ya tenemos AST.
Piénsalo de nuevo. En cuatro líneas de código Scala, construimos una jerarquía de tipos para representar un conjunto arbitrariamente profundo de expresiones matemáticas (por supuesto, estas expresiones matemáticas son simples pero aún así muy útiles). Eso no es nada comparado con la capacidad de Scala para hacer que la programación de objetos sea más fácil y más expresiva (no se preocupe por las características realmente poderosas que están por venir).
A continuación, necesitamos una función de evaluación que tome el AST y encuentre su valor numérico. Con potentes capacidades de coincidencia de patrones, es fácil escribir dicha función.
Calculadora de lista (src/calc escalar)
paquete tedneward calcdsl {? // ?Cálculo de objetos? {?def evaluar(e: Expr) : Doble =? {?e coincide con {? Número de caso (x) = >x? caso UnaryOp(x)=>(evaluar(x))? caso BinaryOp( + x x ) = >(evaluación(x) +evaluación(x))? caso BinaryOp( x x ) =>(evaluar(x)evaluar(x))? case BinaryOp(* x x)= >(evaluar(x) *evaluar(x))? case BinaryOp(/x x)= >(evaluar(x)/evaluar(x))? }?}?}}
Tenga en cuenta que evaluar() devuelve un valor Doble, lo que significa que cada caso en la coincidencia del patrón debe evaluarse como un valor Doble. No es difícil que un número devuelva simplemente el valor que contiene, pero para el resto de casos (con dos operadores) también tenemos que averiguar primero los operandos y luego hacer las operaciones necesarias (suma negativa, resta, etc.). ), como vemos a menudo en los lenguajes funcionales, se usa la recursividad, por lo que solo necesitamos llamar a evalua() en cada operando antes de realizar la operación completa.
La mayoría de los programadores orientados a objetos comprometidos argumentarían que la idea de realizar operaciones fuera de los propios operadores es simplemente incorrecta; tal idea va claramente en contra de los principios de encapsulación y polimorfismo, francamente, no lo es. Aunque vale la pena discutirlo, viola claramente el principio de encapsulación, al menos en el sentido tradicional.
La pregunta más importante que debemos considerar aquí es ¿dónde empaquetamos el código? Tenga en cuenta que las clases AST no son visibles fuera del paquete, y a los clientes (eventualmente) solo se les pasará una cadena de expresiones que desean evaluar, lo que sugiere que solo las pruebas unitarias utilicen las clases de casos AST directamente.
Pero esto no significa que todos los paquetes sean inútiles u obsoletos. De hecho, por el contrario, intenta convencernos de que existen muchos otros métodos de diseño que son válidos además de los familiares en el dominio de los objetos. No olvide que Scala tiene objetos y funciones. A veces, Expr necesita tener un comportamiento adicional adjunto a sí mismo y a sus subclases (como un método toString para obtener un buen resultado). En este caso, se puede poner en práctica fácilmente. Agregar estos métodos a la combinación funcional y orientada a objetos de Expr proporciona otra opción. Ya sean programadores funcionales o de objetos, no ignorarán los métodos de diseño de la otra mitad, pero considerarán cómo combinar los dos para lograr algunos resultados interesantes.
Algunas de las otras opciones son problemáticas desde una perspectiva de diseño. Por ejemplo, el uso de operadores de transporte de cadenas puede provocar pequeños errores tipográficos que, en última instancia, generarán resultados incorrectos. En el código de producción, se podrían usar enumeraciones (quizás deban usarse) en lugar de cadenas, lo que significa que potencialmente podríamos abrir operadores y permitir llamadas a funciones más complejas como abs sin cos tan o incluso funciones definidas por el usuario. Es difícil admitir estas funciones basadas en métodos de enumeración.
Para todos los diseños e implementaciones, no existe un método de toma de decisiones adecuado, solo las consecuencias.
Sin embargo, aquí se puede utilizar un truco interesante. Algunas expresiones matemáticas se pueden simplificar, optimizando así (potencialmente) la evaluación de la expresión (demostrando así la utilidad de los AST).
●?Cualquier operando agregado se puede reducir a un operando distinto de cero●?Cualquier operando multiplicado se puede reducir a un operando distinto de cero●?Cualquier operando multiplicado se puede reducir a cero.
No solo eso, también introdujimos un paso llamado simplificar() para realizar estas tareas de simplificación específicas.
¿Calculadora de lista (src/calc escalar)
? def simplificar(e: Expr): Expr =? {?e coincide con {? // ¿Doble negativo devuelve el valor original? caso UnaryOp(UnaryOp(x))=>x? // ¿El número positivo devuelve el valor original? caso UnaryOp( + x) = >x? //¿x se multiplica para devolver el valor original? caso BinaryOp(* x Número())=>x? // ¿Multiplicar por x y devolver el valor original? caso BinaryOp(*Number()x)=>x? // ¿Multiplicar x devuelve cero? case BinaryOp(* x Número())= >Número()? // ¿Multiplicar por x devuelve cero? case BinaryOp(*Número()x)=>Número()? // ¿X dividido por devuelve el valor original? caso BinaryOp(/x Número())=>x? // ¿Agregar x devolverá el valor original? caso BinaryOp(+x Número())= >x? // ¿Agregar a x y devolver el valor original? caso BinaryOp(+Number()x)= >x? //¿Hay algo más que no se pueda simplificar? caso _ = >e? }?}
Aún debes prestar atención a cómo utilizar las funciones de coincidencia constante y enlace variable de la coincidencia de patrones para que estas expresiones sean fáciles de escribir. El único cambio en evaluar() es la inclusión de una llamada simplificada antes de la evaluación.
¿Calculadora de lista (src/calc escalar)
? def evaluar(e: Expr): Doble =? {?Coincidencia(e)simplificada{? Número de caso (x) = >x? caso UnaryOp(x)=>(evaluar(x))? caso BinaryOp( + x x ) = >(evaluación(x) +evaluación(x))? caso BinaryOp( x x ) =>(evaluar(x)evaluar(x))? case BinaryOp(* x x)= >(evaluar(x) *evaluar(x))? case BinaryOp(/x x)= >(evaluar(x)/evaluar(x))? }?}
Se puede simplificar aún más. Observe cómo solo simplifica la parte inferior del árbol. Si tenemos un BinaryOp que contiene BinaryOp( * Number() Number Number()) y Number(), entonces el BinaryOp interno se puede simplificar a Number(), y también lo hará el BinaryOp externo, porque uno de los operandos del BinaryOp externo es cero.
De repente sufrí una enfermedad profesional de escritor, así que quería dejar que los lectores la definieran. De hecho, fue solo para agregar algo de diversión. Si los lectores están dispuestos a enviarme su implementación, la incluiré en el análisis de código del próximo artículo. Habrá dos unidades de prueba para probar esta situación y fallará inmediatamente. Su tarea (si decide aceptarla) es hacer que estas pruebas, y cualquier otra prueba, pasen, siempre y cuando las pruebas adopten algún grado de anidamiento op binario y unario.
Conclusión
Evidentemente no he terminado el análisis, pero la calculadora AST ha ido tomando forma. Podemos agregar operaciones adicionales para ejecutar AST sin realizar grandes cambios y sin requerir mucho código (siguiendo el patrón de visitantes de la Banda de los Cuatro), ya tenemos algo de código de trabajo para realizar el cálculo en sí (si el cliente está dispuesto a hacerlo). construir el código de evaluación para nosotros).
Lishi Xinzhi/Article/program/Java/hx/201311/25735