Conceitos básicos
Esta seção discute os conceitos básicos que aparecem ao longo das seções subsequentes.
Valores
Um único dado é chamado de valor. Em termos gerais, existem duas categorias gerais de valores: valores primitivos, que são atômicos, e valores estruturados, que são construídos a partir de valores primitivos e outros valores estruturados. Por exemplo, os valores
1
true
3.14159
"abc"
são primitivos na medida em que não são constituídos por outros valores. Por outro lado, os valores
{1, 2, 3}
[ A = {1}, B = {2}, C = {3} ]
são construídos utilizando valores primitivos e, no caso do registo, outros valores estruturados.
Expressões
Uma expressão é uma fórmula usada para construir valores. Uma expressão pode ser formada usando uma variedade de construções sintáticas. Seguem-se 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 mais simples de expressão, como visto acima, é um literal que representa um valor.
Expressões mais complexas são construídas a partir de outras expressões, chamadas subexpressões. Por exemplo:
1 + 2
A expressão acima é, na verdade, composta por três expressões. Os 1
literais e 2
são subexpressões da expressão 1 + 2
pai.
A execução do algoritmo definido pelas construções sintáticas usadas em uma expressão é chamada de avaliação da expressão. Cada tipo de expressão tem regras para a forma como é avaliada. Por exemplo, uma expressão literal como 1
produzirá um valor constante, enquanto a expressão a + b
tomará 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 dentro de 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 identificador.
Uma expressão de nível superior (ou raiz) é avaliada dentro do ambiente global. O ambiente global é fornecido pelo avaliador de expressões em vez de ser determinado a partir do 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 algum conjunto de documentos. (Para simplificar, os exemplos nesta seção assumirão um ambiente global vazio. Ou seja, assume-se que não existe uma biblioteca padrão e que não existem outras definições baseadas em secções.)
O ambiente usado para avaliar uma subexpressão é determinado pela expressão pai. A maioria dos tipos de expressão pai avaliará uma subexpressão dentro do mesmo ambiente em que foram avaliados, mas alguns usarão um ambiente diferente. O ambiente global é o ambiente pai dentro do qual a expressão global é avaliada.
Por exemplo, o record-initializer-expression 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 o que está sendo inicializado. A inclusão dos 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, o let-expression avalia a subexpressão para cada variável com um ambiente contendo cada uma das variáveis do let, exceto a que está sendo inicializada. O let-expression avalia a expressão após o in com um ambiente contendo 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 record-initializer-expression quanto let-expression 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 no 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 deve ter um nome exclusivo). O conflito é resolvido da seguinte forma: se o nome de uma nova variável que está sendo mesclada for o mesmo que uma variável existente no ambiente pai, a nova variável terá precedência no novo ambiente. No exemplo a seguir, a variável x
interna (mais profundamente aninhada) terá precedência sobre a variável x
externa.
[
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 identificadores
Uma referência de identificador é usada para se referir a uma variável dentro de um ambiente.
expressão-identificador:
Referência-identificador
Referência-identificador:
exclusive-identifier-reference
inclusive-identifier-reference
A forma mais simples de referência de identificador é uma referência de identificador exclusivo:
exclusive-identifier-reference:
identificador
É um erro para uma referência de identificador exclusivo referir-se a uma variável que não faz parte do ambiente da expressão na qual o identificador aparece.
É um erro para uma referência de identificador exclusivo referir-se a um identificador que está sendo inicializado no momento se o identificador referenciado estiver definido dentro de um record-initializer-expression ou let-expression. Em vez disso, uma referência de identificador inclusivo pode ser usada para obter acesso ao ambiente que inclui o identificador que está 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.
inclusive-identifier-reference:
@
identificador
Isso é útil ao definir funções recursivas, uma vez que 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)
]
Como em uma expressão de inicializador de registro, uma referência de identificador inclusivo 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, esta expressão produz o seguinte valor de registo:
[
C = 6,
A = 2,
B = 4
]
A expressão afirma que, para realizar o A + B
cálculo para campo C
, os valores de campo A
e campo B
devem ser conhecidos. Este é um exemplo de uma ordenação de dependência de cálculos que é fornecida por uma expressão. O avaliador M obedece à ordem de dependência fornecida pelas expressões, mas é livre para executar os cálculos restantes em qualquer ordem que escolher. Por exemplo, a ordem de cálculo pode ser:
A = 1 + 1
B = 2 + 2
C = A + B
Ou poderia ser:
B = 2 + 2
A = 1 + 1
C = A + B
Ou, uma vez que A
não B
dependem uns dos outros, podem ser calculados simultaneamente:
B = 2 + 2
concomitantemente com A = 1 + 1
C = A + B
Efeitos colaterais
Permitir que um avaliador de expressões calcule 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, depende da capacidade de reordenar os 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 onde a ordem de cálculo importa, 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 é chamada repetidamente, então as alterações externas nesse arquivo podem ser observadas e, portanto, a reordenação pode causar diferenças observáveis no comportamento do programa. Dependendo dessa avaliação observada, a ordenação da correção de uma expressão M causa uma dependência de escolhas de implementação particulares que podem variar de um avaliador para o seguinte, ou podem mesmo variar no mesmo avaliador em circunstâncias variáveis.
Imutabilidade
Uma vez que um valor tenha sido calculado, ele é imutável, o que significa que não pode mais ser alterado. Isso simplifica o modelo de avaliação de uma expressão e torna mais fácil raciocinar sobre o resultado, uma vez que não é possível alterar um valor depois que ele foi usado para avaliar uma parte subsequente da expressão. Por exemplo, um campo de registro só é computado quando necessário. No entanto, uma vez calculado, permanece fixo durante o tempo de vida do registo. Mesmo que a tentativa de calcular o campo gere um erro, esse mesmo erro será gerado novamente em cada tentativa de acessar esse campo de registro.
Uma exceção importante à regra imutável-uma vez calculada aplica-se a valores de lista, tabela e binários, 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 uma determinada tabela, lista ou valor binário são produzidos sob demanda cada vez que são solicitados. Como as expressões que definem os valores enumerados são avaliadas cada vez que são enumeradas, a saída que elas produzem pode ser diferente em várias enumerações. Isso não significa que várias enumerações sempre resultam em valores diferentes, apenas que elas podem ser diferentes se a fonte de dados ou a lógica M que está sendo usada for não determinística.
Além disso, observe que o aplicativo de função não é o mesmo que a construção de valor. As funções de biblioteca podem expor o estado externo (como a hora atual ou os resultados de uma consulta em relação a um banco de dados que evolui ao longo do tempo), tornando-os não determinísticos. Embora as funções definidas em M não possam, como tal, expor qualquer comportamento não determinístico, elas podem, se forem definidas, invocar outras funções que são não-determinísticas.
Uma última fonte de não-determinismo em M são os erros. Os erros interrompem as avaliações quando ocorrem (até o nível em que são manipulados por uma expressão try). Normalmente não é observável se a + b
causou a avaliação de antes b
ou b
antes a
(a
ignorando a simultaneidade aqui para simplificar). No entanto, se a subexpressão que foi avaliada primeiro gera um erro, então pode ser determinado qual das duas expressões foi avaliada primeiro.