Archivi tag: howto

Scala e Pattern Matching

Ultimamente ho iniziato a giocare con Scala, passato agli onori della cronaca per essere il linguaggio con cui è scritto Apache Spark, uno dei motori di data-processing più in voga nel mondo dei Big Data (e che ha soppiantato da tempo Pig e altre tecnologie basate su Map&Reduce).

Uno degli aspetti più interessanti, riguarda la possibilità di usare dei Pattern di Matching per tipo (sia scalare che su oggetti) per evitare le righe e righe di codice di instanceof , type ,if-else, o semplicemente realizzare la tanto agognata switch basata su stringhe. Altro utilizzo molto interessante del Pattern Matching potrebbe essere quello di eseguire diverse azioni sulla base del tipo di dato incontrato, durante il traversal di strutture composite, come ad esempio un DOM Xml.

Quello che in Java si scrive con diverse if, in Scala si riduce all’essenziale.
Il seguente pezzo di codice, definisce una funziona X che torna una stringa ed accetta qualsiasi tipo di dato in input (Any in scala è l’equivalente di Object) . Un Esempio classico è quello della decodifica dei parametri passati come argomento al nostro main.

def parseMainArgument(arg:String) = arg match{
  case "--help" | "-h" => displayHelpMessage
  case "--log"  | "-l" => showLog
  case "--verbose" | "-v" => setVerbose
  case _  => showArgumentNotFound
}

Il carattere underscore “_” sta a significare il valore di default, quando nessuno dei suddetti viene invocato, quindi nella switch sopra descritta, sta a rappresentare il caso “else”..

Un esempio di utilizzo di più valori nel match:

def myDecodeParam(funcName: String, value: Any) = (funcName,value) match{
   case ("test",val) => test(val)
   case ("log",msg) => log("messaggio:"+msg)
}

Altro aspetto interessante dei match, è la possibilità di analizzare i tipi di dati, ad esempio:

def myfunc(x:Any):String = x match{
   case i:Int => ""+ i + "è un Intero"
   case d:Double => "" + d + "+ un Double"  
   case s:String => s+ " è una Stringa"
}

Un utilizzo frequente è quello tramite le Case Classes.
Le Case Classes sono classi regolari che esportano in automatico i parametri del costruttore e che è possibile poi scomporre e analizzare attraverso il Pattern Matching. Le Case Classes non richiedono l’utilizzo della new, ed il compilatore genera in automatico il metodo “equals” ed il “toString”.

Un esempio classico, riportato anche dalla documentazione, è quello della definizione di classi case per la scomposizione e risoluzione di formule matematiche, ma che è facile poi utilizzare per creare un DSL applicativo.
In questo esempio definiamo una serie di case classes che descrivono operazioni matematiche che vengono svolte in quello che definiamo un ambiente di test, un insieme di valori che assumono le variabili del calcolo. Invece di usare una collection per la definizione delle variabili, lo facciamo utilizzando le funzioni. La notazione

{case "x" => 5}

ad esempio crea una funzione che quando riceve x in input ritorna il valore “5”.
la sua definizione è :

type NomeFunzione = String => Int

dove String è l’input, e Int l’output.

In questo esempio usiamo anche i Traits,cioè i tratti, le caratteristiche da aggiungere alle classi. Sono interfacce con la possibilità di avere implementazioni parziali.

trait Espressione {
    def decodifica(caso:AmbienteTest.Ambiente) : String = {
      this match {        
        case Somma (a,b) => "(" + a.decodifica(caso) +" + " + b.decodifica(caso) + ")"
        case Sottrai (a,b) => "(" + a.decodifica(caso) +" - " + b.decodifica(caso) + ")"        
        case Moltiplica (a,b) => "(" + a.decodifica(caso) +" * " + b.decodifica(caso) + ")"
        case Dividi (a,b) => "(" + a.decodifica(caso) +" / " + b.decodifica(caso) + ")"        
        case Variabile(x) => "" + caso(x)
        case Costante(n) => "" + n
      }
    }
   
    def risolvi(caso:AmbienteTest.Ambiente) : Int = {
      this match {  
        case Somma (a,b) =>a.risolvi(caso) + b.risolvi(caso)
        case Sottrai (a,b) =>a.risolvi(caso) - b.risolvi(caso)
        case Moltiplica (a,b) =>a.risolvi(caso) * b.risolvi(caso)
        case Dividi (a,b) =>a.risolvi(caso) / b.risolvi(caso)
        case Variabile(x) => caso(x)
        case Costante(n) =>  n
      }
    }
   
}

case class Somma (a:Espressione, b:Espressione) extends Espressione
case class Sottrai (a:Espressione, b:Espressione) extends Espressione
case class Moltiplica (a:Espressione, b:Espressione) extends Espressione
case class Dividi (a:Espressione, b:Espressione) extends Espressione
case class Variabile (x:String) extends Espressione
case class Costante (n:Int) extends Espressione
package object  AmbienteTest {type Ambiente =  String => Int}

object ExpressionDecoder {
  def main(args:Array[String]){
     val myTest:AmbienteTest.Ambiente = {case "x"=>1  case "y"=>2}
     val exp:Espressione =Sottrai(Moltiplica(Somma(Somma(Costante(5),Variabile("x")),Variabile("y")),Costante(2)),Variabile("x"))
     println("Espressione: "+exp.decodifica(myTest));
     println("Risultato: "+exp.risolvi(myTest));
  }
 
}

Il risultato in console dell’esecuzione del codice sopra esposto è:

Espressione: ((((5 + 1) + 2) * 2) - 1)
Risultato: 15

Come possiamo vedere, i pattern matching con Scala, aprono tutta una serie di alternative alla programmazione per “IF” , arrivando a rendere più snello e leggibile il nostro codice.

MongoDB , JavaDriver e Full Text Search

Con le nuove versioni di MongoDB, è stata portata alla luce, anche sul driver java ,la possibilità di avere indici testuali su cui effettuare ricerche.

Per definire l’indice ed eseguire una ricerca:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 MongoClient mongo = new MongoClient("localhost",27017);
 DB db = mongo.getDB("persons");
 DBCollection table = db.getCollection("personCollection");
 //creo un indice full text sulla "colonna" name dell'oggetto Person
 table.createIndex(new BasicDBObject("name","text"));
 BasicDBObject search= new BasicDBObject("$search","valori da ricercare separati da spazio");
 BasicDBObject textSearch = new BasicDBObject("$text",search);
 DBCursor cursor = tablefind(textSearch);
 personList = new ArrayList<Person>();
       
 while (cursor.hasNext()){
  DBObject obj = cursor.next();
  Person pers = (new Gson()).fromJson(obj.toString(), Person.class);
  personList.add(pers);
 }

Fare attenzione alla versione che si usa di MongoDB perché nelle versioni precedenti la full text search non era abilitata di default.

Interessante la possibilità di ottenere come risultato l’item con il più alto numero di match di parole chiave inserite:

1
2
3
4
  BasicDBObject search= new BasicDBObject("$search","valori da ricercare separati da spazio");
  BasicDBObject textSearch = new BasicDBObject("$text",search);
  BasicDBObject score= new BasicDBObject("score", new BasicDBObject("$meta", "textScore"));
  bestItem= coll.findOne(textSearch, score);

MongoDB e Java Driver,esempi vari

Iniziamo a giocare con MongoDB tramite il java driver.
Consiglio il download della versione 2.12.3 o successive (fate attenzione che i downloads della libreria non sono in ordine numerico, e quindi si rischia spesso di scaricarne una vecchia).
Altra libreria utile per le conversioni da pojo a stringa, la libreria Google gson, versione 2-2-4.

Una volta messo il jar nel classpath della nostra applicazione, e fatto partire il nostro server database mongodb, stabiliamo la connessione, al server e otteniamo il riferimento al database che vogliamo usare, in questo caso il db “person”. Ottenuto il DB, possiamo accedere alla collection dei nostri futuri oggetti da persistere (se la collection non esiste, viene creata in questa fase).

1
2
3
MongoClient mongo = new MongoClient("localhost",27017);
DB db = mongo.getDB("persons");
DBCollection table = db.getCollection("personCollection");

Realizziamo un pojo classico per la gestione dell’entità “Person” da persistere su MongoDB.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.io.Serializable;

public class Person implements Serializable{

    private static final long serialVersionUID = -8413862529676269210L;
    private String id = java.util.UUID.randomUUID().toString();
    private String name;
    private String lastname;
    private Integer age;
   
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getLastname() {
        return lastname;
    }
    public void setLastname(String lastname) {
        this.lastname = lastname;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
   
    public String getId() {
        return id;
    }
   
    public void setId(String id) {
        this.id = id;
    }
   
   
}

In questo esempio generiamo un UUID come id dell’oggetto, ma potremmo usare l’hashcode.

Per effettuare una insert sulla collection di cui sopra:

1
2
3
4
5
6
7
8
9
10
//istanza dell'oggetto Person da inserire
Person persona = new Person();
persona.setName("cesare");
//utilizziamo JSon per la conversione automatica da pojo a BasicDBObject

Gson gson = new Gson();
BasicDBObject personDBO =(BasicDBObject) JSON.parse(gson.toJson(persona));

//table è la DBCollection indicata nel primo esempio
table.insert(personDBO );

Col nuovo driver è possibile usare operazioni batch in automatico passando all’insert un array

1
2
3
4
5
6
7
8
List<DBObject> dbObjList = new ArrayList<DBObject>();
for(int i =0;i<100;i++){
 Person p = new Person();
 p.setName("test"+i);
 BasicDBObject pDB=(BasicDBObject) JSON.parse(gson.toJson(p));
 dbObjList.add(pDB);
}
table.insert(dbObjList);

Per la modifica di un oggetto in una collection (per definizione con MongoDB parliamo in realtà di documenti), è possibile effettuare direttamente il replace dell’oggetto con una nuova istanza.

1
2
3
4
5
6
7
 //creo un dbObject per eseguire la find di un elemento che abbia la chiave name uguale a 'test1':
 BasicDBObject personOriginal = new BasicDBObject().append("name", "test1");
 
 //e sostituirlo con questo nuovo oggetto
 BasicDBObject personNew = new BasicDBObject().append("name","nuovotest1").append("lastname","kiraya");

 table.update(personOriginal,personNew);

Per rimuovere un elemento dalla collection,è possibile indicare un criterio di ricerca oppure un DBObject specifico da eliminare:

1
2
3
 BasicDBObject personQuery = new BasicDBObject().append("name","nuovotest1");
 BasicDBObject personDB = table.findOne(personQuery);
 table.remove(personDB);

Per svuotare completamente una collection, è possibile eseguirne il drop:

1
 table.drop();

Oppure usare un BasicDBObject “byExample” che non contenga dati, il che sta a significare una rimozione senza nessun particolare criterio di filtro (e quindi tutti):

1
table.remove(new BasicDBObject());

E’ possibile filtrare i dati in diversi modi, utilizzando le query byExample, utilizzando il QueryBuilder oppure tramire Regular Expressions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//by example il contenuto deve matchare completamente
DBCursor cursor = table.find(new BasicDBObject("name","test1"));
while (cursor.hasNext()){
  DBObject obj = cursor.next();
  Person pers = (new Gson()).fromJson(obj.toString(), Person.class);
  System.out.println(pers.getName());
}

//tramite regexp utilizzando un pattern per simulare una like %nome%
BasicDBObject filter= new BasicDBObject();
filter.append("name", Pattern.compile("nuovo"));
filter.append("lastname",Pattern.compile("kira"));
cursor = table.find(filter);
while (cursor.hasNext()){
  DBObject obj = cursor.next();
  Person pers = (new Gson()).fromJson(obj.toString(), Person.class);
  System.out.println(pers.getName());
}

Per estrarre tutti gli elementi di una collection basta eseguire un find senza parametri:

1
DBCursor cursor = table.find();