Share via


Conceptos básicos

En esta sección se describen los conceptos básicos que aparecen en las secciones siguientes.

Valores

Un único fragmento de datos se denomina valor. En general, hay dos categorías generales de valores: valores primitivos, que son atómicos, y valores estructurados, que se crean a partir de valores primitivos y otros valores estructurados. Por ejemplo, los valores

1 
true
3.14159 
"abc"

son primitivos ya que no se componen de otros valores. Por otra parte, los valores

{1, 2, 3} 
[ A = {1}, B = {2}, C = {3} ]

se construyen con valores primitivos y, en el caso del registro, otros valores estructurados.

Expresiones

Una expresión es una fórmula que se usa para construir valores. Una expresión se puede formar mediante una variedad de construcciones sintácticas. Los siguientes son algunos ejemplos de expresiones. Cada línea es una expresión independiente.

"Hello World"             // a text value 
123                       // a number 
1 + 2                     // sum of two numbers 
{1, 2, 3}                 // a list of three numbers 
[ x = 1, y = 2 + 3 ]      // a record containing two fields: 
                          //        x and y 
(x, y) => x + y           // a function that computes a sum 
if 2 > 1 then 2 else 1    // a conditional expression 
let x = 1 + 1  in x * 2   // a let expression 
error "A"                 // error with message "A"

La forma más sencilla de expresión, como se ha mencionado antes, es un literal que representa un valor.

Las expresiones más complejas se crean a partir de otras expresiones, llamadas subexpresiones. Por ejemplo:

1 + 2

La expresión anterior se compone realmente de tres expresiones. Los literales 12 son subexpresiones de la expresión primaria 1 + 2.

La ejecución del algoritmo definido por las construcciones sintácticas que se usan en una expresión se denomina evaluación de la expresión. Cada tipo de expresión tiene reglas sobre cómo se evalúa. Por ejemplo, una expresión literal como 1 generará un valor constante, mientras que la expresión a + b tomará los valores resultantes generados al evaluar otras dos expresiones (a y b), y los sumará en función de un conjunto de reglas.

Entornos y variables

Las expresiones se evalúan dentro de un entorno determinado. Un entorno es un conjunto de valores con nombre, denominados variables. Cada variable de un entorno tiene un nombre único dentro del entorno, denominado identificador.

Una expresión de nivel superior (o raíz) se evalúa dentro del entorno global. El evaluador de expresiones proporciona el entorno global, en lugar de determinarse a partir del contenido de la expresión que se evalúa. El contenido del entorno global incluye las definiciones de la biblioteca estándar y se puede ver afectado por las exportaciones de secciones de algún conjunto de documentos. (para simplificar, en los ejemplos de esta sección se asumirá un entorno global vacío. Es decir, se supone que no hay ninguna biblioteca estándar y que no hay otras definiciones basadas en secciones).

La expresión primaria determina el entorno que se usa para evaluar una subexpresión. La mayoría de los tipos de expresión primaria evaluarán una subexpresión en el mismo entorno en el que se hayan evaluado, pero algunas usarán otro. El entorno global es el entorno primario en el que se evalúa la expresión global.

Por ejemplo, record-initializer-expression evalúa la subexpresión para cada campo con un entorno modificado. El entorno modificado incluye una variable para cada uno de los campos del registro, excepto el que se está inicializando. La inclusión de los demás campos del registro permite que los campos dependan de los valores de los campos. Por ejemplo:

[  
    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
] 

Del mismo modo, let-expression evalúa la subexpresión para cada variable con un entorno que contenga cada una de las variables de let, excepto la que se está inicializando. let-expression evalúa la expresión que sigue a "in" con un entorno que contenga todas las variables:

let 

    x = 1,          // environment: y, z 
    y = 2,          // environment: x, z 
    z = x + y       // environment: x, y
in
    x + y + z       // environment: x, y, z

(Tanto record-initializer-expression como let-expression definen realmente dos entornos, y uno incluye la variable que se inicializa. Esto resulta útil para las definiciones recursivas avanzadas y se describe en Referencias de identificador).

Para crear los entornos de las subexpresiones, las variables nuevas se "mezclan" con las del entorno primario. En el ejemplo siguiente se muestran los entornos para los registros anidados:

[
    a = 
    [ 

        x = 1,      // environment: b, y, z 
        y = 2,      // environment: b, x, z 
        z = x + y   // environment: b, x, y 
    ], 
    b = 3           // environment: a
]  

En el ejemplo siguiente se muestran los entornos para un registro anidado dentro de una instrucción let:

Let
    a =
    [
        x = 1,       // environment: b, y, z 
        y = 2,       // environment: b, x, z 
        z = x + y    // environment: b, x, y 
    ], 
    b = 3            // environment: a 
in 
    a[z] + b         // environment: a, b

La combinación de variables con un entorno puede introducir un conflicto entre las variables (ya que cada variable de un entorno debe tener un nombre único). El conflicto se resuelve de la siguiente manera: si el nombre de una variable nueva que se va a combinar es igual que el de una variable existente en el entorno primario, la variable nueva tendrá prioridad en el nuevo entorno. En el ejemplo siguiente, la variable interna x (la que está más profundamente anidada) tendrá prioridad sobre la variable externa x.

[
    a =
    [ 
        x = 1,       // environment: b, x (outer), y, z 
        y = 2,       // environment: b, x (inner), z 
        z = x + y    // environment: b, x (inner), y 
    ], 
    b = 3,           // environment: a, x (outer) 
    x = 4            // environment: a, b
]  

Referencias de identificador

Una referencia de identificador se usa para hacer referencia a una variable dentro de un entorno.

identifier-expression:
      identifier-reference
identifier-reference:
      exclusive-identifier-reference
      inclusive-identifier-reference

La forma más sencilla de referencia de identificador es exclusive-identifier-reference:

exclusive-identifier-reference:
      identifier

Es un error que un elemento exclusive-identifier-reference haga referencia a una variable que no forma parte del entorno de la expresión en la que aparece el identificador.

Es un error que un elemento exclusive-identifier-reference haga referencia a un identificador que se está inicializando actualmente, si el identificador al que se hace referencia está definido en un elemento record-initializer-expression o let-expression. En su lugar, se puede usar inclusive-identifier-reference para obtener acceso al entorno que incluye el identificador que se va a inicializar. Si se usa un elemento inclusive-identifier-reference en cualquier otra situación, este equivale a un elemento exclusive-identifier-reference.

inclusive-identifier-reference:
      @identifier

Esto resulta útil a la hora de definir funciones recursivas, ya que el nombre de la función normalmente no se encontraría en el ámbito.

[ 
    Factorial = (n) =>
        if n <= 1 then
            1
        else
            n * @Factorial(n - 1),  // @ is scoping operator

    x = Factorial(5) 
]

Como sucede con record-initializer-expression, inclusive-identifier-reference se puede usar dentro de let-expression para acceder al entorno que incluye el identificador que se inicializa.

Orden de evaluación

Considere la expresión siguiente que inicializa un registro:

[ 
    C = A + B, 
    A = 1 + 1, 
    B = 2 + 2 
]

Cuando se evalúa, esta expresión genera el siguiente valor de registro:

[ 
    C = 6, 
    A = 2, 
    B = 4 
]

La expresión indica que para realizar el cálculo A + B para el campo C, se deben conocer los valores de los campos A y B. Este es un ejemplo de una ordenación de dependencias de cálculos proporcionada por una expresión. El evaluador de M se atiene a la ordenación de dependencias proporcionada por las expresiones, pero es libre de realizar los cálculos restantes en el orden que elija. Por ejemplo, el orden de cálculo podría ser este:

A = 1 + 1 
B = 2 + 2 
C = A + B

O bien, podría ser:

B = 2 + 2 
A = 1 + 1 
C = A + B

O bien, ya que como A y B no dependen entre sí, se pueden calcular de manera simultánea:

    B = 2 + 2concurrently withA = 1 + 1
    C = A + B

Efectos secundarios

Al permitir que un evaluador de expresiones calcule de forma automática el orden de los cálculos para los casos en los que no hay dependencias explícitas indicadas por la expresión, es un modelo de cálculo sencillo y eficaz.

Pero se basa en la capacidad de reordenar los cálculos. Como las expresiones pueden llamar a funciones, y esas funciones podrían observar un estado externo a la expresión mediante la emisión de consultas externas, es posible construir un escenario en el que el orden del cálculo sea importante, pero que no se capture en el orden parcial de la expresión. Por ejemplo, una función puede leer el contenido de un archivo. Si se llama a esa función varias veces, se pueden observar cambios externos en ese archivo y, por tanto, la reordenación puede producir diferencias tangibles en el comportamiento del programa. En función del orden de evaluación observado para la corrección de una expresión de M, se produce una dependencia de determinadas opciones de implementación que pueden variar de un evaluador a otro, o incluso dentro del mismo evaluador en circunstancias variables.

Inmutabilidad

Una vez que se ha calculado un valor, es inmutable, lo que significa que ya no se puede cambiar. Esto simplifica el modelo para evaluar una expresión y facilita el razonamiento sobre el resultado, ya que no es posible cambiar un valor una vez que se haya usado para evaluar un elemento posterior de la expresión. Por ejemplo, un campo de registro solo se calcula cuando es necesario. Pero una vez calculado, permanece fijo durante la vigencia del registro. Incluso si se ha producido un error al intentar calcular el campo, ese mismo error se volverá a producir en cada intento de acceso a ese campo de registro.

Una excepción importante a la regla inmutable una vez calculada se aplica a los valores de lista, tabla y binarios, que tienen semántica de transmisión. La semántica de transmisión permite a M transformar todos a la vez conjuntos de datos que no caben en la memoria. Con la transmisión por secuencias, los valores devueltos al enumerar una tabla, lista o valor binario determinado se producen según demanda cada vez que se solicitan. Dado que las expresiones que definen los valores enumerados se evalúan cada vez que se enumeran, el resultado que producen puede ser diferente en varias enumeraciones. Esto no significa que varias enumeraciones siempre den como resultado valores diferentes, solo que pueden ser diferentes si la fuente de datos o la lógica M que se utiliza no es determinista.

Además, tenga en cuenta que la aplicación de funciones no es lo mismo que la construcción de valores. Las funciones de biblioteca pueden exponer el estado externo (como la hora actual o los resultados de una consulta en una base de datos que evoluciona con el tiempo) y representarlos como no deterministas. Aunque las funciones definidas en M, como tales, no exponen ningún comportamiento no determinista, pueden hacerlo si se definen para invocar a otras funciones que sean no deterministas.

En M, un origen final del no determinismo son los errores. Cuando se producen, los errores detienen las evaluaciones (hasta el nivel en el que se controlan mediante una expresión try). Normalmente, no se puede ver si a + b ha provocado la evaluación de a antes de b o de b antes de a (se omite la simultaneidad por motivos de simplicidad). Pero si la subexpresión que se ha evaluado en primer lugar genera un error, se puede determinar cuál de las dos expresiones se ha evaluado en primer lugar.