Se Scala gira su JVM, come può fare Scala cose che a quanto pare Java non può fare? [duplicare]

27

Ho appena saputo di Scala ieri, e mi piacerebbe saperne di più. Una cosa che mi è venuta in mente, tuttavia, leggendo il sito web di Scala è che se Scala viene eseguito sulla JVM, allora come è possibile che il bytecode compilato dal sorgente di Scala sia in grado di ottenere cose che Java non è in grado di fare facilmente, come (ma non limitato a) generici reificati?

Capisco che il compilatore è ciò che genera il bytecode; finché il compilatore può sottoporre a massaggio il codice sorgente in un bytecode valido supportato dalla JVM, entrambi dovrebbero essere equivalenti. Ma avevo l'impressione che Java non potesse nemmeno reificare i propri generici, quindi come potrebbe un altro compilatore essere in grado di estrometterlo?

    
posta agent154 25.04.2015 - 20:50
fonte

5 risposte

29

Tutti i linguaggi di programmazione funzionano su x86, quindi come possono essere molto diversi l'uno dall'altro?

Brainfuck e Haskell sono entrambi Turing completi , in modo che entrambi possano eseguire esattamente le stesse attività.

C'è un po 'di spazio per le modifiche di sintassi, lo zucchero della sintassi e la magia del compilatore in mezzo. Puoi fare parecchio, ma c'è sempre un limite. Nel tuo caso, è il codice byte JVM.

Se Java può produrre lo stesso codice byte di Scala, sono equivalenti. Potrebbe, tuttavia, accadere che una nuova funzione in JVM venga implementata solo in Scala. Molto improbabile, ma possibile.

    
risposta data 25.04.2015 - 21:30
fonte
21

Per affrontare il problema specifico che sollevate, di generici reificati. . .

In molti contesti, i parametri di tipo sono effettivamente salvati nei file di classe e sfruttabili tramite riflessione, anche a dispetto della cancellazione. Ad esempio, il seguente programma stampa class java.lang.String :

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;

public class ErasureWhatErasure {
    private final ArrayList<String> foo = null;

    public static void main(final String... args) throws Exception {
        final Field fooField = ErasureWhatErasure.class.getDeclaredField("foo");
        final ParameterizedType fooFieldType =
            (ParameterizedType) fooField.getGenericType();
        System.out.println(fooFieldType.getActualTypeArguments()[0]);
    }
}

Tutti "cancellazione" significa che quando si crea un'istanza di un tipo parametrizzato, il parametro tipo non viene registrato come parte dell'istanza; new ArrayList<String>() e new ArrayList<Integer>() creano istanze identiche. Ma anche in Java esistono alcune soluzioni alternative ben note, come ad esempio:

  1. Qualcosa come new ArrayList<String>() { } crea effettivamente una nuova istanza di una sottoclasse anonima di ArrayList<String> , e la relazione di sottoclasse fa registra il parametro tipo. Quindi puoi recuperare il String in modo riflessivo. (In particolare: ((ParameterizedType) new ArrayList<String>() { }.getClass().getGenericSuperclass()).getActualTypeArguments()[0] è String.class .) Questo è sfruttato da vari framework, come Guice e Gson.
  2. Se stai creando la tua classe, puoi richiedere che il parametro type venga passato tramite il costruttore e archiviarlo in un campo. (Il compilatore può aiutarti a far sì che l'argomento del costruttore corrisponda al parametro type. E se l'argomento type è esso stesso generico, puoi combinarlo con il metodo n. 1 per assicurarti di avere l'argomento intero tipo.)

Un altro linguaggio che risiede sulla JVM potrebbe reificare i generici facendo sì che # 2 avvenga implicitamente; ogni volta che hai dichiarato una classe generica, i campi del parametro tipo ei parametri del costruttore sarebbero stati aggiunti implicitamente e ogni volta che lo istanziati o lo estendi, l'argomento tipo sarebbe stato copiato implicitamente in argomento costruttore ( s) per popolarli. Java fa già la stessa cosa - campi impliciti e parametri-costruttore / argomenti - in un contesto diverso, vale a dire, per le classi locali che fanno riferimento a final variabili locali nei loro metodi di contenimento.

La limitazione principale è che le classi generiche nel JDK - il framework delle collezioni, java.util.Class , ecc. - non hanno già questa configurazione e le lingue alternative in esecuzione nella JVM non possono aggiungerle. Quindi tali linguaggi dovrebbero fornire i loro equivalenti delle classi JDK. Ma possono ancora usare la JVM stessa.

    
risposta data 26.04.2015 - 01:54
fonte
7

Il bytecode JVM finge di essere una sorta di codice macchina generico, ed è proprio così, quindi ... cosa ti fa pensare che non possa supportare nessun'altra lingua? Il bytecode JVM è un linguaggio completo di Turing e, quindi, ogni programma, indipendentemente da quale lingua sia stata scritta, può essere compilato / tradotto in bytecode.

Ci sono molti linguaggi che hanno già un compilatore di codice (ad esempio Jython per Python e JRuby per Ruby) e ci sono anche diversi da Java.

Si noti che tecnicamente, ogni linguaggio di programmazione completo di Turing può essere compilato in un altro. Ad esempio, potrebbe essere possibile compilare da JS a C o da Ruby a Python.

    
risposta data 25.04.2015 - 21:49
fonte
3

In poche parole, Scala può farlo perché il compilatore Scala è un maestro nella trasformazione / generazione del codice. Il che significa che Java potrebbe farlo anche tu (forse lo è già). Il trucco non viene eseguito a livello di byte, ma a livello di sorgente.

Potresti indovinare l'output di questo codice:

def test[T](f : => Any) : T = {
  try { val x = f.asInstanceOf[T]
    println("f.asInstanceOf did not raise an error")
      x
  }
  catch { case e : Throwable =>
    println("f.asInstanceOf did raise an error")
    throw e
  }
}

val x = test[Int]("x")

(Non così) Sorprendentemente emette

f.asInstanceOf did not raise an error
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:105)

... 33 elided

A causa della cancellazione dei tipi, f.asInstanceOf[T] non può fallire ma obviouly una String non è un Int, quindi deve fallire da qualche parte. Fortylely Scala offre soluzioni a questo problema:

import shapeless.Typeable
import shapeless.syntax.typeable._

def test[T : Typeable](f : => Any) : T = {

    f.cast[T] match {
        case Some(x) => {
            println("f.cast[T] succeed")
            x
        }
        case None    => {
            println("f.cast[T] failed")
            throw new RuntimeException("cast failed!")
        }
    }
}

val x = test[Int]("x")

La differenza principale qui è che dichiariamo T come Typeable . Scala ha diversi modi per fornire rappresentazioni runtime dei tipi.

    
risposta data 26.04.2015 - 20:46
fonte
2

I generici reificati non richiedono il supporto JVM. Sì, sarebbero più facili e più performanti con il supporto JVM, ma il supporto JVM non è necessario. Ad esempio, un compilatore Scala potrebbe, per ogni classe che presenta una variabile di tipo, aggiungere un campo che memorizza l'oggetto corrispondente:

class List<T> {

}

void test() {
    List<?> list = new List<String>();
    List<Integer> intList = (List<Integer>) list;
}

sarebbe compilato per

class List<T> {
    final Class<T> tClass;
    public List(Class<T> tClass) {
        this.tClass = tClass;
    }

    public <O> List<O> castTo(Class<O> oClass) {
        if (tClass == oClass) {
            return (List<O>) this;
        } else {
            throw new ClassCastException("Incompatible type parameter: " + tClass);
        }
    }
}

void test() {
    List<?> list = new List<String>(String.class);
    List<Integer> intList = list.castTo(Integer.class);
}

Esistono ovviamente strategie di traduzione più elaborate che si integrano più facilmente con il sistema di tipi di host, ad esempio avendo effettivamente classi separate per argomenti di tipo diverso con lo stesso tipo generico:

class List<T> {

}

class List#Integer extends List<Integer> { }

class List#String extends List<String> { }

void test() {
    List<?> list = new List#String();
    List<Integer> intList = (List#Integer) list;
}

(Ci sarebbero naturalmente alcune sfide non apparenti in questo banale esempio, ad esempio argomenti di tipo ricorsivo, corretto isolamento di caricatori di classi differenti, ...)

La ragione per cui Java non ha generici reificati non è che la reificazione sarebbe impossibile sulla JVM, ma che i generici reificati avrebbero rotto la compatibilità all'indietro (in particolare la compatibilità binaria) con i programmi Java esistenti - qualcosa di cui Scala non doveva preoccuparsi .

    
risposta data 26.04.2015 - 19:53
fonte

Leggi altre domande sui tag