Kotlin: Che cos’è e cosa serve sapere ai programmatori Java

K

Che cos’è Kotlin?

Ammettiamolo. Se non ne avete mai sentito parlare, “Kotlin” è un nome che suona molto strano in un contesto in lingua italiana, soprattutto se pronunciato ad alta voce!

Kotlin è un linguaggio open-source, multi-paradigma (fondamentalmente ad oggetti), sviluppato dalla JetBrains, l’azienda conosciuta per la qualità dei suoi IDE, i più famosi per Java (IntelliJ IDEA) e Android (Android Studio).

Cosa serve sapere ai programmatori Java?

Sia IntelliJ IDEA che Android Studio, sono IDE sviluppati in Java e disponibili quindi su tutte le piattaforme (Windows/Mac/Linux).

Java inoltre è il linguaggio su cui è basato l’SDK Android, quindi anche il linguaggio primario utilizzato per scrivere applicazioni per Android.

In simbiosi con questo ecosistema fondamentalmente basato sul Java in cui si trova JetBrains, il Kotlin, è stato pensato per compilare primariamente sulla JVM, su cui vanta un 99.9% di compatibilità.

Per tale ragione, sappiate che quando scrivete in Kotlin, potrete tranquillamente usare vostre classi/librerie scritte in Java (o anche compilati .jar), e persino fare l’opposto: ovvero sfruttare codice che avete scritto in Kotlin da Java. Si dice dunque che Kotlin e Java sono linguaggi interopatibili, dunque non c’è discriminazione tra una classe o una libreria scritta in Kotlin e una scritta in Java quando lavorate.

Come se non bastasse, sia IntelliJ IDEA che Android Studio, offrono la conversione automatizzata di un file Java in Kotlin se volete provare

Perché considerare Kotlin (e su Android)

Ok, Kotlin è interopatibile con Java. Ma perché usarlo innanzitutto? Perché non affidarsi a Java direttamente che ha decenni di storia e praticità?

Perché Kotlin 1.0 è stato rilasciato a Febbraio 2016. Java 1 nel 1995, e il codice scritto in esso è retrocompatibile da allora in Java 9, portandosi tutte le vecchie features e scelte di design.

Questa differenza di 21 anni nel caso ve lo state chiedendo, si sente, in termini di scelte di design.

Inoltre, nel Google I/O 2017, Google ha dichiarato Kotlin come linguaggio ufficiale Android tanto quanto Java.

Kotlin, essendo di JetBrains, così come lo è Android Studio, era già integrato prima in esso come plug-in (come già detto, Kotlin compilava sulla JVM già da prima quindi è sempre stato interopatibile).

Dopo l’annuncio del supporto ufficiale tuttavia, si trova pre-installato da Android Studio 3.0 e durante la creazione di un nuovo progetto, è disponibile una spunta per utilizzare Kotlin.

Dunque scegliere Kotlin al posto di Java (o insieme a Java) non è solo una scelta di affidarsi al “nativo” al 100%, ma c’è anche Google che vi dà l’ok ;]

Perché usare Kotlin al posto di Java

Avendo stabilito l’interopatibilità totale tra Java e Kotlin, è lecito porsi questa domanda. E a questa domanda si possono fornire diverse risposte, anche se sostanzialmente dipende anche dal contesto in cui vi troviate, e dalle regole stabilite dalla vostra azienda se non spetta a voi la decisione.

Una riga di codice vale più di mille parole, dunque, vediamo degli esempi pratici:

Esempio #1: La NullPointerException

Diciamocelo, odiata da tutti i programmatori Java. Oggetto di sfogo nei meme su Facebook e nei forum di programmazione, la NullPointerException è quell’antipatica circostanza che incombe a runtime quando deferenziate (implicitamente) un riferimento nullo.

// Ipotizziamo una classe persona con un attributo nome, uno cognome, e opportuni metodi getter e setter

Persona persona = null;
...
if (persona.getNome().equals("John") ) { // Possibile NPE qui
    System.out.println("Ciao Mr.");
}

L’utilizzo dell’operatore punto (.) per chiamare un metodo infatti, restituirà un risultato se state chiamando il metodo di un oggetto, ma rilancerà a runtime un’eccezione se la variabile corrisponderà al riferimento/valore speciale null. A runtime vuol dire che la causa dell’eccezione siete stati voi (ovvero avete commesso un errore logico) e non una causa esterna di cui il programmatore doveva essere messo in guardia (come l’accesso su file).

Spesso è prassi dunque verificare che le variabili non siano nulle prima di chiamare metodi di istanza.

Si è dunque tentati a scrivere qualcosa del tipo:

Persona persona = null;
...
if (persona != null && persona.getNome().equals("John") ) { // Nessuna NPE qui
    System.out.println("Ciao Mr.");
} else {
    // Cosa si fa qui?
}

Aumentando la verbosità sostanzialmente quando ci sono tante variabili possibilmente nulle di cui tenere in conto, magari di un oggetto con molti attributi senza garanzia di inizializzazione fatta.

Dove ci aiuta Kotlin?

Riscriviamo il nostro esempio in Kotlin adesso. La sintassi verrà spiegata più tardi.

class Persona {
    val nome: String
    val cognome: String
 
    constructor(nome: String, cognome: String) {
        this.nome = nome
        this.cognome = cognome
    }
}

fun main(args: Array<String>) {
    var persona: Persona = null

    if(persona.name.equals("John")) {
        print("Hello sir")
    }
}

Bene, adesso qualcuno sarebbe tentato di dire in questo snippet. Dov’è la sicurezza in più? In cosa ci sta assistendo Kotlin?

La risposta è banale: Questo programma non compilerà.

Prendetevi un attimo. Bene. Allora, partiamo dal presupposto che in Kotlin, a tempo di compilazione, non runtime, dovete esattamente sapere quali variabili potranno avere un valore nullo in principio.

La riga incriminata è questa

var persona: Persona = null

Un oggetto di tipo <inserisci classe qui> non può essere nullo. Deve riferirsi a un oggetto esistente, anche nei tipi primitivi (Int, Double, Float, ecc.) e quelli non-così-primitivi come le Stringhe. L’esempio può sembrare banale. Ma pensate invece ad esempi dove avete un Integer, e i valori null e 0 possono avere significati diversi, oppure semplicemente, avete un oggetto di una classe scritta da voi, che non ha un accettabile valore di default.

Se persona può assumere un valore nullo, la dichiarazione dev’essere così.

var persona: Persona? = null

Quel punto interrogativo (?) stabilisce che persona può avere in qualsiasi momento nel suo scope un valore nullo. E non solo, ma voi vi prenderete la responsabilità esplicitamente di questa possibilità quando vorrete lavorare con str. Infatti, se copiate l’if dello snippet di prima, sorpresa, non compilerà quello adesso. Perché starete provando a chiamare un metodo di un oggetto che potrà essere nullo, causando l’odiatissima NPE.

Come bisogna fare dunque?

if(persona!!.name.equals("John")) {
    print("Hello Mr.")
}

Quel doppio punto esclamativo (!!) è inequivocabile. Solo scrivendolo, state dando la vostra parola di boy-scout che persona non sarà nullo, garantito da voi, pena NPE assicurata.

Come riduce la verbosità?

Se ammettete che persona possa essere nullo, e in tal caso non fare nulla, allora basterà utilizzare l’operatore ?. per eseguire l’azione solo se la variabile non è nulla

if(persona?.name.equals("John")) {
    print("Hello Mr.")
}

Equivalente del secondo snippet Java, col null-check.

Ancora meglio, è possibile scrivere

if(persona?.name == "John") {
    print("Hello Mr.")
}

Esatto, una comparazione tra stringhe con l’== senza che sia un errore logico e faccia saltare un warning al compilatore. Adesso i vostri colleghi C# vi guarderanno con un po’ di aria di superiorità in meno  :]

Il confronto di identità invece è dato in Kotlin agli operatori === e !==.

OPTIONALS. CONCLUSIONE?

Quest’annotazione (? dopo il tipo) vale per tutti i tipi con cui potete avere a che fare in Kotlin. Può sembrare banale in primis, ma se nella vostra funzione, le variabili saranno di tipo Classe1, Classe2, Int, String, ecc. senza punti di annotazione, sapete senza nemmeno dover eseguire il vostro codice, che nessuna variabile nulla sarà deferenziata.

Viceversa, tutti gli oggetti potenzialmente problematici saranno bollati con il suddetto simbolo.

Esempi più subdoli con i wrapper dei primitivi (in Java) possono essere:

Float f = null;
...
if (f < 0.0f) { // Possibile NPE qui
    ... 
}

che se riprodotto così in Kotlin, anche se con Float? anziché Float, risulterà in un errore di compilazione all’if.

E se unA VARIABILE può essere nullA ma voglio entrare in un contesto in cui non lo sarà più?

Supponiamo che col vostro oggetto dovete chiamare non uno ma diversi metodi diverse volte. Ogni ?. corrisponderà a un if-then sotto il cofano. Invece ogni !! corrisponderà a un “posso avere una brutta eccezione qui”. Il cosiddetto optional-unwrapping (nome rubato da Swift) consiste nel verificare la nullità per entrare in un contesto dove ce ne si priva. Vediamo un esempio.

var person: Persona? = null
...
if (person != null) {
    print(person.nome + " " + person.cognome)
}

Osservate che una volta fatto il null-check, non occorrerà più scrivere !! per utilizzare l’oggetto. Infatti il compilatore inferrà che person non sarà più null, dunque come fosse di tipo Persona anziché Persona?.

Dunque è chiaro. Senza la notazione !!, non avrete mai più NullPointerException nella vostra vita (a meno che non chiamiate codice Java!)

Vediamo adesso le altre novità e caratteristiche di Kotlin.

#2 Type-Inference alla dichiarazione

In Kotlin una variabile si dichiara con la seguente notazione:

var nomeVariabile: TipoVariabile

Se l’istanziazione è nella stessa riga della dichiarazione, è possibile omettere il tipo, inferendolo.

var stringa: String  = "Ciao mondo!"
var stringa = "Ciao mondo!"          // equivalente

Il tipo della variabile rimane definito a compile-time, quindi, dopo che inizializzate la variabile, anche se non specificate il suo tipo, non è possibile assegnare un valore di un tipo diverso.

var stringa = "Ciao mondo!"
stringa = 2 // errore di compilazione! stringa è di tipo String

La type-inference risulta particolarmente comoda in casi del tipo

// Java
ClasseConUnNomeVeramenteLungo myObject = new ClasseConUnNomeVeramenteLungo();

// Kotlin
var object = ClasseConUnNomeVeramenteLungo()

dove c’è un’inutile ripetizione espressiva con nomi molto lunghi di classi, oppure abbiamo a che fare con parametri generici innestati.

Inoltre come si può notare, in Kotlin è stata rimossa la keyword new in fase di istanziazione. Ciò è stato fatto perché new risale ai tempi del C++ che già esisteva da 12 anni quando è stato annunciato Java.

Perché allora eliminare una parola che ricalca una convenzione?

Ci sono più ragioni. Una è che nel C++, dove l’allocazione e la dealloczione della memoria erano affidate al programmatore, il senso della keyword new era quello di ricordare al programmatore che andava fatta una corrispondente chiamata delete per pulire la memoria. Alternativamente, quella memoria non sarebbe mai stata liberata fino a fine esecuzione del programma (memory leak). Nelle funzioni si utilizzava una simile convenzione. Se una funzione ritornava un oggetto allocato nella memoria heap, la funzione era solita ad avere new nel nome (ad esempio newInstance() ).

Sia in Kotlin che Java tuttavia, la gestione della memoria è completamente automatica, regolata dal Garbage Collector (GC). Dunque tali accorgimenti non sono necessari.

La seconda ragione è che si vuole trattare la chiamata di un costruttore come quella di una funzione.

Il modo in cui si distinguono resta convenzionale. Ovvero le classi hanno normalmente un nome con l’iniziale maiuscola (UpperCamelCase o PascalCase). Le funzioni/metodi invece hanno il nome con l’iniziale minuscola (camelCase).

Si può saperne di più nelle Coding Conventions.

#2.5 Costanti

Una particolare menzione va fatta alle costanti. In Java per dichiarare una variabile come costante, va posta la keyword final prima della dichiarazione, e spesso viene omessa dai programmatori anche se non hanno nessuna intenzione di modificare la variabile che dichiarano.

In Kotlin tuttavia, dichiarare una costante è costoso in lettere da digitare tanto quanto dichiarare una variabile.

Infatti, basterà semplicemente usare la keyword val anziché var

val stringa = "Ciao mondo!"
stringa = "Qualcos'altro" // errore di compilazione

Il consiglio è di usare val di default, e cambiare in var se avete la necessità di avere una variabile e non una costante.

Il linter di IntelliJ/Android Studio per rinforzare questa convenzione, vi darà un warning se dichiarate variabili senza modificarle.

#3 Le Properties E LE DATA CLASSES

Avete presente quando dovete avere una classe che rappresenta un’entità come una Persona ad esempio, e solo per memorizzare i suoi dati anagrafici, mantenendovi allo stile dell’OOP, vi ritrovate con un file con quasi 100 righe di codice? Come le classiche classi POJO, oppure semplicemente delle classi che rappresentano entità che incorporano anche una logica (immaginate un gioco di carte) oltre ai dati.

Riprendiamo la classe Persona di prima, scrivendo questa volta i nomi degli attributi in inglese, e fermandoci solo a nome e cognome per il bene della lunghezza di questo articolo.

class Person {
    private String name;
    private String surname;

    public Person() {
        
    }
    
    public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }
}

In Kotlin lo abbiamo scritto così prima

class Person {
    var name: String
    var surname: String

    constructor(name: String, surname: String) {
        this.name = name
        this.surname = surname
    }
}

La mancanza di getters e setters non è stata una scelta deliberata per abbreviare il codice.

Infatti se la dichiarazione var name: String corrisponde a una variabile se scritta nel corpo di un metodo o una funzione, nel corpo di una classe, non corrisponde semplicemente a un attributo come in Java, ma corrisponde a una property, cioè un attributo con getters e setters.

Attualmente, c’è un mapping sotto il cofano con i metodi getter e setter per quanto riguarda Java, e la loro assenza per quanto riguarda Kotlin.

Ciò vuol dire che se avete una classe Java come quella sopra, in Kotlin, potrete riferirvi a namesurname come sono dichiarati nella forma Kotlin. Se un setter manca o non è visibile in Java, invece potrete accedervi in sola lettura in Kotlin.

Allo stesso tempo, se avete una classe Kotlin come quella di sotto, da Java, dovrete usare i getters e i setters per modificare i valori o leggerli da un oggetto.

Ma a dire la verità, anche qui c’è della verbosità. Sono ancora ben 8 righe di codice per una classe con 2 attributi.

Si può riscrivere tutto infatti così:

class Person(var name: String, var surname: String)

Il costruttore, i metodi getter e setter esposti a Java, sono implicitamente definiti. Non è fantastico?

Se pensate inoltre che abbia senso avere oggetti di tipo Person copiabili per valore e non solo per riferimento, non vi serve scrivere manualmente il metodo di copia

data class Person(var name: String, var surname: String)

E adesso, sono disponibili dei factory method copy(…) per copiare l’oggetto.

var person = Person("John", "Smith")
var p2 = person.copy()
p2.name = "Joe"
print(person) // Person(nome=John, cognome=Smith)

// oppure

var person = Person("John", "Smith")
var p2 = person.copy(name = "Joe")
print(person) // Person(nome=John, cognome=Smith)

Se ritenete che in un oggetto i campi debbano essere immutabili dopo l’inizializzazione, è sufficiente sostituire var con val, che rende immutabile la dichiarazione.

val str: String = "Hello World"
str = "Ciao Mondo" // errore. non si può ri-assegnare un val

#4 STRING-INTERPOLATION

Questo articolo si pone di mostrare come la programmazione di tutti giorni in Kotlin può essere più semplice e piacevole con una sintassi più versatile e immediata.

Il Java offre della verbosità un po’ in tutto.

Prendiamo ad esempio i modi convenzionali di convertire un intero (primitivo) in String in Java:

int x = 42;

String str1 = Integer.toString(x);
String str2 = String.valueOf(x);

e forse il più comune ma “magico”

String str3 = x + "";

x + "", con x intero (o qualsiasi altro tipo) “sommato” con una stringa vuota restituisce una stringa!

Naturale, no?

Questo meccanismo avviene perché l’overload dell’operatore + in Java per le stringhe è definito da un supporto speciale dal compilatore, che istanzia uno StringBuilder, oggetto per costruire le stringhe a parti con performance buone (essendo le stringhe immutabili), e accoda ogni operando per poi restituire la stringa.

Rendendo dunque la dichiarazione equivalente a:

String str3 = new StringBuilder().append(x).append("").toString();

Per formattare una stringa in Java, è comune utilizzare o quest’approccio (operatore +), oppure utilizzare un factory method che ricalca il C.

Person john = new Person("John", "Smith");

// StringBuilder
String output1 = "Ciao " + john.getName() + " " + john.getSurname() + ", come stai?";
// Formattazione alla C
String output2 = String.format("Ciao %s %s, come stai?", john.getName(), john.getSurname());

In Kotlin è disponibile una forma ben definita a compile-time, naturale e non verbosa. Essenzialmente si basa sul mettere il $ davanti al nome della chiamata al codice nella stringa, e ponendo il codice nelle parentesi graffe se si tratta di un’espressione più complessa.

// Convertire int in stringa
var x = 42
var output = "$x" 
// Alternativamente
output = x.toString()

// Formattare una stirnga
var output2 = "Ciao ${person.name}  ${person.surname}, come stai?"

Anche se ci si trova dentro le apici, se commettete un errore di sintassi, il compilatore ve lo farà notare perché il codice dopo il $ è valutato in compilazione.

#5 La programmazione funzionale E I TIPI FUNZIONE

Dopo aver introdotto le basi di Kotlin che possono interessare ai programmatori Java, senza allo stesso tempo esporre la sintassi costrutto per costrutto come un manuale, è possibile farsi un’idea di com’è Kotlin.

Come si può notare, ci sono minori differenze, come la mancanza di punti e virgola obbligatori, la mancanza della keyword new alla chiamata del costruttore, la possibilità di etichettare i parametri delle chiamate a funzione o istanziazione di oggetto (es. Persona(nome = “John”, cognome = “Smith”), la possibilità di scrivere funzioni al di fuori delle classi (ma che ne sa Java?), eccetera eccetera, osserviamo gli aspetti interessanti del linguaggio che lo rendono attraente al di là di una sintassi un po’ più semplice.

Modi di dichiarare funzioni

Innanzitutto, come si dichiara una funzione in Kotlin?

A differenza del Java o altri derivati del C, in Kotlin la dichiarazione di una funzione comincia con fun, seguita dal nome della funzione, i parametri (con sintassi analoga a quella delle variabili quanto al loro tipo), e infine il tipo della funzione se c’è, seguito dai due punti (:).

Vediamo un esempio

fun sum(x: Int, y: Int): Int {
    return x + y
}

Oppure per funzioni dalla logica nulla/modesta:

fun sum(x: Int, y: Int): Int = x + y

C’è anche un altro metodo, che vedremo adesso.

Il tipo funzione

Il paradigma funzionale nei linguaggi ad oggetti consiste nel trattare le funzioni come cittadini di prima classe, ovvero esattamente come sono trattati gli oggetti.

Essendo cittadini di prima classe le funzioni in Kotlin, è possibile, in aggiunta a quanto è possibile fare nel classico Java (cioè definirle e chiamarle), assegnarle a variabili, passarle come parametro ad altre funzioni, e persino allocarne istanze a runtime. (Proprio come gli oggetti).

Ad esempio, una variabile potrebbe esprimere come tipo, una funzione con due parametri interi che restiuisce un altro intero, come la funzione somma di prima.

fun sum(x: Int, y: Int): Int {
    return x + y
}

fun main(args: Array<String>) {
    var operation: (Int, Int) -> Int

    operation = sum
    println(operation(10, 15)) // 25
}

Proviamo adesso a sfruttare più dinamismo, con una funzione che prende in input due operandi, un operatore, e restituisce il risultato.

fun sum(left: Int, right: Int): Int {
    return left + right
}

fun calculator(lhs: Int, rhs: Int, operator: (Int, Int) -> Int): Int {
    return operator(lhs, rhs)
}

fun main(args: Array<String>) {
    println(calculator(10, 5, ::sum))

    // Definiamo una funzione lambda
    println(calculator(10, 5, fun(x,y): Int { return x * y }))

    // Abbreviamo la notazione precedente
    println(calculator(10, 5, { x,y -> x * y }))

}

Abbiamo prima chiamato calculator con la funzione definita sopra. Poi con due funzioni lambda, ovvero funzioni anonime, non dichiarate ma passate direttamente come parametro. Il secondo modo si rifà anche al concetto di literal, ovvero di una notazione di definizione simbolica così come le stringhe, ma con le parentesi graffe {}.

::sum rappresenta il riferimento alla funzione sum. La notazione :: sta ad indicare che ci si vuole riferire a una funzione. Il motivo per cui altrimenti non funzionerebbe in questo caso, deriva direttamente dal Java: In Java e in Kotlin, le funzioni si trovano in uno spazio dei nomi differente da quello delle variabili/costanti/attributi. Dunque, se si scrivesse solo sum, ci sarebbe ambiguità con una variabile locale, globale o di istanza di oggetto/classe. (In quanto è possibile avere in una classe sia un attributo sum che una funzione sum())

Una calcolatrice in Kotlin

Vediamo come è possibile scrivere con uno stile funzionale quindi una semplice calcolatrice in Notazione polacca.

Allora, prendiamo un esempio di espressione in notazione polacca

* 2 + 3 4

L’espressione la si può valutare da destra a sinistra, valutando prima + 3 4 = 7, e successivamente * 2 7 = 14, come se l’operatore fosse una funzione, e i due numeri dopo, i parametri della funzione (detti operandi appunto).

Come si può notare, l’espressione è equivalente in forma classica a 2 * (3 + 4), ma scritta in una struttura da non dover avere parentesi né generare ambiguità.

Se vogliamo scomporre in token la stringa di input, è sufficiente separare gli spazi (nella nostra convenzione). Dopo di ché i numeri saranno token operandi. I simboli token operatori

data class Token(val type: Type, val value: String) {
 enum class Type { OPERAND, OPERATOR }
}

Adesso, creiamo un array associativo (Map), con le stringhe che rappresentano gli operatori come chiave, e come valore, il riferimento a funzione per le corrispettive operazioni (useremo quelle della libreria standard Kotlin/Java)

val operatorMapper: HashMap<String, ((Double, Double) -> Double)> =
        hashMapOf(
                "+" to Double::plus, "+" to Double::plus,
                "*" to Double::times, "×" to Double::times,
                "-" to Double::minus, "−" to Double::minus,
                "/" to Double::div, "÷" to Double::div,
                "%" to Double::rem)

Rompiamo riga per riga questa dichiarazione

val operatorMapper: HashMap<String, ((Double, Double) -> Double)>

operatorMapper è un HashMap, con chiave di tipo Stringa, e come valore, una funzione con due parametri Double, che restituisce in output un altro Double.

= hashMapOf(
                "+" to Double::plus, ...,
                "*" to Double::times,
              ...)

Inizializzazione comoda per associare a ogni stringa, un riferimento a funzione. “+” come chiave, Double::plus come valore. “*” come chiave, Double::times come valore, ecc.

Scriviamo adesso il nostro tokenizer, che data la stringa di input, ci restituirà una collection (lista) di Token, dove i numeri saranno OPERAND, mentre gli operatori OPERATOR.

fun tokenizer(input: String): List<Token> {
    return input.split(" ").map {
            if (operatorMapper.contains(it)) {
                Token(Token.Type.OPERATOR, value = it)
            } else {
                Token(Token.Type.OPERAND, value = it)
            }
    }
}

fun calculator(lhs: Double, rhs: Double, operator: (Double, Double) -> Double): Double {
    return operator(lhs, rhs)
}

La funzione List::map è analoga a quella nella Java 8 API con gli stream. Il blocco di codice scritto dopo mapsenza parentesi, è zucchero sintattico per dire che

  1. il blocco di codice è una funzione lambda, e corrisponde al primo (e unico) parametro della funzione map.
  2. questa funzione lambda ha un parametro di entrata e uno di uscita. Il parametro di entrata è di tipo String, perché è String::split restituisce una List<String>, ed è identificato dal nome automatico it (sennò sarebbe bastato usare la notazione parametro-freccia per dargli un nome personalizzato).
    Il parametro di uscita è di tipo Token, perché questo mapping avviene da una List<String>, a una List<Token>, e quella istanziazione di Token assegnata apparentemente a nulla, corrisponde al ritorno nella closure (con return usciremmo fuori da tokenizer).

Quello che fa è dunque, per ogni elemento (stringa) della lista, se la stringa corrisponde a un simbolo degli operatori, corrispondi un token operatore nella lista di token, altrimenti un token operando.

La funzione calculator è stata ripescata dagli esempi di prima, ma adattata per i Double.

Scriviamo adesso la procedura main che si occuperà di eseguire la computazione di una stringa di input in risultato

fun main(args: Array<String>) {
    val input = "- × ÷ 15 - 7 + 1 1 3 + 2 + 1 1" // Stringa di esempio

    val operandStack = Stack<Double>() // 1

    tokenizer(input).asReversed().forEach { token -> // 2
        when (token.type) { // 3
            Token.Type.OPERATOR -> operandStack.push( // 4
                    calculator( // bonus
                            lhs = operandStack.pop(),
                            rhs = operandStack.pop(),
                            operator = operatorMapper[token.value]!!)) 
            Token.Type.OPERAND -> operandStack.push(token.value.toDouble()) // 5
        }
    }
    println("input: $input")
    println("result: ${operandStack.peek()}") // 6
}

Ecco cosa fa questo codice per eseguire la computazione:

  1. Dichiara uno stack vuoto di Double. Qui ci inseriremo gli operandi
  2. Idealmente, l’input non è una Lista ma anch’esso uno Stack (cioè gli elementi vanno presi dall’ultimo inserito al primo). Si potrebbe fare un for-each per partire dall’ultimo elemento, ma sicché la funzione List::forEach parte dal primo elemento, invertendo l’accesso alla collection con asReversed(), pescheremo gli elementi originali dall’ultimo al primo. Il funzionamento di forEach è ancora fondamentalmente basato sulle closure. Una funzione a cui è stata passata una espressione lambda, il cui primo parametro rappresenta un Token, perché stiamo iterando una List<Token>.
  3. Equivalente dello switch in Java e C-derivati. In base al tipo di token ci comporteremo in maniera differente
  4. Se l’elemento preso dall’input-stack è un operatore, prendi dallo stack degli operandi gli ultimi due elementi, e aggiungi allo stack degli operandi la valutazione (operatore operando1 operando2) equivalente a operatore(lhs, rhs).
  5. Se l’elemento preso dall’input-stack è un operando, aggiungilo allo stack degli operandi.
  6. Restituisci l’elemento in cima dallo stack degli operandi.

BONUS: Qualcuno potrebbe notare che sicché operatorMapper[chiave] restituisce una funzione, avremmo semplicemente potuto fare

operatorMapper[it.value]!!(operandStack.pop(), operandStack.pop())

Vero, però dedicarci una funzione apposita per questo, ci permette di gestire input potenzialmente scorretti, ad esempio potremmo accettare una funzione optional anziché mandare per parametro una funzione di cui garantiamo noi la null-safety, e sollevare un’eccezione adhoc che rappresenti un operatore inesistente.

Ecco il codice completo dell’esempio:

data class Token(val type: Type, val value: String) {
    enum class Type { OPERAND, OPERATOR }
}
val operatorMapper: HashMap<String, ((Double, Double) -> Double)> =
        hashMapOf(
                "+" to Double::plus, "+" to Double::plus,
                "*" to Double::times, "×" to Double::times,
                "-" to Double::minus, "−" to Double::minus,
                "/" to Double::div, "÷" to Double::div,
                "%" to Double::rem)

fun tokenizer(input: String): List<Token> {
    return input.split(" ").map {
            if (operatorMapper.contains(it)) {
                Token(Token.Type.OPERATOR, value = it)
            } else {
                Token(Token.Type.OPERAND, value = it)
            }
    }
}

fun calculator(lhs: Double, rhs: Double, operator: (Double, Double) -> Double): Double {
    return operator(lhs, rhs)
}

fun main(args: Array<String>) {
    val input = "- × ÷ 15 - 7 + 1 1 3 + 2 + 1 1"

    val operandStack = Stack<Double>()

    tokenizer(input).asReversed().forEach { token ->
        when (token.type) {
            Token.Type.OPERATOR -> operandStack.push(
                    calculator(
                            lhs = operandStack.pop(),
                            rhs = operandStack.pop(),
                            operator = operatorMapper[token.value]!!))
            Token.Type.OPERAND -> operandStack.push(token.value.toDouble())
        }
    }
    println("input: $input")
    println("result: ${operandStack.peek()}")
}

Nota per i programmatori Java

Anche Java 8 dal 2014 offre un paradigma funzionale, eppure, sintatticamente si fa ancora sentire come vecchio.

Infatti in Java da un punto di vista di linguaggio (non bytecode), non esiste ancora fondamentalmente il tipo funzione. Esistono le interfacce funzionali, cioè interfacce che obbligano l’implementante a implementare una sola funzione. Un’istanza di classe anonima (o concreta) che implementa solo quella funzione, può essere riscritta come closure.

Facciamo un esempio dall’API di Android

class View {
...
    public interface OnClickListener {
        void onClick(View v);
    }
...
}

Un’interfaccia funzionale per implementare un callback in risposta a un evento di click.

Diciamo che noi abbiamo un bottone, e vogliamo aggiungere un listener per eseguire un’azione (es. mandare un messaggio) quando il pulsante viene premuto

Button button = ... // Istanzia il bottone

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println("Pulsante cliccato");
    }
});

Da Java 8, è possibile semplificare questo codice come (in quanto si ha a che fare con un’interfaccia funzionale)

button.setOnClickListener(v -> {
    System.out.println("Pulsante cliccato");
});

Dove effettivamente, stiamo passando una funzione, con un parametro v di tipo View, e l’opportuno corpo di implementazione per gestire l’evento del click.

Il problema, è che mentre da un punto di vista di cliente dell’API, si sta utilizzando un paradigma prettamente funzionale, quando si scrivono higher-order-functions (funzioni che accettano per parametro altre funzioni o ne restituiscono, di quelle che abbiamo visto, la nostra calculator, map,  forEach, e adesso setOnClickListener), bisogna ancora utilizzare interfacce funzionali.

In Java 8, non si ha il tipo funzione di default, ma delle classi nella nuova API.

Ecco come dovremmo scrivere in stile funzionale il nostro metodo di prima in Java

// Definizione
interface DoubleBinaryOperation {
     double doOperation(double lhs, double rhs);
}

double calculator(double lhs, double rhs, DoubleBinaryOperation operation) {
     return operation.doOperation(lhs, rhs);
}

// Utilizzo - esempio
calculator(x, y, { x, y -> x * y });

Java 8 tuttavia, fornisce delle interfacce generiche già fatte per utilizzi del genere

double operation(double lhs, double rhs, BiFunction<Double, Double, Double> operation) {
    return operation.apply(lhs, rhs);
}

BiFunction è una funzione pensata per due parametri. Se ve ne serve uno solo, c’è Function. Se ne volete di più, dovete definire le vostre interfacce funzionali.

Nota per i programmatori Java Android

Se siete programmatori Android, probabilmente saprete della scomodità di Java 8 in Android. Non si può utilizzarlo realmente sotto Android 7 Nougat, però, è offerto un transpiling per la sintassi Java 8, purché si mantiene l’API di Java 7.

API di Java 7 o inferiore vuol dire, niente classi Functionniente supporto Stream (map, filter, ecc.) e qualsiasi ogni altra API funzionale di Java 8!

Includere Kotlin al vostro progetto tuttavia, vi fornisce anche le API di Kotlin utilizzabili in Java, che consistono anche – ma non esclusivamente – in interfacce Function con tanti differenti parametri anche in Java, e in Kotlin un supporto alla programmazione funzionale migliore di quello che Java 7 esteso di Android pre-Nougat vi offre.

Se vi piace la programmazione funzionale e allo stesso tempo avere un bacino d’utenza che superi il 26% di tutti gli utenti Android (16 gennaio 2018), usate Kotlin!

NOTE

Questa mini-serie non è volta a spiegare il Kotlin come linguaggio di programmazione per chi non ha mai programmato, ma per chi ha familiarità con i linguaggi ad oggetti e ancora meglio Java.

Nel prossimo articolo

Nel prossimo articolo si parlerà di altre caratteristiche del Kotlin che mancano in Java o non sono ben implementate.

Esse includono:

  • Operator Overloading: Desiderato da molti, odiato da tanti, sempre mancato in Java.
  • Inline functions (o funzioni inline): Eliminare nel bytecode l’istanza di una funzione e porre il suo corpo direttamente. Elimina l’overhead corrisposto con le higher-order-functions e le espressioni lambda.
  • Le Extensions (o estensioni): Aggiungere un metodo o una property a classi di cui non possiamo modificare il codice (o anche che possiamo modificare), come le classi delle librerie standard, o librerie di terze parti.
  • Le Coroutines: Un meccanismo asincrono a singolo thread per procedure che fanno un lavoro intenso oppure semplicemente richieste attraverso la rete, senza istanziare appositamente un thread a parte. Ciò include, l’async/await da ECMAScript, i channels e select da Go, e i generators/yield da C# e Python.

A proposito di me

Ruggiero Altini

Studente di Informatica, sviluppatore iOS e Android, e all'occorrenza backend e Desktop, ma appassionato di algoritmi. Ha usato e usa estensivamente C++, Java, Kotlin e Swift.

Di Ruggiero Altini

Gli articoli più letti

Articoli recenti

Commenti recenti