Por que exatamente é eval mau?

141

Eu sei que os programadores de Lisp e Scheme geralmente dizem que isso evaldeve ser evitado, a menos que seja estritamente necessário. Eu vi a mesma recomendação para várias linguagens de programação, mas ainda não vi uma lista de argumentos claros contra o uso de eval. Onde posso encontrar uma conta dos possíveis problemas do uso eval?

Por exemplo, conheço os problemas da GOTOprogramação processual (torna os programas ilegíveis e difíceis de manter, dificulta a localização de problemas de segurança etc.), mas nunca vi argumentos contra isso eval.

Curiosamente, os mesmos argumentos contra GOTOdevem ser válidos contra continuações, mas vejo que os Schemers, por exemplo, não dirão que continuações são "más" - você deve ter cuidado ao usá-las. Eles são muito mais propensos a desaprovar o código usando do evalque o código usando continuações (tanto quanto eu posso ver - eu posso estar errado).

Jay
fonte
5
eval não é mau, mas o mal é o que eval faz
Anurag
9
@ yar - acho que seu comentário indica uma visão de mundo muito centralizada em objeto de despacho único. Provavelmente é válido para a maioria das linguagens, mas seria diferente no Common Lisp, onde os métodos não pertencem às classes e ainda mais diferente no Clojure, onde as classes são suportadas apenas por funções de interoperabilidade Java. Jay marcou essa pergunta como Esquema, que não possui nenhuma noção interna de classes ou métodos (várias formas de OO estão disponíveis como bibliotecas).
Zak
3
@ Zak, você está correto, só conheço os idiomas que conheço, mas mesmo se você estiver trabalhando com um documento do Word sem usar o Styles, não estará SECO. Meu objetivo era usar a tecnologia para não se repetir. OO não é universal, verdade ...
Dan Rosenstark
4
Tomei a liberdade de adicionar a tag clojure a esta pergunta, pois acredito que os usuários do Clojure podem se beneficiar da exposição às excelentes respostas postadas aqui.
Michał Marczyk
... bem, para o Clojure, pelo menos um motivo adicional se aplica: você perde a compatibilidade com o ClojureScript e seus derivados.
Charles Duffy

Respostas:

148

Existem várias razões pelas quais não se deve usar EVAL.

A principal razão para iniciantes é: você não precisa disso.

Exemplo (assumindo Common Lisp):

AVALUE uma expressão com diferentes operadores:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (eval (list op 1 2 3)))))

Isso é melhor escrito como:

(let ((ops '(+ *)))
  (dolist (op ops)
    (print (funcall op 1 2 3))))

Existem muitos exemplos em que os iniciantes que aprendem o Lisp acham que precisam EVAL, mas não precisam - já que as expressões são avaliadas e também é possível avaliar a parte da função. Na maioria das vezes, o uso de EVALmostra falta de entendimento do avaliador.

É o mesmo problema com macros. Geralmente, os iniciantes escrevem macros, onde devem escrever funções - sem entender para que servem as macros e sem entender que uma função já faz o trabalho.

Geralmente, é a ferramenta errada para o trabalho usar EVALe geralmente indica que o iniciante não entende as regras usuais de avaliação do Lisp.

Se você acha que precisa EVAL, em seguida, verificar se algo assim FUNCALL, REDUCEou APPLYpoderia ser usado em seu lugar.

  • FUNCALL - chame uma função com argumentos: (funcall '+ 1 2 3)
  • REDUCE - chame uma função em uma lista de valores e combine os resultados: (reduce '+ '(1 2 3))
  • APPLY- chamar uma função com uma lista de argumentos: (apply '+ '(1 2 3)).

P: Eu realmente preciso de avaliação ou o compilador / avaliador já é o que realmente quero?

Os principais motivos a serem evitados EVALpara usuários um pouco mais avançados:

  • você deseja garantir que seu código seja compilado, porque o compilador pode verificar se há muitos problemas e gerar um código mais rápido, às vezes MUITO MUITO MUITO (o fator 1000 ;-)) código mais rápido

  • o código que é construído e precisa ser avaliado não pode ser compilado o mais cedo possível.

  • avaliação de entrada arbitrária do usuário abre problemas de segurança

  • algum uso da avaliação EVALpode ocorrer no momento errado e criar problemas de compilação

Para explicar o último ponto com um exemplo simplificado:

(defmacro foo (a b)
  (list (if (eql a 3) 'sin 'cos) b))

Então, talvez eu queira escrever uma macro que, com base no primeiro parâmetro, use SINou COS.

(foo 3 4)faz (sin 4)e (foo 1 4)faz (cos 4).

Agora podemos ter:

(foo (+ 2 1) 4)

Isso não fornece o resultado desejado.

Pode-se reparar a macro FOOEVALuating a variável:

(defmacro foo (a b)
  (list (if (eql (eval a) 3) 'sin 'cos) b))

(foo (+ 2 1) 4)

Mas isso ainda não funciona:

(defun bar (a b)
  (foo a b))

O valor da variável simplesmente não é conhecido no momento da compilação.

Um motivo geral importante a ser evitado EVAL: geralmente é usado para hacks feios.

Rainer Joswig
fonte
3
Obrigado! Só não entendi o último ponto (avaliação na hora errada?) - você pode elaborar um pouco, por favor?
Jay
41
+1, pois essa é a resposta real - as pessoas recorrem evalsimplesmente porque não sabem que há um recurso específico de idioma ou biblioteca para fazer o que querem. Exemplo semelhante de JS: desejo obter uma propriedade de um objeto usando um nome dinâmico, então escrevo: eval("obj.+" + propName)quando poderia ter escrito obj[propName].
Daniel Earwicker
Entendo o que você quer dizer agora, Rainer! Thansk!
Jay
@Daniel "obj.+":? A última vez que verifiquei, +não é válida ao usar referências de ponto em JS.
precisa saber é o seguinte
2
@ Daniel provavelmente significava eval ("obj." + PropName), que deveria funcionar como esperado.
claj
41

eval(em qualquer idioma) não é mau da mesma maneira que uma serra elétrica não é má. É uma ferramenta. Por acaso, é uma ferramenta poderosa que, quando mal utilizada, pode cortar membros e eviscerar (metaforicamente falando), mas o mesmo pode ser dito para muitas ferramentas na caixa de ferramentas de um programador, incluindo:

  • goto e amigos
  • rosca baseada em bloqueio
  • continuações
  • macros (higiênicas ou outras)
  • ponteiros
  • exceções reiniciáveis
  • código auto-modificável
  • ... e um elenco de milhares.

Se você precisar usar alguma dessas ferramentas poderosas e potencialmente perigosas, pergunte a si mesmo três vezes "por quê?" em uma corrente. Por exemplo:

"Por que eu tenho que usar eval?" "Por causa de foo." "Por que é necessário?" "Porque ..."

Se você chegar ao final dessa cadeia e a ferramenta ainda parecer que é a coisa certa a fazer, faça-o. Documente o inferno com isso. Teste o inferno com isso. Verifique novamente a correção e a segurança repetidamente. Mas faça isso.

APENAS MINHA OPINIÃO correta
fonte
Obrigado - foi o que eu ouvi de eval antes ("pergunte a si mesmo por que"), mas eu nunca tinha ouvido ou lido quais são os problemas em potencial. Agora vejo pelas respostas aqui o que são (problemas de segurança e desempenho).
Jay,
8
E legibilidade do código. O Eval pode estragar totalmente o fluxo do código e torná-lo incompreensível.
APENAS MINHA OPINIÃO correta
Não entendo por que a "segmentação baseada em bloqueio" [sic] está na sua lista. Existem formas de simultaneidade que não envolvem bloqueios, e os problemas com bloqueios são geralmente bem conhecidos, mas nunca ouvi alguém descrever o uso de bloqueios como "mau".
Asveikau 31/10/10
4
asveikau: A segmentação baseada em bloqueio é notoriamente difícil de acertar (eu acho que 99,44% do código de produção usando bloqueios é ruim). Não compõe. É propenso a transformar seu código "multiencadeado" em código serial. (Corrigir isso apenas torna o código lento e inchado). Existem boas alternativas para o encadeamento baseado em bloqueio, como modelos STM ou ator, que o utilizam em qualquer coisa, exceto no código de nível mais baixo.
APENAS MINHA OPINIÃO correta
o "por que cadeia" :) não se esqueça de parar após três etapas, pois isso pode doer.
Szymanowski
27

Eval está bem, desde que você saiba EXATAMENTE o que está acontecendo. Qualquer entrada do usuário deve ser verificada e validada e tudo mais. Se você não sabe como ter 100% de certeza, não faça isso.

Basicamente, um usuário pode digitar qualquer código para o idioma em questão e ele será executado. Você pode imaginar por si mesmo quanto dano ele pode causar.

Tor Valamo
fonte
1
Então, se estou gerando expressões S com base na entrada do usuário, usando um algoritmo que não copia diretamente a entrada do usuário, e se isso é mais fácil e mais claro em uma situação específica do que usar macros ou outras técnicas, suponho que não haja nada de "mau" " sobre isso? Em outras palavras, os únicos problemas com eval são os mesmos com consultas SQL e outras técnicas que usam a entrada do usuário diretamente?
Jay
10
A razão pela qual é chamado de "mal" é porque fazer errado é muito pior do que fazer outras coisas erradas. E como sabemos, os novatos farão coisas erradas.
Tor Valamo 03/04
3
Eu não diria que o código deve ser validado antes de avaliá-lo em todas as circunstâncias. Ao implementar um REPL simples, por exemplo, você provavelmente colocaria a entrada em eval desmarcada e isso não seria um problema (é claro que ao escrever um REPL baseado na Web, você precisaria de uma caixa de areia, mas esse não é o caso do normal CLI-REPLs que são executados no sistema do usuário).
sepp2k
1
Como eu disse, você precisa saber exatamente o que acontece quando você alimenta o que você alimenta na avaliação. Se isso significa "ele executará alguns comandos dentro dos limites da caixa de areia", é isso que significa. ;)
Tor Valamo 03/04
@TorValamo já ouviu falar em fuga da prisão?
Loïc Faure-Lacroix
21

"Quando devo usar eval?" pode ser uma pergunta melhor.

A resposta curta é "quando seu programa pretende gravar outro programa em tempo de execução e depois executá-lo". A programação genética é um exemplo de uma situação em que provavelmente faz sentido usar eval.

Zak
fonte
14

IMO, esta questão não é específica para LISP . Aqui está uma resposta sobre a mesma pergunta para PHP e se aplica a LISP, Ruby e outra outra linguagem que possui uma avaliação:

Os principais problemas com eval () são:

  • Entrada potencial insegura. Passar um parâmetro não confiável é uma maneira de falhar. Geralmente, não é uma tarefa trivial garantir que um parâmetro (ou parte dele) seja totalmente confiável.
  • Trickyness. Usar eval () torna o código inteligente e, portanto, mais difícil de seguir. Para citar Brian Kernighan "A depuração é duas vezes mais difícil do que escrever o código em primeiro lugar. Portanto, se você escrever o código da maneira mais inteligente possível, você não é, por definição, inteligente o suficiente para depurá-lo "

O principal problema com o uso real de eval () é apenas um:

  • desenvolvedores inexperientes que o usam sem consideração suficiente.

Retirado daqui .

Eu acho que a peça complicada é um ponto incrível. A obsessão pelo código golf e pelo código conciso sempre resultou em um código "inteligente" (para o qual as avaliações são uma ótima ferramenta). Mas você deve escrever seu código para facilitar a leitura, IMO, para não demonstrar que é esperto e não economizar papel (você não o imprimirá de qualquer maneira).

Então, no LISP, há algum problema relacionado ao contexto em que o eval é executado, para que o código não confiável possa ter acesso a mais coisas; esse problema parece ser comum de qualquer maneira.

Dan Rosenstark
fonte
3
O problema de "entrada incorreta" com EVAL afeta apenas linguagens não-Lisp, porque nesses idiomas, eval () geralmente usa um argumento de seqüência de caracteres e a entrada do usuário é tipicamente emendada. O usuário pode incluir uma citação em sua entrada e escapar para o código gerado. Mas no Lisp, o argumento de EVAL não é uma string e a entrada do usuário não pode escapar para o código, a menos que você seja absolutamente imprudente (como você analisou a entrada com READ-FROM-STRING para criar uma expressão S, que você inclui em o código EVAL sem citá-lo. Se você citá-lo, não há como escapar da citação).
Jogue fora a conta
12

Houve muitas ótimas respostas, mas aqui está outro exemplo de Matthew Flatt, um dos implementadores do Racket:

http://blog.racket-lang.org/2011/10/on-eval-in-dynamic-languages-generally.html

Ele faz muitos dos pontos que já foram abordados, mas algumas pessoas podem achar sua opinião interessante, no entanto.

Resumo: O contexto em que é usado afeta o resultado da avaliação, mas geralmente não é considerado pelos programadores, levando a resultados inesperados.

stchang
fonte
11

A resposta canônica é ficar longe. O que eu acho estranho, porque é um primitivo, e dos sete primitivos (os outros são contras, carro, cdr, if, eq e citação), fica de longe a menor quantidade de uso e amor.

De On Lisp : "Normalmente, telefonar para eval explicitamente é como comprar algo em uma loja de presentes do aeroporto. Depois de esperar até o último momento, é preciso pagar preços altos por uma seleção limitada de mercadorias de segunda categoria".

Então, quando uso eval? Um uso normal é ter um REPL dentro do seu REPL, avaliando (loop (print (eval (read)))). Todo mundo está bem com esse uso.

Mas você também pode definir funções em termos de macros que serão avaliadas após a compilação combinando eval e backquote. Você vai

(eval `(macro ,arg0 ,arg1 ,arg2))))

e isso matará o contexto para você.

O Swank (para o emacs slime) está cheio desses casos. Eles se parecem com isso:

(defun toggle-trace-aux (fspec &rest args)
  (cond ((member fspec (eval '(trace)) :test #'equal)
         (eval `(untrace ,fspec))
         (format nil "~S is now untraced." fspec))
        (t
         (eval `(trace ,@(if args `(:encapsulate nil) (list)) ,fspec ,@args))
         (format nil "~S is now traced." fspec))))

Não acho que seja um truque imundo. Eu o uso o tempo todo para reintegrar macros em funções.

Daniel Cussen
fonte
1
Você pode querer verificar para fora a língua do kernel;)
artemonster
7

Mais alguns pontos em Lisp eval:

  • Ele é avaliado no ambiente global, perdendo o contexto local.
  • Às vezes, você pode se sentir tentado a usar eval, quando realmente pretendia usar a macro de leitura '#'. que avalia em tempo de leitura.
pyb
fonte
Entendo que o uso de env global é verdadeiro para Common Lisp e Scheme; isso também é verdade para Clojure?
Jay
2
No esquema (pelo menos para o R7RS, talvez também para o R6RS), você deve passar um ambiente para avaliar.
CSL
4

Como a "regra" do GOTO: se você não sabe o que está fazendo, pode fazer uma bagunça.

Além de apenas criar algo com dados conhecidos e seguros, há o problema de que algumas linguagens / implementações não conseguem otimizar o código o suficiente. Você pode acabar com o código interpretado dentro eval.

stesch
fonte
O que essa regra tem a ver com o GOTO? Existe algum recurso em qualquer linguagem de programação com a qual você não possa bagunçar?
Ken
2
@ Ken: Não há regra GOTO, portanto, as aspas na minha resposta. Existe apenas um dogma para as pessoas que têm medo de pensar por si mesmas. O mesmo para avaliação. Lembro-me de acelerar drasticamente alguns scripts Perl usando eval. É uma ferramenta na sua caixa de ferramentas. Os novatos costumam usar eval quando outras construções de linguagem são mais fáceis / melhores. Mas evitá-lo completamente só para ser legal e agradar as pessoas dogmáticas?
Stesch
4

Eval é apenas inseguro. Por exemplo, você tem o seguinte código:

eval('
hello('.$_GET['user'].');
');

Agora, o usuário chega ao seu site e insere o URL http://example.com/file.php?user= ); $ is_admin = true; echo (

Então o código resultante seria:

hello();$is_admin=true;echo();
Ragnis
fonte
6
ele estava falando Lisp pensamento não php
FMSF
4
@fmsf Ele estava falando especificamente sobre o Lisp, mas geralmente evalem qualquer idioma que o possua.
Skilldrick
4
@ fmsf - esta é realmente uma pergunta independente da linguagem. Ele ainda se aplica a linguagens compiladas estáticas, pois elas podem simular eval chamando o compilador em tempo de execução.
Daniel Earwicker
1
nesse caso, o idioma é duplicado. Eu já vi muitos como este aqui.
Fmsf 03/04/19
9
O PHP eval não é como o Lisp eval. Veja, ele opera em uma cadeia de caracteres, e a exploração na URL depende de poder fechar um parêntese textual e abrir outro. O Lisp eval não é suscetível a esse tipo de coisa. Você pode avaliar os dados que chegam como entrada de uma rede, se você os seleciona na caixa de areia corretamente (e a estrutura é fácil de percorrer para fazer isso).
Kaz
2

Eval não é mau. Eval não é complicado. É uma função que compila a lista que você passa para ela. Na maioria dos outros idiomas, compilar código arbitrário significaria aprender o AST do idioma e vasculhar as partes internas do compilador para descobrir a API do compilador. No lisp, você apenas chama eval.

Quando você deve usá-lo? Sempre que você precisar compilar algo, normalmente um programa que aceita, gera ou modifica código arbitrário em tempo de execução .

Quando você não deveria usá-lo? Todos os outros casos.

Por que você não deveria usá-lo quando não precisa? Porque você faria algo de uma maneira desnecessariamente complicada que pode causar problemas de legibilidade, desempenho e depuração.

Sim, mas se eu sou iniciante, como sei se devo usá-lo? Sempre tente implementar o que você precisa com funções. Se isso não funcionar, adicione macros. Se isso ainda não funcionar, avalie!

Siga estas regras e você nunca fará o mal com eval :)

optevo
fonte
0

Gosto muito da resposta de Zak e ele chegou à essência do assunto: eval é usado quando você está escrevendo um novo idioma, um script ou uma modificação de um idioma. Ele realmente não explica mais, então vou dar um exemplo:

(eval (read-line))

Neste programa Lisp simples, o usuário é solicitado a inserir e, em seguida, o que quer que ele insira é avaliado. Para que isso funcione, todo o conjunto de definições de símbolos deve estar presente se o programa for compilado, porque você não tem idéia de quais funções o usuário pode inserir, portanto, você deve incluir todas elas. Isso significa que, se você compilar esse programa simples, o binário resultante será gigantesco.

Por uma questão de princípio, você não pode sequer considerar isso uma declaração compilável por esse motivo. Em geral, depois de usar eval , você está operando em um ambiente interpretado e o código não pode mais ser compilado. Se você não usar eval , poderá compilar um programa Lisp ou Scheme como um programa em C. Portanto, você deseja ter certeza de que deseja e precisa estar em um ambiente interpretado antes de se comprometer a usar o eval .

Tyler Durden
fonte