lunedì 21 marzo 2016

Progetto Java: Ricercare Informazione in Grafi Sintattici

Commenti in merito al  progetti in java: Ricerca Informazione in Grafi Sintattici consegnati al prof Zanzotto.

Parte I - Uno stile migliore

1
2
3
if (labels.get(i).equals("nsubj")) {
  return new Word("nsubj", words.get(i));
}

In questo piccolo "pezzo" di codice possiamo apportare due importanti modifiche:

  1. Attuare un pattern di programmazione difensiva (Vedi http://lmpcoding.blogspot.it/2016/03/progetto-in-java-cruciverba-i-buoni.html)
  2. Utilizzare le costanti al posto delle stringhe
Il primo pensiero che deve venire in mente al momento della scrittura di un blocco "if-else" è 
Devo evitare assolutamente le condizioni che possono portare ad un errore
Nel nostro caso è importante proteggersi dall'istruzione
labels.get(i).equals("nsubj")
che può ritornare un valore pari a null: a runtime può succedere che il valore ritornato dalla get sia pari a null generando l'istruzione
null.equals("nsubj")
sollevando una NullPointerException.
Nei casi in cui bisogna effettuare dei controlli di uguaglianza su stringhe è sufficiente, per evitare situazioni di errore, eseguire il seguente "trick"

1
2
3
if ("nsubj".equals(labels.get(i))) {
  return new Word("nsubj", words.get(i));
}

In questo modo l'istruzione che verrà eseguita, nel caso in cui "labels.get(i)" sia pari a null, sarà
"nsubj".equals(null)
che restituirà semplicemente false.

Il secondo punto, per quanto banale possa essere, è importante al fine di diminuire la dipendenza tra le classi e gli oggetti che utilizziamo (in questo caso le stringhe).
Supponiamo che la stringa "nsubj"  cambi e si trasformi in "nsubje". A causa di questa modifica dobbiamo cambiare tutte le istruzioni in cui utilizziamo "a forza bruta" tale stringa.

Aggiungendo una costante alla classe, invece, sarà necessario modificare all'occorrenza solamente il valore di tale costante. Il codice precedente sarà quindi:

public class NodeFactory {

private static final String N_SUBJ_COSTANT = "nsubj";
...

if (N_SUBJ_COSTANT.equals(labels.get(i))) {
  return new Word(N_SUBJ_COSTANT words.get(i));
}


Parte II - Un unico metodo per domarli tutti

In ottica del principio "meno scrivo, meno devo modificare in futuro",  analizziamo i seguenti metodi:

public List<String> returnLabels(String sentence, LexicalizedParser lp) {

List<CoreLabel> rawWords = Sentence.toCoreLabelList(sentence);
Tree parse = lp.apply(rawWords);

 TokenizerFactory<CoreLabel> tokenizerFactory = PTBTokenizer.factory(new CoreLabelTokenFactory(), "");
 Tokenizer<CoreLabel> tok = tokenizerFactory.getTokenizer(new StringReader(sentence));
 List<CoreLabel> rawWords2 = tok.tokenize();
 parse = lp.apply(rawWords2);

 // PennTreebankLanguagePack for English
 TreebankLanguagePack tlp = lp.treebankLanguagePack();
 GrammaticalStructureFactory gsf = tlp.grammaticalStructureFactory();
 GrammaticalStructure gs = gsf.newGrammaticalStructure(parse);
 List<TypedDependency> tdl = gs.typedDependenciesCCprocessed();

 String lista = null;
 List<String> listaGrammaticale = new ArrayList<String>();
 for (int i = 0; i < tdl.size(); i++) {
  lista = tdl.get(i).toString();
  listaGrammaticale.add(lista);
 }
 List<String> listOfWords = new ArrayList<String>();
 for (int j = 0; j < listaGrammaticale.size(); j++) {

  if (listaGrammaticale.get(j).startsWith("nmod") || listaGrammaticale.get(j).startsWith("nsubj")
    || listaGrammaticale.get(j).startsWith("dobj")) {
   String partOfTheSentence = listaGrammaticale.get(j);
   if (listaGrammaticale.get(j).startsWith("nmod")) {
    String[] parts = partOfTheSentence.split("[:]");
    listOfWords.add(parts[0]);
   } else {
    String[] part = partOfTheSentence.split("[(]");
    listOfWords.add(part[0]);
   }
  }
 }
 return listOfWords;
}
..
public List<String> returnWords(String sentence, LexicalizedParser lp) {

List<CoreLabel> rawWords = Sentence.toCoreLabelList(sentence);
Tree parse = lp.apply(rawWords);

 TokenizerFactory<CoreLabel> tokenizerFactory = PTBTokenizer.factory(new CoreLabelTokenFactory(), "");
 Tokenizer<CoreLabel> tok = tokenizerFactory.getTokenizer(new StringReader(sentence));
 List<CoreLabel> rawWords2 = tok.tokenize();
 parse = lp.apply(rawWords2);

 // PennTreebankLanguagePack for English
 TreebankLanguagePack tlp = lp.treebankLanguagePack();
 GrammaticalStructureFactory gsf = tlp.grammaticalStructureFactory();
 GrammaticalStructure gs = gsf.newGrammaticalStructure(parse);
 List<TypedDependency> tdl = gs.typedDependenciesCCprocessed();

 String lista = null;
 List<String> listaGrammaticale = new ArrayList<String>();
 for (int i = 0; i < tdl.size(); i++) {
  lista = tdl.get(i).toString();
  listaGrammaticale.add(lista);
 }
 List<String> listOfWords = new ArrayList<String>();
 for (int j = 0; j < listaGrammaticale.size(); j++) {

  if (listaGrammaticale.get(j).startsWith("nmod") || listaGrammaticale.get(j).startsWith("nsubj")
    || listaGrammaticale.get(j).startsWith("dobj")) {
   String partOfTheSentence = listaGrammaticale.get(j);
   if (listaGrammaticale.get(j).startsWith("nmod")) {
    String[] parts = partOfTheSentence.split("[:]");
    listOfWords.add(parts[1]);
   } else {
    String[] part = partOfTheSentence.split("[(]");
    listOfWords.add(part[1]);
   }
  }
 }
 return listOfWords;
}

Guardandoli da vicino si nota che sono praticamente uguali a meno delle due line di codice:
listOfWords.add(parts[0]);listOfWords.add(parts[1]);
Questo implica che, guardando al futuro, se dovesse cambiare la logica dell'inizializzazione delle strutture sintattiche siamo costretti a modificarle in entrambi i metodi. Per risolvere questo problema possiamo creare un metodo di supporto che si occupa di inizializzare la lista grammaticale

public List<String> inizializeTreeDependencyList(String sentence, LexicalizedParser lp) {
List<CoreLabel> rawWords = Sentence.toCoreLabelList(sentence); Tree parse = lp.apply(rawWords); TokenizerFactory<CoreLabel> tokenizerFactory = PTBTokenizer.factory(new CoreLabelTokenFactory(), ""); Tokenizer<CoreLabel> tok = tokenizerFactory.getTokenizer(new StringReader(sentence)); List<CoreLabel> rawWords2 = tok.tokenize(); parse = lp.apply(rawWords2); // PennTreebankLanguagePack for English TreebankLanguagePack tlp = lp.treebankLanguagePack(); GrammaticalStructureFactory gsf = tlp.grammaticalStructureFactory(); GrammaticalStructure gs = gsf.newGrammaticalStructure(parse); List<TypedDependency> tdl = gs.typedDependenciesCCprocessed(); String lista = null; List<String> listaGrammaticale = new ArrayList<String>(); for (int i = 0; i < tdl.size(); i++) { lista = tdl.get(i).toString(); listaGrammaticale.add(lista); } return listaGrammaticale; }

I metodi
returnWords(...) e returnLabels(...)
verranno trasformati in

public List<String> returnLabels(String sentence, LexicalizedParser lp) {

List<String> listaGrammaticale = inizializeTreeDependencyList(sentence,lp);

List<String> listOfWords = new ArrayList<String>();
for (int j = 0; j < listaGrammaticale.size(); j++) {

 if (listaGrammaticale.get(j).startsWith("nmod") || listaGrammaticale.get(j).startsWith("nsubj")
   || listaGrammaticale.get(j).startsWith("dobj")) {
  String partOfTheSentence = listaGrammaticale.get(j);
  if (listaGrammaticale.get(j).startsWith("nmod")) {
   String[] parts = partOfTheSentence.split("[:]");
   listOfWords.add(parts[0]);
  } else {
   String[] part = partOfTheSentence.split("[(]");
   listOfWords.add(part[0]);
  }
 }
}
return listOfWords;
}
..
public List<String> returnWords(String sentence, LexicalizedParser lp) {

List<String> listaGrammaticale = inizializeTreeDependencyList(sentence,lp);
List<String> listOfWords = new ArrayList<String>();
for (int j = 0; j < listaGrammaticale.size(); j++) {

 if (listaGrammaticale.get(j).startsWith("nmod") || listaGrammaticale.get(j).startsWith("nsubj")
   || listaGrammaticale.get(j).startsWith("dobj")) {
  String partOfTheSentence = listaGrammaticale.get(j);
  if (listaGrammaticale.get(j).startsWith("nmod")) {
   String[] parts = partOfTheSentence.split("[:]");
   listOfWords.add(parts[1]);
  } else {
   String[] part = partOfTheSentence.split("[(]");
   listOfWords.add(part[1]);
  }
 }
}
return listOfWords;
}



Parte III - Modularizzare

I metodi 
returnWords(...) e returnLabels(...)
descritti precedentemente sono stati presi dalla classe NodeFactory. Analizzando la classe Relation facente parte dello stesso progetto  notiamo che il metodo

public void recognizeVerb(String sentence, LexicalizedParser lp) {
List<CoreLabel> rawWords = Sentence.toCoreLabelList(sentence);
Tree parse = lp.apply(rawWords);

TokenizerFactory<CoreLabel> tokenizerFactory = PTBTokenizer.factory(new CoreLabelTokenFactory(), "");
Tokenizer<CoreLabel> tok = tokenizerFactory.getTokenizer(new StringReader(sentence));
List<CoreLabel> rawWords2 = tok.tokenize();
parse = lp.apply(rawWords2);

// PennTreebankLanguagePack for English
TreebankLanguagePack tlp = lp.treebankLanguagePack();
GrammaticalStructureFactory gsf = tlp.grammaticalStructureFactory();
GrammaticalStructure gs = gsf.newGrammaticalStructure(parse);
List<TypedDependency> tdl = gs.typedDependenciesCCprocessed();

String lista = null;
List<String> listaGrammaticale = new ArrayList<String>();
for (int i = 0; i < tdl.size(); i++) {
 lista = tdl.get(i).toString();
 listaGrammaticale.add(lista);
}

for (int j = 0; j < listaGrammaticale.size(); j++) {
 if (listaGrammaticale.get(j).startsWith("root")) {
  String partOfTheSentence = listaGrammaticale.get(j);
  System.out.println(partOfTheSentence);
  String[] parts = partOfTheSentence.split("[(]");
  setLabel(parts[0]);
  setName(parts[1]);

 }
}

}

presenta le stesse line di codice utilizzate nella classe NodeFactory. Trovarci in questa situazione
non significa solamente trovarsi in presenza di codice duplicato all'interno di una classe ma anche tra classi diverse ,"sporcando" ancora di più il nostre codice. In caso di cambiamenti siamo costretti a cercare all'interno del progetto tutte le classi che utilizzano quella determinata sequenza di istruzioni e modificarle: operazione soggetta ad errori e sinonimo di una "cattiva" progettazione delle classi.

Per ovviare al problema è possibile creare un classe a parte per la gestione dell'interfacciamento con le classi del parser di Stanford. In questa classe posizioneremo tutti quei metodi di utilità per il parser.

public class StanfordParserUtils {
 
 

 public StanfordParserUtils() {
  ...
 }


 public List<String> inizializeTreeDependencyList(
            String sentence, LexicalizedParser lp) {

  List<CoreLabel> rawWords = Sentence.toCoreLabelList(sentence);
  Tree parse = lp.apply(rawWords);

  TokenizerFactory<CoreLabel> tokenizerFactory = PTBTokenizer.factory(new CoreLabelTokenFactory(), "");
  Tokenizer<CoreLabel> tok = tokenizerFactory.getTokenizer(new StringReader(sentence));
  List<CoreLabel> rawWords2 = tok.tokenize();
  parse = lp.apply(rawWords2);

  // PennTreebankLanguagePack for English
  TreebankLanguagePack tlp = lp.treebankLanguagePack();
  GrammaticalStructureFactory gsf = tlp.grammaticalStructureFactory();
  GrammaticalStructure gs = gsf.newGrammaticalStructure(parse);
  List<TypedDependency> tdl = gs.typedDependenciesCCprocessed();

  String lista = null;
  List<String> listaGrammaticale = new ArrayList<String>();
  for (int i = 0; i < tdl.size(); i++) {
   lista = tdl.get(i).toString();
   listaGrammaticale.add(lista);
  }

  return listaGrammaticale;
 }

}

Cosi facendo, ogni qual volta dobbiamo eseguire un parser di un frase attraverso le strutture del Parser di Stanford, basterà chiamare il metodo
inizializeTreeDependencyList(...)

all'interno della classe StanfordParserUtils().


Parte IV - Attenti al "Cast"

Analizziamo il seguente codice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public List<Variable> relationMatch (Relation a){
List<Variable> ls = new Vector<>();
if ( this.label == a.label){
 if (a.in.getClass().equals(Variable.class)){
  if(a.out.getClass().equals(Variable.class)){
   ls.add(new Variable( ((Variable)a.in).name , new Word(((Word)this.in).surface)));
   ls.add(new Variable( ((Variable)a.out).name , new Word(((Word)this.out).surface)));
   return ls;
  }
  else if (((Word)a.out).surface == ((Word)this.out).surface){
   ls.add(new Variable( ((Variable)a.in).name , new Word(((Word)this.in).surface)));
   return ls;
  }
 }
 else if (((Word)a.in).surface == ((Word)this.in).surface) {
  if(a.out.getClass().equals(Variable.class)){
   ls.add(new Variable( ((Variable)a.out).name , new Word(((Word)this.out).surface)));
   return ls;
  }
 }
}
return null;
}

Alla riga 6,7,11,17 ci sono una serie di cast che devono essere controllati prima di essere eseguiti. Potrebbe succedere che le variabili che si stanno castando non sono le variabili oggetto del cast, generando una situazione di errore. In questi casi è necessario utilizzare, prima del cast, l'operatore instanceof 



...
if(a instanceof Variable && in instanceof Word){
 ls.add(new Variable( ((Variable)a.in).name , new Word(((Word)this.in).surface)));
...
}
...

lunedì 14 marzo 2016

Progetto in Java: Cruciverba - I buoni pattern di programmazione

In questo post elencherò alcuni pattern di programmazione utili per lo sviluppo di un "buon" programma prendendo spunto dagli elaborati che sono stati consegnati al Prof. Zanzotto per il corso di LMP.

Parte I - La Legge di Murphy

« Se qualcosa può andar male, andrà male.»
La legge di Murphy può essere utilizzata come uno dei principi cardine della Programmazione Difensiva. La programmazione difensiva consiste, in poche parole, nel cercare di prevenire tutte le possibili condizioni di errore che possono avvenire all'interno del codice. Supponiamo di avere il seguente metodo java :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean controllaVerticali(Schema s, ListaParole p) {
		boolean risultato = true;
		//controllo tutte le colonne
		//ma appena il controllo su una fallisce, interrompo il controllo
		//e restituisco false
		int r = 0; //indice della riga in cui inizia la parola "verticale" corrente
		String parolaCorrente;
		for (int c=0;c<s.schema[0].length && risultato ; c++){
			r = trovaProssimoNonNeroSuColonna(s,c,r); //cerco la prima lettera della prossima parola su questa colonna
			while (r<s.schema.length){ //controllo tutta la colonna corrente

				parolaCorrente = "";
				//aggiungo alla parola corrente tutti i caratteri fino alla prossima casella nera o alla fine della colonna 
				while(r<s.schema.length && s.schema[r][c].usabile){
					parolaCorrente += s.schema[r][c].carattere;
				}
				if (!controllaParolaSensata(parolaCorrente, p))
					return false;
				r = trovaProssimoNonNeroSuColonna(s,c,r+parolaCorrente.length()); //cerco la prima lettera della prossima parola su questa colonna
			}


		}
		return risultato;
	}

Alla linea 8 abbiamo la seguente istruzione:

for (int c=0;c<s.schema[0].length && risultato ; c++)
                    

L'istruzione precedente può generare condizioni di errore di questo tipo:
  • s è null
  • s.schema è null
  • s.schema[0] contiene un elemento uguale a null
  • s.schema[0] corrisponde ad un elemento non valido. Il vettore s.schema potrebbe non contenere alcun elemento.
Per evitare che l'applicazione termini con una una famigerata "NullPointerException" è necessario effettuare dei controlli sulle variabili che vengono utilizzate all'interno dei nostri metodi. Una possibile soluzione a questo problema è :

if(s != null && s.schema != null && s.schema[0] != null){
   for (int c=0;c<s.schema[0].length && risultato ; c++){
	  ...
	}
}

Parte II - Public vs Private e l' Information Hiding

Un altro aspetto importante della programmazione ad oggetti, specialmente nel caso del linguaggio java, è cercare di rispettare il principio dell'information hiding.
L'information Hiding si basa sul principio che i dettagli interni dell'implementazione di una classe dovrebbero essere nascosti alle altre classi: in questo modo è possibile migliorare la modularizzazione delle applicazioni. Scegliere se rendere un attributo di una classe private o pubblic riflette l'uso dell'information hiding sul nostro codice in quanto non permette alle altre classi di accedere a tale parte della classe se non attraverso la classe stessa.

Esamiamo il seguente metodo:


1
2
3
4
5
6
7
private boolean controllaParolaSensata(String parolaCorrente, ListaParole p) {
 for (Parola parola : p){
	 if (parola.stringa.equals(parolaCorrente))
		 return true;
	 }
 return false;
}

E' giusto accedere all'attributo parola.stringa della classe Parola in modo pubblico ? La risposta è no in quanto è un attributo che deve gestire solamente la classe Parola e non Schema. Inoltre, rendendolo privato, si elimina la possibilità di modificarlo direttamente: azione che deve accadere solamente tramite la classe che gestisce tale attributo (attraverso i metodi get e set).
Una soluzione a questo problema potrebbe essere la seguente:

public class Parola {
 private String stringa;
 ...
 public String getStringa() {
  if (stringa == null){
    string = "";
   }
  return stringa;
 }

In questo modo rendendo privato l'attributo e aggiungendo il metodo getString() obblighiamo il programmatore ad accedere all'attributo solamente tramite la classe Parola. In aggiunta è stato attuata anche una tecnica di programmazione difensiva prevenendo una delle situazioni descritte nella Parte I.
Un altro esempio che può aiutare a comprendere meglio il problema descritto precedentemente è il seguente:

1
2
3
4
5
6
7
8
// scrive all'interno della riga la parola
public static void scriviInCaselle(Schema schema, Parola parola, Coordinata coordinata) {
 int i;
 for (i = 0; i < parola.getParola().length(); i++) {
	schema.getCaselle()[coordinata.getX() + i][coordinata.getY()].setLettera(parola.getParola().charAt(i));
	schema.getCaselle()[coordinata.getX() + i][coordinata.getY()].setFree(false);
 }
}

Supponiamo di cambiare l'attributo caselle della classe Schema da Caselle[][] a Matrix (un oggetto che rappresenta una matrice). A questo punto, le istruzioni alla riga 5 e 6, non sono piu valide in quanto il tipo Matrix è diverso da tipo Caselle[][] e non è possibile accedervi con l'istruzione
[coordinata.getX() + i][coordinata.getY()]
Cambiando il tipo dell'attributo caselle è necessario cambiare tutte le istruzioni di codice del tipo [coordinata.getX() + i][coordinata.getY()] presenti all'interno del nostro programma. Questo problema è sinonimo di un alto accoppiamento tra la classi che stiamo utilizzando e la classe Schema: effettuare delle modifiche alla classe Schema comporta modificare tutte le classi che fanno utilizzo di quest'ultima.
Per risolvere questo problema è utile introdurre un metodo all'interno di Schema che restituisca una casella



public class Schema {
 ...
 public Casella getCasella(int x, int y) {
    return caselle[x][y];
 }
}

In questo modo se in futuro dovesse cambiare il metodo di accesso ad una casella sarà necessario modificare solamente il codice all'interno del metodo getCasella(...).

III - L'arabo è difficile da capire

Un aspetto fondamentale che spesso viene trascurato è l'inserimento dei commenti all'interno del codice. I commenti non solo aiutano a capire il flusso del programma ad un programmatore che non non conosce il programma ma anche a far riflettere sulla implementazione del programma: processo che può essere utile per rivedere la logica del codice.
Ad esempio:

private Schema controllaColonna(int row, Schema s, ListaParole p, Stack<Insieme> pila) {
boolean check = true;
int j = 0;
for(; j < s.schema.length - 1 && check; j++){
 if(!s.schema[j][row].piena && !s.schema[j+1][row].piena){
	ArrayList<Character> parola = new ArrayList<Character>();
	int z = 0;
	for(z = 0; j < s.schema.length - 1 && !s.schema[j+1][row].piena; j++, z++){
		parola.add(z, s.schema[j][row].carattere);
	}
	if(z == s.schema.length - 1){
	 parola.add(z, s.schema[j][row].carattere);
	}
	z = 0;
	for(; z < p.size() && z >= 0; z++){
	 if(parola.toString().compareTo(p.get(z).stringa)==0){
		z = -1;
	  }
	}
	if(z == p.size()){
	  pila.pop();
	  check = false;
	}
 }
}
if(j == s.schema.length - 1){
	Schema schem = s.clona();
	return schem;
}
return null;
}

L'assenza di commenti all'interno del codice precedente non permette una immediata comprensione della logica utilizzata all'interno del metodo. Inoltre è necessario dare un nome significativo alle variabili, specialemente quando si lavora con molti indici (per i cicli for).