Last Updated on 13 Giugno 2021 by Roberto De Pedrini
Si può fare una chiamata ricorsiva in RPG ? Impossibile !
Questa è spesso la prima risposta che si ottiene a questa domanda… Tutto vero, ma solo parzialmente. In RPG infatti non è possibile richiamare ricorsivamente un programma. Ma un programma ILE può contenere al suo interno diverse procedure, e qui il discorso cambia…
Le procedure infatti, grazie alle loro specificità e al loro livello di isolamento, possono essere richiamate ricorsivamente: tutto sommato, una procedura è una funzione che viene richiamata passando dei parametri, e restituisce un valore di ritorno (o nessuno). Questa caratteristica fa si che una chiamata ad una procedura possa avvenire tranquillamente anche all’interno della procedura stessa. Tutto sta a scrivere il codice in maniera opportuna, tenendo conto della ricorsività.
Presentiamo qui un classico esempio nel quale la ricorsività permette di ottenere un codice molto più semplice: lo sviluppo di una distinta base.
Per questo esempio abbiamo creato un file anagrafico che contiene il tipo e il codice dell’articolo, il tipo e il codice del componente, ed il fabbisogno di componente necessario. Il “tipo” identifica la tipologia del prodotto (finito, semilavorato, ecc…). Ogni articolo può ovviamente avere più componenti, per cui nel file esisteranno uno o più record per ogni articolo “padre”, e lo stesso avviene per i suoi componenti (che a loro volta sono “padri” di altri articoli).
Ecco l’istruzione SQL per la creazione della tabella (file).
CREATE OR REPLACE TABLE BOMDT00F (
ITEMTYPE CHAR(1) NOT NULL DEFAULT ' ' ,
ITEMCODE CHAR(20) NOT NULL DEFAULT ' ' ,
COMPTYPE CHAR(1) NOT NULL DEFAULT ' ' ,
COMPCODE CHAR(20) NOT NULL DEFAULT ' ' ,
QUANTITY NUMERIC(9, 2) NOT NULL DEFAULT 0 )
RCDFMT BOMDT ;
ITEMTYPE e ITEMCODE identificano l’articolo “padre”, mentre COMPTYPE e COMPCODE sono il componente.
Su questa tabella è stato creato un indice:
CREATE INDEX BomDT01L ON BomDT00F (
itemType, itemCode
)
RCDFMT BOMDT ;
Questi sono i dati caricati per il nostro test:
Il programma di esempio legge la distinta base e scrive i risultati in un file di lavoro. Partendo da un articolo iniziale, ricevuto come parametro, il programma ricerca i suoi componenti, e per ogni componente ne calcola il fabbisogno scrivendo il file di lavoro. Il processo viene poi ripetuto ricorsivamente per ogni sottocomponente, scendendo di livello fino ad incontrare le materie prime (articoli che non hanno componente). Nell’uso di procedure ricorsive è importante stabilire sempre una condizione di uscita, in caso contrario si rischia un loop infinito.
Di seguito la definizione del file di lavoro:
CREATE OR REPLACE TABLE BOMWRK (
LEVEL NUMERIC(4, 0),
ITEMTYPE CHAR(1) NOT NULL DEFAULT ' ' ,
ITEMCODE CHAR(20) NOT NULL DEFAULT ' ' ,
COMPTYPE CHAR(1) NOT NULL DEFAULT ' ' ,
COMPCODE CHAR(20) NOT NULL DEFAULT ' ' ,
QUANTITY NUMERIC(9, 2) NOT NULL DEFAULT 0 )
RCDFMT WBOM ;
Questo è il source completo del programma FAQ40014B:
ctl-opt decedit('0,') datedit(*dmy.)
actgrp(*caller) dftactgrp(*no) option(*nodebugio:*srcstmt);
//-------------------------------------------------------------*
// Bill Of Material development
//
// Reads the BOM from base item and develops all components
// and related required quantities, down to raw material.
// Output is written to a work file
//
// INPUT PARMS:
// - item type CHAR(1)
// - item code CHAR(20)
//-------------------------------------------------------------*
// BOM master file
dcl-f BomDT01l disk usage(*input) keyed;
// work file
dcl-f BomWRK disk usage(*output) prefix(W_);
// ENTRY -------------------------------------------------
dcl-pi FAQ40014B;
dcl-parm pType char(1);
dcl-parm pCode char(20);
end-pi;
// Prototypes ------------------------------------------------
dcl-pr getBOM;
*n char(1);
*n char(20);
*n zoned(9:2);
*n zoned(4:0);
end-pr;
// Variables -----------------------------------------------
dcl-ds dsBOM extname('BOMDT00F') end-ds;
dcl-s qty0 like(Quantity);
dcl-s level0 zoned(4:0);
// PSDS ------------------------------------------------------
dcl-ds pgmDS psds qualified;
user char(10) pos(254);
end-ds;
//*********************************************************
// SQL options
Exec SQL
SET OPTION COMMIT = *None,
ALWCPYDTA = *Yes,
CLOSQLCSR = *EndMod,
DLYPRP = *Yes
;
// Clear work file
Exec SQL
DELETE FROM BomWRK ;
// Get initial item
chain (pType:pCode) BomDT01l;
if %found(BomDT01l);
qty0 = 1;
level0 = 1;
getBOM(pType:pCode:qty0:level0);
endif;
close *all;
*inLR = *on;
return;
// --------------------------------------------------
// BOM development
// --------------------------------------------------
dcl-proc getBOM;
// Procedure interface -------------------
dcl-pi getBOM ;
dcl-parm pType char(1); // item type
dcl-parm pItem char(20); // item code
dcl-parm pQty zoned(9:2); // required quantity
dcl-parm pLevel zoned(4:0); // depth level
end-pi;
// BOM master file
dcl-f BomDT01l disk usage(*input) keyed;
// Variables -------------------------------------------------
dcl-s qtyComp like(Quantity);
dcl-s level zoned(4:0);
dcl-ds dsBOM2 extname('BOMDT01L':*all) end-ds;
// read item's components and develop BOM
setll (pType:pItem) BomDT01l;
level = pLevel + 1;
dou %eof(BomDT01l);
reade (pType:pItem) BomDT01l dsBOM2;
if not %eof(BomDT01l) and CompCode <> *blank;
eval(h) qtyComp = pQty * quantity;
exsr wrtFile;
// recursive call
getBOM(CompType:
CompCode:
qtyComp:
level);
endif;
enddo;
// --------------------------------------------------
// write item component to work file
// --------------------------------------------------
begsr wrtFile;
Clear WBOM;
W_Level = pLevel;
W_itemType = pType;
W_itemCode = pItem;
W_compType = compType;
W_compCode = compCode;
W_quantity = qtyComp;
Write WBOM;
endsr;
// --------------------------------------------------
end-proc;
Analizziamo in particolare la procedura getBOM: i parametri di input sono il tipo ed il codice dell’articolo “padre” ed il relativo fabbisogno. La procedura legge tutti i componenti dell’articolo padre, calcola il fabbisogno, scrive un record nel file di lavoro e richiama ricorsivamente la stessa procedura getBOM passando come parametri il tipo e codice del componente, ed il fabbisogno calcolato, che si propaga quindi al livello inferiore. Notare come il file BOMDT01L sia stato dichiarato localmente nella procedura; in questo modo ad ogni richiamo ricorsivo (che di fatto crea una nuova istanza della procedura nello stack dei richiami), non si perde il posizionamento sul file all’interno di cicli di lettura per chiave.
Al termine, il file di lavoro conterrà tutto lo sviluppo della distinta base, fino alle materie prime con i relativi fabbisogni. Lanciando il programma per l’articolo MADRID di tipo 4, ecco il risultato:
La colonna LEVEL indica il livello di profondità dello sviluppo: 1 è il prodotto finito, 2 il primo semilavorato ecc.. in questo caso ci si ferma a 3 livelli. Dal contenuto di questo file si capisce quindi che per produrre una unità di prodotto finito (articolo 4-MADRID), servono tre materie prime, WOOL-002, POLY-001, SILK-002 con un fabbisogno rispettivamente di 144, 104 e 14,29 “unità” (per restare generici non è indicata alcuna unità di misura specifica).
L’uso di procedure ricorsive facilita notevolmente alcune situazioni applicative tipiche, oltre alla distinta base pensiamo allo sviluppo di un albero di menù, oppure la definizione di itinerari in ambito logistica, con partenza e arrivo.
Nella prossima “puntata” vedremo come anche l’SQL non sia da meno e permetta di gestire la ricorsività.
Tutti i source di questo esempio sono disponibili su GitHub a questo link: https://github.com/MD2706GIT/FAQ400
DOCUMENTAZIONE
IBM – Recursive Calls: https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_74/rzasc/recursv.htm