Compartilhar via


Conceitos básicos

Esta seção aborda os conceitos básicos que aparecem nas seções posteriores.

Valores

Apenas um dado é chamado de valor. Falando de maneira mais ampla, há duas categorias gerais de valores: valores primitivos, que são atômicos, e valores estruturados, que são construídos com valores primitivos e outros valores estruturados. Por exemplo, os valores

1 
true
3.14159 
"abc"

são primitivos, pois não são compostos por outros valores. Por outro lado, os valores

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

são construídos usando valores primitivos e, no caso do registro, outros valores estruturados.

Expressões

Uma expressão é uma fórmula usada para construir valores. Uma expressão pode ser formada usando diversas construções sintáticas. Veja a seguir alguns exemplos de expressões. Cada linha é uma expressão separada.

"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"

A forma de expressão mais simples, como visto acima, é um literal que representa um valor.

Expressões mais complexas são criadas com base em outras expressões, chamadas de subexpressões. Por exemplo:

1 + 2

Na verdade, a expressão acima é composta por três expressões. Os literais 1 e 2 são subexpressões da expressão pai 1 + 2.

Executar o algoritmo definido pelas construções sintáticas usadas em uma expressão é chamado de avaliar a expressão. Cada tipo de expressão tem regras de como é avaliada. Por exemplo, uma expressão literal como 1 produzirá um valor constante, enquanto a expressão a + b usará os valores resultantes produzidos pela avaliação de duas outras expressões (a e b) e as adicionará de acordo com algum conjunto de regras.

Ambientes e variáveis

As expressões são avaliadas em um determinado ambiente. Um ambiente é um conjunto de valores nomeados, chamados variáveis. Cada variável em um ambiente tem um nome exclusivo dentro do ambiente, chamado de identificador.

Uma expressão de nível superior (ou raiz) é avaliada dentro do ambiente global. O ambiente global é fornecido pelo avaliador da expressão, em vez de ser determinado com base no conteúdo da expressão que está sendo avaliada. O conteúdo do ambiente global inclui as definições de biblioteca padrão e pode ser afetado por exportações de seções de um conjunto de documentos. (Para simplificar, os exemplos nesta seção vão pressupor um ambiente global vazio. Ou seja, presume-se que não haja biblioteca padrão e que não haja outras definições baseadas em seção.)

O ambiente usado para avaliar uma subexpressão é determinado pela expressão pai. A maioria dos tipos de expressão pai avalia uma subexpressão dentro do mesmo ambiente em que foi avaliada, mas alguns usam um ambiente diferente. O ambiente global é o ambiente pai no qual a expressão global é avaliada.

Por exemplo, a expressão-inicializadora-de-registro avalia a subexpressão de cada campo com um ambiente modificado. O ambiente modificado inclui uma variável para cada um dos campos do registro, exceto pelo que está sendo inicializado. Incluir os outros campos do registro permite que os campos dependam dos valores dos campos. Por exemplo:

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

Da mesma forma, a expressão-let avalia a subexpressão de cada variável com um ambiente que contém cada uma das variáveis de ler, exceto pela que está sendo inicializada. A expressão-let avalia a expressão que segue in com um ambiente que contém todas as variáveis:

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

(Acontece que tanto a expressão-inicializadora-de-registro quanto a expressão-let na verdade definem dois ambientes, um dos quais inclui a variável que está sendo inicializada). Isso é útil para definições recursivas avançadas e é abordado em Referências de identificador.

Para formar os ambientes para as subexpressões, as novas variáveis são "mescladas" com as variáveis do ambiente pai. O exemplo a seguir mostra os ambientes para registros aninhados:

[
    a = 
    [ 

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

O exemplo a seguir mostra os ambientes para um registro aninhado em um 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

Mesclar variáveis com um ambiente pode introduzir um conflito entre variáveis (uma vez que cada variável em um ambiente precisa ter um nome exclusivo). O conflito é resolvido da seguinte maneira: se o nome de uma nova variável que está sendo mesclada for igual ao de uma variável existente no ambiente pai, a nova variável terá precedência no novo ambiente. No exemplo a seguir, a variável interna (aninhada mais profundamente) x terá precedência sobre a variável 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
]  

Referências de identificador

Uma referência-de-identificador é usada para fazer referência a uma variável em um ambiente.

expressão-de-identificador:
      referência-de-identificador
referência-de-identificador:
      referência-de-identificador-exclusiva
      referência-de-identificador-inclusiva

A forma mais simples de referência de identificador é uma referência-de-identificador-exclusiva:

referência-de-identificador-exclusiva:
      identificador

É um erro uma referência-de-identificador-exclusiva se referir a uma variável que não faz parte do ambiente da expressão em que o identificador aparece.

Esse é um erro que ocorre quando uma referência de identificador exclusivo menciona um identificador que está sendo inicializado no presente momento nos casos em que esse identificador referenciado está definido dentro de uma expressão de inicialização de registro ou expressão de let. Como alternativa, uma referência de identificador inclusivo pode ser usada para ter acesso ao ambiente que inclui o identificador sendo inicializado. Se uma referência de identificador inclusivo for usada em qualquer outra situação, ela será equivalente a uma referência de identificador exclusivo.

referência-de-identificador-inclusiva:
      @ identificador

Isso é útil ao definir funções recursivas, pois o nome da função normalmente não estaria no escopo.

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

    x = Factorial(5) 
]

Assim como ocorre com uma expressão-inicializadora-de-registro, uma referência-de-identificador-inclusiva pode ser usada dentro de uma expressão-let para acessar o ambiente que inclui o identificador que está sendo inicializado.

Ordem de avaliação

Considere a seguinte expressão que inicializa um registro:

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

Quando avaliada, essa expressão produz o seguinte valor de registro:

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

A expressão declara que, para executar o cálculo A + B para o campo C, os valores dos campos A e B devem ser conhecidos. Este é um exemplo da ordenação de dependências de cálculos fornecida por uma expressão. O avaliador de M segue a ordenação de dependências fornecida pelas expressões, mas é livre para executar os cálculos restantes em qualquer ordem escolhida. Por exemplo, a ordem de computação poderia ser:

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

Ou poderia ser:

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

Ou, como não dependem uns dos outros, A e B podem ser computados simultaneamente:

    B = 2 + 2 simultaneamente com A = 1 + 1
    C = A + B

Efeitos colaterais

Permitir que um avaliador de expressão compute automaticamente a ordem dos cálculos para casos em que não há dependências explícitas declaradas pela expressão é um modelo de computação simples e poderoso.

No entanto, ele depende da capacidade de reordenar cálculos. Como as expressões podem chamar funções, e essas funções podem observar o estado externo à expressão emitindo consultas externas, é possível construir um cenário no qual a ordem de cálculo é importante, mas não é capturada na ordem parcial da expressão. Por exemplo, uma função pode ler o conteúdo de um arquivo. Se essa função for chamada repetidamente, as alterações externas nesse arquivo poderão ser observadas e, portanto, a reordenação poderá causar diferenças observáveis no comportamento do programa. Dependendo da ordem de avaliação observada, para que haja exatidão de uma expressão M, há uma dependência de determinadas opções de implementação que podem variar de um avaliador para outro ou pode até mesmo variar no mesmo avaliador em diferentes circunstâncias.

Imutabilidade

Depois que um valor é calculado, ele é imutável, o que significa que não pode mais ser alterado. Isso simplifica o modelo para avaliar uma expressão e facilita a interpretação do resultado, uma vez que não é possível alterar um valor após ele ser usado para avaliar uma parte posterior da expressão. Por exemplo, um campo de registro só é computado quando necessário. No entanto, após computado, ele permanece fixo durante o tempo de vida do registro. Mesmo que a tentativa de computar o campo tenha gerado um erro, esse erro será gerado novamente a cada tentativa de acessar esse campo do registro.

Uma exceção importante à regra immutable-once-calculated aplica-se a valores de lista, tabela e binário, que têm semântica de streaming. A semântica de streaming permite que M transforme conjuntos de dados que não cabem na memória de uma só vez. Com o streaming, os valores retornados ao enumerar um determinado valor de tabela, lista ou binário são produzidos sob demanda sempre que são solicitados. Como as expressões que definem os valores enumerados são avaliadas sempre que são enumeradas, a saída que produzem pode ser diferente entre várias enumerações. Isso não significa que várias enumerações sempre resultem em valores diferentes, apenas que eles podem ser diferentes se a fonte de dados ou a lógica M que está sendo usada não for determinística.

Além disso, observe que a aplicação da função não é o mesmo que a construção do valor. As funções de biblioteca podem expor o estado externo (como a hora atual ou os resultados de uma consulta em um banco de dados que evolui ao longo do tempo), tornando-as não determinísticas. Embora dessa maneira as funções definidas em M não exponham nenhum comportamento não determinístico desse tipo, elas poderiam se fossem definidas para invocar outras funções não determinísticas.

A última fonte de não determinismo em M são os erros. Os erros param as avaliações quando ocorrem (até o nível em que são tratados por uma expressão try). Normalmente, não é observável se a + b causou a avaliação de a antes de b ou de b antes de a (ignorando a simultaneidade aqui para simplificar). No entanto, se a subexpressão que foi avaliada primeiro gerar um erro, será possível determinar qual das duas expressões foi avaliada primeiro.