Compilação de bytes de um pacote com vários arquivos: “sabe-se que a função não está definida”

7

Imagine que eu tenho os seguintes arquivos no meu pacote (ridículo):

Arquivo test1.el:

;;; test1.el ---                                   

;;; Code:

(defvar test-var1)

(defun test-fun1 (test)
  nil)

(require 'test2 "./test2.el)

(provide 'test1)
;;; test1.el ends here

Arquivo test2.el:

;;; test2.el ---  

;;; Code:

(defun test-fun2 ()
  (let ((test test-var1))
    (test-fun1 test)))

(provide 'test2)
;;; test2.el ends here

Se então eu corro:

emacs -batch -f batch-byte-compile *.el

Eu recebo o seguinte resultado:

Compiling .../test1.el...
Wrote .../test1.elc
Compiling .../test2.el...

In test-fun2:
test2.el:9:15:Warning: reference to free variable `test-var1'

In end of data:
test2.el:14:1:Warning: the function `test-fun1' is not known to be defined.
Wrote .../test2.elc

Entendo por que esses avisos aparecem e entendo que são apenas avisos. No entanto, seria fácil perder um erro de digitação no nome de uma função ignorando todos os avisos desse tipo.

De alguma forma, pensei que adicionar uma (require 'test2)linha test2.eldeveria corrigi-lo. No entanto, neste caso, recebo:

Compiling .../test1.el...

In toplevel form:
test1.el:10:1:Error: Recursive `require' for feature `test2'
Compiling .../test2.el...

In toplevel form:
test2.el:5:1:Error: Recursive `require' for feature `test1'

Isso é enigmático, porque pensei que o objetivo requireera exatamente evitar carregamento recursivo. Eu suponho que requireesteja se comportando como loaddurante o tempo de compilação.

Qual é uma maneira boa (e segura) de se livrar desses avisos?

O manual fornece uma solução alternativa (eu a coloco como uma resposta melhor do que nada abaixo), mas, no final das contas, gostaria que a solução fosse bastante automática (não exigindo que eu listasse todas as funções e variáveis ​​necessárias em cada arquivo).

A solução ideal seria incorporada no emacs ou fornecida com o Cask. Se não existir, aceitarei o que está disponível, é claro.

T. Verron
fonte

Respostas:

8

Sobre exigir

requirenão se destina a evitar carregamento recusado, mas sim para evitar carregamento repetitivo . Então não, isso não resolve o seu problema aqui.

Sobre o problema

A maneira correta de abordar isso (na minha opinião) seria evitar a dependência mútua.

O test1arquivo no seu exemplo não tem motivos para exigir test2. Mesmo que isso não seja verdade para o seu pacote real, talvez você possa reprojetar como está delegando código entre os arquivos. Em geral, é possível evitar a dependência mútua entre seus arquivos.

Soluções alternativas

  1. Se a dependência mútua não puder ser evitada, o manual menciona uma solução. Você precisará adicionar linhas como as seguintes para cada função / variável necessária.

    (declare-function test-fun1 "./test1.el")
    (defvar test-var1)
    
  2. Outra opção é requireos arquivos apenas condicionalmente. Adicione algo assim ao arquivo 1:

    (defvar test1-is-loading t)
    (unless (and (boundp 'test2-is-loading)
                 test2-is-loading)
      (require 'test2))
    

    E algo assim para arquivar 2:

    (defvar test2-is-loading t)
    (unless (and (boundp 'test1-is-loading)
                 test1-is-loading)
      (require 'test1))
    
Malabarba
fonte
A situação realista é um pacote em que o arquivo principal declara algumas coisas (variáveis ​​como um mapa de modos, caminhos executáveis, grupo de personalização), requer outros arquivos que fornecem várias funções para o pacote e reúne tudo (pelo menos em um -modefunção). O exemplo é um exemplo de brinquedo, usá-lo test-fun2não mudaria o problema.
T. Verron
11
@ T.Verron Sim, trabalhei no pressuposto de que seu exemplo não era completamente preciso e ofereci soluções alternativas. Ainda assim, mantenho minha afirmação de que o pacote pode ser melhor projetado. No seu exemplo realista, não há razão para que o arquivo que reúne tudo também seja o único a definir variáveis ​​e grupos. Crie um arquivo extra que contenha essas definições (como test-variables), e esse arquivo não precisará exigir nenhum dos outros.
Malabarba 7/11
Como mencionei na minha resposta inicial, sua primeira solução alternativa funciona bem, mas é bastante tediosa. O segundo parece promissor, mas será um pouco mais complicado: as instruções precisarão ser agrupadas em eval-when-compile's' e os pacotes precisarão definir sua variável nilno final do arquivo (porque todos os arquivos são compilados em uma única sessão). Também tem o benefício de me mostrar por que o carregamento exatamente recursivo é mais complicado de evitar do que o carregamento repetido.
7118 T. Verron
E obrigado pela sugestão de refatoração, isso realmente funcionaria.
7118 T. Verron
4

Seu exemplo é estranho:

  • Você requiretest2 no final do test1, enquanto requireque "sempre" deve estar no início de um arquivo.
  • Seu test1 não chama nenhuma função test2; portanto, não precisa do test2 para funcionar (portanto, requireé desnecessário); e OTOH seu test2 chama as funções test1, portanto, precisa do test1, mas falha requirenele.

Como você requireestá atrasado.

Stefan
fonte
Em um exemplo real, é test1claro que usaria as funções definidas por test2e o pacote seria carregado apenas através test1(através de carregamento automático). Esta resposta não deveria ser um comentário? É apenas salientar que meu exemplo é mal escolhido e, de outra forma, não fornece uma resposta para a pergunta.
T. Verron
@ T.Verron Esta resposta respondeu ao caso específico que você ofereceu. O fato de ter sido um exemplo mal escolhido não é culpa dele. ;-)
Malabarba 7/11
Como as duas respostas abordam esse ponto de refatoração, devo assumir que é um ponto válido, dada a pergunta. O seu teve o benefício de abordar os problemas gerais, além das indicações no comentário para o meu caso específico. É provável que este seja útil para alguém com um caso real?
7118 T. Verron
E com uma visão mais de curto prazo, se tivesse sido postada como um comentário, eu poderia ter editado a pergunta para torná-la (um pouco) mais realista. Mas como ela é postada como resposta, não posso editar a pergunta sem torná-la uma pergunta diferente (diferente porque uma resposta anterior não seria mais aplicável). Imo, essa resposta se encaixa exatamente no campo dos comentários: solicitando esclarecimentos (neste caso, uma confirmação de que o exemplo mínimo é realmente representativo do problema) ao solicitante.
7118 T. Verron
3

O manual sugere adição declare-functione defvarlinhas.

O test2arquivo resultante é:

;;; test2.el ---  

;;; Code:

(declare-function test-fun1 "./test1.el")
(defvar test-var1)

(defun test-fun2 ()
  (let ((test test-var1))
    (test-fun1 test)))

(provide 'test2)
;;; test2.el ends here

No entanto, isso precisa ser feito para todas as funções e todas as variáveis ​​definidas nos arquivos "principais".

T. Verron
fonte