Red de conocimiento informático - Conocimiento informático - Cómo diseñar un lenguaje

Cómo diseñar un lenguaje

¿Por qué diseñar un nuevo lenguaje? Solo hay dos razones para esto: o el idioma antiguo es difícil de dominar o es un idioma de dominio específico. No voy a discutir esto último, porque este tipo de cosas nunca se pueden hacer sin conocimientos específicos del dominio (por ejemplo, alguien que no sabe nada sobre bases de datos nunca podrá desarrollar SQL), y básicamente no es una cuestión de diseño de lenguaje. Por lo tanto, esta serie sólo discutirá el primer caso, el diseño de un lenguaje universal. Los lenguajes de propósito general también tienen sus propios "campos", pero hay tantos que se diluyen. A lo largo de la historia, encontrará que cuando las personas que solo han trabajado en unas pocas áreas están diseñando lenguajes, si no han recibido una educación sistemática en la teoría de los lenguajes de programación, harán un desastre. Por ejemplo, Go es uno de ellos: su padre es genial, pero no incluye "lenguaje de diseño" de ninguna manera.

Entonces, si quieres crear un idioma en el siglo XXI, no estás satisfecho con todos los idiomas comunes, por lo que quieres crear tu propio idioma. ¿De qué maneras se manifiesta esta insatisfacción? Por ejemplo, la razón para usar C# puede ser que su padre no es lo suficientemente guapo. Por ejemplo, la razón para usar C++ puede ser porque su coeficiente intelectual es demasiado bajo y no puede mantenerlo. Por ejemplo, la razón para usar Haskell puede ser. Debido a que hay muy poca gente y no se puede reclutar gente, por ejemplo, la razón para crear el lenguaje C puede ser que es realmente imposible completar la interacción y la abstracción entre humanos y computadoras, por lo que las personas sin el nivel de Linus escribirán mierda en el lenguaje C, pero. no puedes reclutar a Linus, etc. Pero no puedes reclutar a Linus por muchas razones. Sin embargo, independientemente del coeficiente intelectual del usuario, hay varios lenguajes que todavía admiro (C++, C#, Haskell, Rust y Ruby). Si tuviera que clasificar los lenguajes del mundo, definitivamente estarían entre los cinco primeros, aunque ellos La diferencia puede no ser grande. Pero aun así, hay algo en estos lenguajes que me irrita y me hace querer crear uno nuevo (para mi propio uso(?)) y la prueba es: "lee mi blog".

Entonces, ¿qué es un buen lenguaje? La gente siempre ha creído que un lenguaje es tan bueno como una biblioteca. De hecho, esto invierte completamente la relación causal. ¿Cómo se puede escribir una buena biblioteca sin una buena sintaxis? Java y C# La razón por la que la biblioteca de C# es fácil de usar es inseparable de la expresividad de su lenguaje, como linq (, xml, sql, parser, etc.), como WCF (solo considere la facilidad de uso). Por ejemplo, ¿se pueden escribir estas bibliotecas en Java? Todavía son difíciles de escribir, pero encontrará que no puede hacerlas fáciles de usar. De hecho, todo esto se debe a que la sintaxis de Java es basura. , es posible que desee buscar y echar un vistazo a los cinco idiomas que enumeré anteriormente. Sus características son que, debido a la sintaxis, la biblioteca es particularmente fácil de usar.

Por supuesto, Esto no requiere que todos aprendan el idioma hasta el punto de poder escribir bibliotecas. La distribución de los programadores es como la estructura de una pirámide, donde algunas personas simplemente escriben bibliotecas y la mayoría simplemente las usa sin aprender demasiado. a menos que quieras ser alguien que escriba bibliotecas. Pero recientemente hay una muy mala tendencia en la que algunas personas sienten que un lenguaje es demasiado difícil para ellos como para convertirse fácilmente en alguien que escriba bibliotecas, así que empiezan a decir que no, no lo es. bueno, no lo nombraré, todo el mundo lo sabe, jejeje.

Además de una biblioteca bien escrita y facilidad de uso, un buen lenguaje tiene dos características importantes: fácil de aprender y fácil de analizar. De hecho, eso no significa que puedas aprenderlo con solo mirarlo, sino que puedes adivinar muchas características desconocidas siempre que domines los trucos. Hay un problema de coherencia gramatical que es fácil de pasar por alto porque todos los errores. causados ​​por una mala coherencia de sintaxis tienen causas particularmente oscuras que son difíciles de ver de un vistazo. Aquí te daré algunos ejemplos para que puedas establecer el concepto.

El primer ejemplo es la definición de la variable puntero en C. que nos encanta ver:

int a, *b, **c;

Creo que muchas personas están confundidas por esto. Las cosas han salido mal, por eso muchos libros de texto nos dicen que cuando Al definir variables, los asteriscos al final del tipo deben escribirse delante de las variables para evitar malentendidos, por lo que muchas personas no entienden por qué está diseñado de esta manera. Este es obviamente un punto de partida. .

Pero, de hecho, este es solo un buen ejemplo de coherencia gramatical. En cuanto a por qué es una trampa, la pregunta no está aquí.

Todos sabemos que cuando la variable b es un puntero a int, el resultado de *b es un int. Definir una variable int a; es equivalente a decir "definir a como int". Entonces, veamos la declaración de variable anterior: int *b;. ¿Qué quiere decir esto? Lo que realmente significa es "definir *b como int". Esta "consistencia de definición y uso" es en realidad lo que queremos defender; las funciones del lenguaje C usan comas para separar los parámetros al definir parámetros, y también usan comas para separarlos al llamar, lo cual es bueno, mientras que las funciones del lenguaje Pascal usan punto y coma para definir; Los parámetros están separados, y también están separados por comas al llamar, lo cual es menos consistente.

Al ver esto, podrías preguntarte: ¿cómo sabes que su padre ve el lenguaje C de esta manera? Personalmente creo que si él no lo pensara, supongo que no sería tan malo, porque están los siguientes ejemplos:

int F(int a, int b);

int ( *f)(int a, int b);

Este también es un ejemplo de "consistencia de definición y uso". En lo que respecta a la primera línea de código, ¿cómo debemos entender "int F(int a, int b);"? Como arriba, dice "definir el resultado de F(a, b) como int". En cuanto a qué son a y b, también te dice: define a como int y define b como int. Entonces, de manera equivalente, la siguiente línea también dice "defina el resultado de (*f)(a, b) como un int". De hecho, el tipo de función no necesita escribir nombres de parámetros, pero aun así recomendamos escribir nombres de parámetros. De esta manera, Intellisense de Visual Studio le permitirá enumerar los nombres de los parámetros cuando escriba "(". Después de ver el. rápido, a veces no es necesario regresar. El código fuente está aquí

Hay un último ejemplo maravilloso de "consistencia de definición y uso" en lenguaje C:

int a;

typedef int a;

int (*f)(int a, int b);

typedef int (*f)(int a, int b) );

Typedef es una palabra clave: cambia un símbolo de una variable a un tipo. Por lo tanto, siempre que necesite nombrar un tipo, piense en cómo definir una variable de ese tipo y luego escriba. , precedido por un typedef. , y listo.

Pero para ser honesto, en términos de coherencia, el lenguaje C sólo puede llegar hasta cierto punto. La razón es que estas "definiciones" aparentemente hermosas. y usar reglas de coherencia" no se pueden usar. Combinados, se ve como esta línea de código:

typedef int(__stdcall*f significa 1 + (2 + (3 + (4 + 0))) . Y (.) es en realidad una función A que combina dos funciones en una: f (.) g = \x->f(g( x )) .

Entonces, lo que significa el código anterior es que si tengo las siguientes tres funciones:

add1 x = x + 1

mul2 x = x * 2

sqr x = x * x

Entonces cuando escribo el siguiente código:

superApply [sqr, mul2, add1] 1

Lo que hace en realidad es sqr( mul2(add1(1)) = ((1+1)*2)* ((1+1)*2) = 16. Por supuesto, Haskell se puede escribir de manera más simple y clara:

superApply [(\x->x*x), (*2), (+1)] 1

La simplicidad de este código en Haskell es realmente una locura, porque si lo escribimos en C++, etc. código de valencia (los parámetros de tipo de matriz en C++ no pueden tener una longitud, por lo que no se puede escribir código equivalente), se vería así:

template

T SuperApply(const vector>& fs, const T& x)

{

T resultado = x;

for(int i=fs .size()-1; i>= 0; i--)

{

resultado = fs[i](resultado);

}

devuelve resultado;

}

C++ no solo necesita escribir cada paso con claridad, sino que también describe el tipo. Además, todo el código se vuelve extremadamente confuso. Además, en C++ no hay forma de combinar tres funciones en un vector y luego llamar a SuperApply directamente. Por supuesto, algunas personas pueden decir que esto no se debe a que Haskell tenga foldr. Veamos cómo C# usa foldr (inverso +. agregado = foldr):

T SuperApply(Func[] fs, T x)

{

return (fs

.Reverse()

.Aggregate(x=>x, (a, b)=>y=>b(a(y)))

)

)

}

C# básicamente logra el mismo proceso de descripción que Haskell. También puede escribir el siguiente código, pero la sintaxis de declaración y uso. es un poco ruidoso. .....

SuperApply(nueva Func[]{

x=>x*x,

x=>x* 2,

x=> x+1

}, 1);

Cuando intento mostrarles otra "definición y coherencia de uso" en la práctica, ¿Por qué hablar de esto en el contexto de una discusión sobre coherencia gramatical? Todo el lenguaje Haskell debe entenderse en términos de patrones, por lo que el código anterior

superApply fs x = (foldr id (.) fs) x

Es decir, cuando usas superApply Con un "patrón" como a b, puedes considerarlo como (foldr id (.) a) b.

Por ejemplo

superApply [(\x->x*x), (*2), (+1)] 1

es

en realidad p>

(foldr id (.) [(\x->x*x), (*2), (+1)])1

Siempre que superApply apunte a esto función, entonces, independientemente del contexto. Sin embargo, puede realizar esta sustitución de forma segura sin cambiar ningún significado del programa, que es el principio de coherencia de Haskell. Veamos cómo Haskell logra la coherencia. Una cosa que debemos saber aquí es que si tenemos un operador +, para tratar + como una función tenemos que escribir (+). Si tenemos una función f, y queremos pensar en ella como un operador, entonces escribimos `f` (¡esa es la clave! (el símbolo de la izquierda). Entonces, Haskell nos permite hacer una declaración como: < / p>

(Punto x y) + (Punto z w) = Punto (x+z) (y+w)

(+) (Punto x y) (Punto z w) = Punto (x+ z ) (y+w)

(Punto x y) `Sumar`( Punto z w) =Punto (x+z) (y+w)

Sumar (Punto x y) ( Punto z w) = Punto (x+z) (y+w)

La forma simple de la secuencia de Fibonacci se puede escribir incluso así:

f 1 = 1

f 2 = 1

f (n+2) = f(n+1) + f(n)

Incluso se puede escribir en forma recursiva:

GetListLength [] = 0

GetListLength (x:xs) = 1 + GetListLength xs

Haskell implementa Haskell en todas partes "reemplazo de funciones y operadores" " y "patrones "coincidencia" de principios como base para la "consistencia entre definición e implementación", logrando así "consistencia entre definición e implementación", que es mucho mejor que los principios confusos y a medias del lenguaje C.

Uno Se podría decir que Haskell hace que escribir recursividad sea muy fácil, entonces, ¿no sería más fácil para los desbordamientos de pila o menos eficiente alentar a las personas a escribir recursividad y llenar el programa con recursividad? Aquí, puede desplazarse hacia arriba, en Hay una oración en. El comienzo de este artículo: "Además de que la biblioteca es fácil de escribir y usar, un buen lenguaje tiene dos características importantes: fácil de aprender y fácil de analizar". Esta oración se refleja en Haskell. > Sabemos que los bucles son recursivos de cola, por lo que si escribimos nuestro código como recursivo de cola, el compilador de Haskell lo reconocerá y lo tratará como una función recursiva de cola al generar código x86. El punto de salida es una expresión que no lo hace. contiene su propia llamada a función, o es una expresión que contiene su propia función y otros parámetros. Esto suena extraño, pero para decirlo sin rodeos, es:

GetListLength_ [ ] c = c

.

GetListLength_ (x:xs) c = GetListLength_ xs (c+1)

GetListLength xs = GetListLength _ xs 0

Cuando escribes Cuando codificas así, Haskell compila su código y en realidad genera un bucle, por lo que todas las preocupaciones anteriores desaparecen.

De hecho, extensas pruebas de rendimiento muestran que Haskell no es más del doble de rápido que C/C++ en la mayoría de las plataformas, y mucho más rápido que el actual. En Windows, el lenguaje funcional más rápido es F#. En Linux, es Scala. Haskell siempre ocupa el segundo lugar, pero es sólo un poco más lento que el primero.

Para evitar que este artículo sea demasiado largo, es mejor dividirlo en varios artículos con intervalos más cortos, por lo que hoy solo discutiré un problema: el problema de los punteros en C++. Dejaré los otros errores para el próximo artículo. Si no me hubieran preguntado en un grupo de fans sobre el error del que voy a hablar a continuación, no habría sabido que nadie más lo había hecho:

class Base

{

...

};

clase Derivada: Base pública

{

.. .

};

Base* bs = nuevo Derivado[10];

eliminar[] bs;

Quiero decir que esto es completamente compatible con C++ versus C y luego dejar que C lo arruine. De hecho, este problema no surge en el lenguaje C porque, para decirlo sin rodeos, solo hay un tipo de puntero en el lenguaje C: char*. Muchas funciones de C aceptan punteros char*, void* o posteriores. Las operaciones malloc y free de C sobre punteros en realidad los tratan como char* en la vista. Entonces, cuando asignas algo, lo conviertes al tipo que necesitas y finalmente lo liberas, la presencia o ausencia de la conversión en este paso no tiene ningún efecto en realizar la liberación correctamente.

Pero la situación es diferente en C++. C++ tiene herencia, y la herencia trae implícita la conversión de tipos de punteros. Mire el código anterior, agregamos [] un puntero de tipo Derivado* y luego lo convertimos implícitamente al tipo Base*. Finalmente, lo convertimos a eliminar [], porque eliminar [] necesita llamar al destructor, pero el puntero de tipo Base * no puede calcular correctamente las posiciones del puntero requeridas para los 10 destructores de la matriz derivada, por lo que el código está torcido en este momento. Maldita sea (si no está torcido, es sólo una coincidencia).

Para ser compatible con el lenguaje C, las dos reglas "el puntero nuevo [] requiere eliminar []" y "el puntero de subclase se puede convertir en puntero principal" colisionaron con éxito. De hecho, si necesitamos resolver este tipo de problema, ¿cómo podemos cambiar el tipo? De hecho, podemos introducir el mismo tipo de puntero Derivado[] que C#. Esto sigue siendo algo que surge de nuevo []. C ++ también puede solicitar eliminar [], pero la diferencia es que ya no se puede convertir a Base []. Es una pena que este tipo T[] esté ocupado por C y se use como T* dentro del tipo de parámetro de función. La sintaxis de C es una pérdida de tiempo...