O que Expression.Quote () faz que Expression.Constant () ainda não pode fazer?

97

Observação: estou ciente da pergunta anterior “ Qual é o propósito do método Expression.Quote do LINQ? , Mas se continuar a ler verá que não responde à minha pergunta.

Eu entendo qual é o propósito declarado de Expression.Quote(). No entanto, Expression.Constant()pode ser usado para a mesma finalidade (além de todas as finalidades para as quais Expression.Constant()já é usado). Portanto, não entendo por que isso Expression.Quote()é necessário.

Para demonstrar isso, escrevi um exemplo rápido onde normalmente se usaria Quote(veja a linha marcada com pontos de exclamação), mas usei em Constantvez disso e funcionou igualmente bem:

string[] array = { "one", "two", "three" };

// This example constructs an expression tree equivalent to the lambda:
// str => str.AsQueryable().Any(ch => ch == 'e')

Expression<Func<char, bool>> innerLambda = ch => ch == 'e';

var str = Expression.Parameter(typeof(string), "str");
var expr =
    Expression.Lambda<Func<string, bool>>(
        Expression.Call(typeof(Queryable), "Any", new Type[] { typeof(char) },
            Expression.Call(typeof(Queryable), "AsQueryable",
                            new Type[] { typeof(char) }, str),
            // !!!
            Expression.Constant(innerLambda)    // <--- !!!
        ),
        str
    );

// Works like a charm (prints one and three)
foreach (var str in array.AsQueryable().Where(expr))
    Console.WriteLine(str);

A saída de expr.ToString()é a mesma para ambos (se eu uso Constantou Quote).

Dadas as observações acima, parece que Expression.Quote()é redundante. O compilador C # poderia ter sido feito para compilar expressões lambda aninhadas em uma árvore de expressão envolvendo em Expression.Constant()vez de Expression.Quote(), e qualquer provedor de consulta LINQ que deseja processar árvores de expressão em alguma outra linguagem de consulta (como SQL) poderia procurar um ConstantExpressiontipo com em Expression<TDelegate>vez de a UnaryExpressioncom o Quotetipo de nó especial e todo o resto seria o mesmo.

o que estou perdendo? Por que foi Expression.Quote()e o Quotetipo de nó especial para UnaryExpressioninventado?

Timwi
fonte

Respostas:

189

Resposta curta:

O operador quote é um operador que induz a semântica de fechamento em seu operando . Constantes são apenas valores.

Aspas e constantes têm significados diferentes e, portanto, têm representações diferentes em uma árvore de expressão . Ter a mesma representação para duas coisas muito diferentes é extremamente confuso e sujeito a erros.

Resposta longa:

Considere o seguinte:

(int s)=>(int t)=>s+t

O lambda externo é uma fábrica de somadores vinculados ao parâmetro do lambda externo.

Agora, suponha que desejamos representar isso como uma árvore de expressão que será posteriormente compilada e executada. Qual deve ser o corpo da árvore de expressão? Depende se você deseja que o estado compilado retorne um delegado ou uma árvore de expressão.

Vamos começar descartando o caso desinteressante. Se desejarmos que ele retorne um delegado, a questão de usar Quote ou Constant é um ponto discutível:

        var ps = Expression.Parameter(typeof(int), "s");
        var pt = Expression.Parameter(typeof(int), "t");
        var ex1 = Expression.Lambda(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt),
            ps);

        var f1a = (Func<int, Func<int, int>>) ex1.Compile();
        var f1b = f1a(100);
        Console.WriteLine(f1b(123));

O lambda tem um lambda aninhado; o compilador gera o lambda interno como um delegado para uma função fechada sobre o estado da função gerada para o lambda externo. Não precisamos mais considerar este caso.

Suponha que desejamos que o estado compilado retorne uma árvore de expressão do interior. Existem duas maneiras de fazer isso: a maneira mais fácil e a mais difícil.

A maneira mais difícil é dizer isso em vez de

(int s)=>(int t)=>s+t

o que realmente queremos dizer é

(int s)=>Expression.Lambda(Expression.Add(...

E então gere a árvore de expressão para isso , produzindo esta bagunça :

        Expression.Lambda(
            Expression.Call(typeof(Expression).GetMethod("Lambda", ...

blá blá blá, dezenas de linhas de código de reflexão para fazer o lambda. O objetivo do operador quote é dizer ao compilador da árvore de expressão que queremos que o lambda fornecido seja tratado como uma árvore de expressão, não como uma função, sem ter que gerar explicitamente o código de geração da árvore de expressão .

A maneira mais fácil é:

        var ex2 = Expression.Lambda(
            Expression.Quote(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f2a = (Func<int, Expression<Func<int, int>>>)ex2.Compile();
        var f2b = f2a(200).Compile();
        Console.WriteLine(f2b(123));

E, de fato, se você compilar e executar esse código, obterá a resposta certa.

Observe que o operador de aspas é o operador que induz a semântica de fechamento no lambda interno que usa uma variável externa, um parâmetro formal do lambda externo.

A questão é: por que não eliminar o Quote e fazer com que isso faça a mesma coisa?

        var ex3 = Expression.Lambda(
            Expression.Constant(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f3a = (Func<int, Expression<Func<int, int>>>)ex3.Compile();
        var f3b = f3a(300).Compile();
        Console.WriteLine(f3b(123));

A constante não induz a semântica de fechamento. Por que deveria? Você disse que isso era uma constante . É apenas um valor. Deve ser perfeito conforme entregue ao compilador; o compilador deve ser capaz de gerar apenas um dump desse valor para a pilha onde ele é necessário.

Visto que não há fechamento induzido, se você fizer isso, obterá uma exceção "variável 's' do tipo 'System.Int32' não está definido" na chamada.

(À parte: acabei de revisar o gerador de código para a criação de delegado de árvores de expressão entre aspas e, infelizmente, um comentário que coloquei no código em 2006 ainda está lá. FYI, o parâmetro externo içado é instantâneo em uma constante quando o a árvore de expressão é reificada como um delegado pelo compilador de tempo de execução. Há uma boa razão para eu ter escrito o código dessa maneira, que não me lembro neste exato momento, mas tem o efeito colateral desagradável de introduzir o fechamento sobre os valores dos parâmetros externos ao invés de fechamento sobre variáveis. Aparentemente, a equipe que herdou esse código decidiu não corrigir essa falha, então se você está contando com a mutação de um parâmetro externo fechado sendo observado em um lambda interno compilado entre aspas, você ficará desapontado. No entanto, uma vez que é uma prática de programação muito ruim (1) alterar um parâmetro formal e (2) confiar na mutação de uma variável externa, eu recomendaria que você altere seu programa para não usar essas duas práticas de programação ruins, em vez de esperando por uma correção que não parece estar próxima. Desculpas pelo erro.)

Então, para repetir a pergunta:

O compilador C # poderia ter sido feito para compilar expressões lambda aninhadas em uma árvore de expressão envolvendo Expression.Constant () em vez de Expression.Quote (), e qualquer provedor de consulta LINQ que deseja processar árvores de expressão em alguma outra linguagem de consulta (como SQL ) poderia procurar uma ExpressãoConstante com o tipo Expressão em vez de uma ExpressãoUnária com o tipo de nó Citação especial e todo o resto seria o mesmo.

Você está certo. Nós poderia codificar a informação semântica que significa "induzir a semântica de fechamento sobre este valor" por usando o tipo da expressão constante como uma bandeira .

"Constante" teria então o significado "use este valor constante, a menos que o tipo seja um tipo de árvore de expressão e o valor seja uma árvore de expressão válida, nesse caso, use o valor que é a árvore de expressão resultante da reescrita de interior da árvore de expressão fornecida para induzir a semântica de fechamento no contexto de quaisquer lambdas externos em que possamos estar agora.

Mas por que teria que fazer essa coisa louca? O operador quote é um operador extremamente complicado e deve ser usado explicitamente se você for usá-lo. Você está sugerindo que, para ser parcimonioso sobre não adicionar um método de fábrica extra e tipo de nó entre as várias dezenas já existentes, que adicionemos um caso estranho bizarro às constantes, de modo que as constantes às vezes sejam logicamente constantes e às vezes sejam reescritas lambdas com semântica de fechamento.

Também teria o efeito um tanto estranho de constante não significar "usar este valor". Suponha que por alguma razão bizarra você queira que o terceiro caso acima compile uma árvore de expressão em um delegado que distribui uma árvore de expressão que tem uma referência não reescrita a uma variável externa. Por quê? Talvez porque você está testando seu compilador e deseja apenas passar a constante para que possa realizar alguma outra análise depois. Sua proposta tornaria isso impossível; qualquer constante que seja do tipo de árvore de expressão seria reescrita independentemente. Tem-se uma expectativa razoável de que "constante" significa "use este valor". "Constante" é um nó "faça o que eu digo". O processador constante ' dizer com base no tipo.

E observe, é claro, que agora você está colocando o fardo do entendimento (isto é, entender que constante tem semântica complicada que significa "constante" em um caso e "induzir semântica de fechamento" com base em um sinalizador que está no sistema de tipos ) em cada provedor que faz análise semântica de uma árvore de expressão, não apenas em provedores Microsoft. Quantos desses fornecedores terceirizados errariam?

"Quote" está acenando uma grande bandeira vermelha que diz "ei, amigo, olhe aqui, sou uma expressão lambda aninhada e tenho uma semântica maluca se estiver fechado em uma variável externa!" enquanto "Constante" é dizer "Não sou nada mais do que um valor; use-me como achar melhor." Quando algo é complicado e perigoso, queremos fazê-lo acenar com bandeiras vermelhas, não escondendo esse fato fazendo o usuário vasculhar o sistema de tipos para descobrir se esse valor é especial ou não.

Além disso, a ideia de que evitar a redundância é até mesmo uma meta é incorreta. Claro, evitar redundâncias desnecessárias e confusas é uma meta, mas a maioria das redundâncias é uma coisa boa; redundância cria clareza. Novos métodos de fábrica e tipos de nós são baratos . Podemos fazer quantos forem necessários para que cada um represente uma operação de forma clara. Não precisamos recorrer a truques desagradáveis ​​como "isso significa uma coisa, a menos que este campo seja definido para essa coisa, caso em que significa outra coisa."

Eric Lippert
fonte
11
Estou envergonhado agora porque não pensei na semântica de encerramento e não consegui testar um caso em que o lambda aninhado captura um parâmetro de um lambda externo. Se eu tivesse feito isso, teria notado a diferença. Muito obrigado novamente por sua resposta.
Timwi,
19

Esta questão já recebeu uma excelente resposta. Além disso, gostaria de apontar um recurso que pode ser útil com perguntas sobre árvores de expressão:

é foi um projeto CodePlex da Microsoft chamado Dynamic Language Runtime. Sua documentação inclui o documento intitulado,"Expression Trees v2 Spec", que é exatamente isso: A especificação para árvores de expressão LINQ no .NET 4.

Atualização: CodePlex está extinto. A especificação Expression Trees v2 (PDF) foi movida para o GitHub .

Por exemplo, diz o seguinte sobre Expression.Quote:

4.4.42 Cotação

Use Quote em UnaryExpressions para representar uma expressão que tenha um valor "constante" do tipo Expression. Ao contrário de um nó Constant, o nó Quote trata especialmente os nós ParameterExpression. Se um nó ParameterExpression contido declarar um local que seria fechado na expressão resultante, Quote substituirá ParameterExpression em seus locais de referência. No tempo de execução, quando o nó Quote é avaliado, ele substitui as referências da variável de fechamento para os nós de referência ParameterExpression e, em seguida, retorna a expressão entre aspas. [...] (p. 63-64)

stakx suporta GoFundMonica
fonte
1
Excelente resposta do tipo ensine um homem a pescar. Gostaria apenas de acrescentar que a documentação foi movida e agora está disponível em docs.microsoft.com/en-us/dotnet/framework/… . O documento citado, especificamente, está no GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relativamente_random
3

Depois dessa resposta realmente excelente, fica claro quais são as semânticas. Não está tão claro por que eles são projetados dessa forma, considere:

Expression.Lambda(Expression.Add(ps, pt));

Quando este lambda é compilado e chamado, ele avalia a expressão interna e retorna o resultado. A expressão interna aqui é uma adição, portanto, o ps + pt é avaliado e o resultado é retornado. Seguindo essa lógica, a seguinte expressão:

Expression.Lambda(
    Expression.Lambda(
              Expression.Add(ps, pt),
            pt), ps);

deve retornar uma referência de método compilado lambda interno quando o lambda externo é invocado (porque dizemos que lambda é compilado para uma referência de método). Então, por que precisamos de uma cotação ?! Para diferenciar o caso em que a referência do método é retornada versus o resultado dessa chamada de referência.

Especificamente:

let f = Func<...>
return f; vs. return f(...);

Por algum motivo, os designers do .Net escolheram Expression.Quote (f) para o primeiro caso e f simples para o segundo. Na minha opinião, isso causa uma grande confusão, uma vez que na maioria das linguagens de programação retornar um valor é direto (sem necessidade de Quote ou qualquer outra operação), mas a invocação requer escrita extra (parênteses + argumentos), que se traduz em algum tipo de invocar no nível MSIL. Os designers .Net tornaram o oposto para as árvores de expressão. Seria interessante saber o motivo.

Konstantin Triger
fonte
-2

Acho que o ponto aqui é a expressividade da árvore. A expressão constante que contém o delegado, na verdade, contém apenas um objeto que por acaso é um delegado. Isso é menos expressivo do que uma divisão direta em uma expressão unária e binária.

Joe Wood
fonte
É isso? Que expressividade isso adiciona, exatamente? O que você pode “expressar” com aquela ExpressãoUnária (que também é um tipo estranho de expressão) que você ainda não conseguia expressar com Expressão Constante?
Timwi de