Una funzione per produrre una coppia o una funzione per articolo?

4

Ho un dilemma. Diciamo che ho un AST che descrive alcune grammatiche, per esempio. Posso scrivere una funzione per stringere questo AST in un formato BNF leggibile dall'uomo o generare un parser da esso. Posso e voglio generare entrambi da dato ast. Posso farlo o definendo due funzioni corrispondenti, ast => string e ast => parser oppure è meglio creare una singola funzione che attraversa l'ast una volta e produce un paio di ast => [stringified,parsified] per ogni nodo Ast in single pass?

Il codice per il primo sarà simile a

stringify(ast) = case ast.tag of
  ":" => ast.value + ":" + stringify(ast.children) // label
  "//" => "/" + ast.value + "/" // regex
  QQ => '"' + ast.value + '"' // literal
  "{}" => '{' + ast.value + '}' // user semantic action
  "()" => '(' + stringify(ast.children) + ')' // parenthized expression
  "&" => stringify(ast.children).join("&")
  "|" => stringify(ast.children).join("|")

parsify(ast, stack) = 
 const childrenWithNewFrame = parseify(ast.children, stack.push({}))
 return case ast.tag of
  ":" => stack.last(ast.value) = parsify(ast.children, stack)
  "//" => reParser(ast.value) // matches a regex
  QQ => literal(ast.value) // matches given literal
  "{}" => userAction(stack) // applies user code to the stack
  "()" => childrenWithNewFrame()
  "&" => [h, tail] = childrenWithNewFrame(); tail.reduce((acc,p) => acc.andThen(p),h)
  "|" => oneOf(parsify(ast.children))

Si vede che la struttura è quasi identica, sia in stringify che in parsify , perché questa è una funzione visit tranne per il fatto che parsify ha bisogno di un dizionario con scope di etichette. Ho quindi iniziato a guardare l'alternativa in cui produco entrambi in una sola volta

both(ast, stack) = 
 const childrenWithNewFrame = parseify(ast.children, stack.push({}))
 return case ast.tag of
  "//" => ["/"+ast.value+"/", reParser(ast.value)]
  QQ => ['"'+ast.value+'"', literal(ast.value)]
  "{}" => ['"'+ast.value+'"', userAction(stack)]
  and so on...

dove ogni caso restituisce sia stringa che parser. Si può notare che se si avvicinano prima i duplicati (DRY) dei casi, il secondo sembra duplicare il modello di coppia [,] in ogni caso. Cosa c'è di meglio?

È un dilemma noto nella programmazione?

    
posta Little Alien 17.11.2016 - 12:30
fonte

2 risposte

8

Ciò che chiedi non ha molto a che fare con il particolare problema dell'elaborazione AST. La domanda che stai chiedendo è

  • Devo progettare un'API per un componente, l'API deve fornire diverse funzioni per calcolare valori diversi, ma il calcolo del valore internamente è complesso e ha passaggi simili, dovrei progettare l'API in modo che restituisca valori diversi in una funzione chiamare subito, o devo fornire funzioni diverse, per ogni possibile valore?

Puoi rispondere a questa domanda se ti fai una domanda leggermente diversa:

  • qual è il modo più semplice dal punto di vista di un utente di quell'API?

Ad esempio, un utente della tua API di solito ha bisogno di una rappresentazione "stringificata" e una "parsificata" entrambe? Quindi progetta l'API in modo che fornisca entrambi. O avrà bisogno di una versione con stringata o parsificata, ma raramente entrambi? Quindi progetta l'API in modo che fornisca due diverse funzioni. Se non sei sicuro, prova a scrivere prima alcuni test in un modo TDD, chiamando l'API, che potrebbe aiutarti a prendere questa decisione.

Tuttavia, questa è la domanda su come la tua API dovrebbe apparire come esternamente . La domanda su come funzionano le tue funzioni internamente è piuttosto diversa, e dovresti evitare di mescolarle con la prima domanda. Anche se l'API fornisce due funzioni separate a un utente, potrebbe calcolare una coppia [stringified,parsified] in una esecuzione internamente per prima, se è più facile da implementare in modo DRY. Oppure potrebbe utilizzare internamente due diverse funzioni per calcolare il risultato che viene restituito in coppia, se è più facile da implementare.

E cerca di evitare che la questione delle prestazioni influenzi il design della tua API esterna, in particolare alcuni timori per le performance superstiziose "nel caso in cui". Quando le prestazioni iniziano a essere importanti (e dovresti avere problemi di prestazioni reali e misurabili prima di iniziare a porre la domanda), potresti cambiare il design delle funzioni interne, ma idealmente non il tuo progetto esterno. Se, ad esempio, l'API esterna fornisce due funzioni separate, per le quali si calcola un risultato intermedio che potrebbe essere utilizzato come valore di ritorno per l'altra funzione, basta ricordare il risultato della prima chiamata in una variabile cache interna, quindi il secondo la chiamata di funzione non deve eseguire nuovamente lo stesso calcolo.

EDIT: dopo aver delineato la domanda, è diventato chiaro che il problema principale è come implementare l'elaborazione AST internamente in modo DRY. Utilizzando un approccio orientato agli oggetti, potresti prendere in considerazione di creare un'interfaccia astratta IASTProcessor , fornendo funzioni per ogni parola chiave possibile (":", "//", QQ, ..) e implementare l'elaborazione in termini di chiamate a tale interfaccia:

evalAst(ast, stack, astProcessor) = 
 const childrenWithNewFrame = parseify(ast.children, stack.push({}))
 return case ast.tag of
  "//" => astProcessor.regex(ast.value)
  QQ => astProcessor.literal(ast.value)
  "{}" => astProcessor.userAction(ast.value)

Quindi implementa questa interfaccia due volte: una volta con astAsStringProcessor e una con astParserProcessor . Questo dovrebbe evitare il codice duplicato mostrato nella prima variante, ed evita anche il ripetuto "modello di coppia" della tua seconda variante. A seconda del linguaggio di programmazione che si sta utilizzando, si potrebbe ancora trovare un codice in più rispetto alla variante 2, a vantaggio di ottenere una soluzione che rispetti il principio Open-Closed.

    
risposta data 17.11.2016 - 12:57
fonte
-1

Alcune lingue, come JS hanno funzioni con nome. Puoi dare nomi di stringhe ai tuoi parser (cioè input = > funzioni ParseResult) che crei. Fare entrambi allo stesso tempo risolve naturalmente il dilemma.

    
risposta data 18.03.2017 - 20:23
fonte

Leggi altre domande sui tag