Perché non entrambi?
In un interprete per un linguaggio digitato in modo dinamico, avrai bisogno di una rappresentazione comune per tutti i valori. Ad esempio, potremmo avere una funzione che può restituire un numero o un dizionario. In JavaScript:
function foo(want_dict) {
if (want_dict) return { value: 42 };
else return 42;
}
var result = foo(someVariable);
Quindi, come potrebbe un linguaggio dinamico rappresentare il contenuto della variabile result
? Poiché la variabile può essere un numero o un dizionario, la variabile non ha un tipo specifico. Invece, le informazioni sul tipo vivono nel valore stesso, spesso sotto forma di un ID di tipo. In C, un valore potrebbe essere rappresentato come
typedef struct {
unsigned type_id;
void *value;
} Value;
In Java, potremmo usare una classe simile:
class Value {
private TypeId type;
private Object value;
public Value(TypeId type, Object value) {
this.type = type;
this.value = value;
}
public TypeId type() { return type; }
public Object value() { return value; }
}
Potremmo quindi avere vari valori come
return new Value(TypeId.Int, new Integer(42));
...
HashMap<String, Value> rawValue = new HashMap<>();
rawValue.put("value", new Value(TypeId.Int, new Integer(42)));
return new Value(TypeId.Dict, rawValue);
Quando proviamo a utilizzare tale valore, dobbiamo prima verificare dinamicamente che tutti i valori abbiano tipi accettabili. Per esempio. l'implementazione dell'aggiunta da parte dell'interprete potrebbe essere simile a questa:
Value doAddition(Value a, Value b) {
if (a.type() != TypeId.Int)
throw ...;
if (b.type() != TypeId.Int)
throw ...;
return new Value(TypeId.Int, (Integer) a.value() + (Integer) b.value());
}
In alternativa, potremmo sfruttare le funzionalità di digitazione dinamica di Java stesso, ovvero le informazioni sul tipo sono codificate in una gerarchia di ereditarietà.
interface Value<T> {
public T value();
}
class MyInt implements Value<Integer> {
private int value;
public MyInt(int value) { this.value = value; }
@Override public Integer value() { return value; }
}
class MyDict implements Value<Map<String, Value>> {
private Map<String, Value> value;
public MyInt(Map<String, Value> value) { this.value = value; }
@Override public Map<String, Value> value() { return value; }
}
Ora possiamo creare vari valori come
return new MyInt(42);
...
HashMap<String, Value> rawValue = new HashMap<>();
rawValue.put("value", new MyInt(42));
return new MyDict(rawValue);
Con questa codifica, un'implementazione di un operatore addizione sarebbe simile a questa:
Value doAddition(Value a, Value b) {
if (!(a instanceof MyInt))
throw ...;
if (!(b instanceof MyInt))
throw ...;
return new MyInt(a.value() + b.value());
}
Quindi, entrambe le codifiche sembrano molto simili, ma preferirei un tag esplicito poiché è un po 'più flessibile e non è limitato dalla cancellazione del tipo di Java (al ribasso, tutto è un Object
e deve essere espressi).
Con la tipizzazione dinamica abbiamo necessariamente a disposizione un tipo di supporto per il tipo di runtime, quindi possiamo supportare banalmente più tipi numerici. In una codifica, dobbiamo solo aggiungere un nuovo ID tipo, nell'altra un'altra sottoclasse:
class MyRat implements Value<Double> {
private double value;
public MyRat(double value) { this.value = value; }
@Override public Double value() { return value; }
}
Possiamo anche rendere la nostra implementazione di addizione polimorfica, in modo che lo stesso operatore possa gestire sia gli interi che i doppi:
Value doAddition(Value a, Value b) {
if ((a instanceof MyInt) && (b instanceof MyInt))
return new MyInt(a.value() + b.value());
if ((a instanceof MyRat) && (b instanceof MyRat))
return new MyRat(a.value() + b.value());
throw ...;
}
Non è stato così difficile!
La parte difficile sta decidendo:
- Quali tipi numerici vogliamo supportare nella nostra lingua? Solo numeri in virgola mobile? (Javascript) Solo tipi interi? (B) Entrambi? (la maggior parte delle lingue) Entrambe, ma come un singolo tipo e passiamo a raddoppiare se il numero dovesse altrimenti traboccare? (Perl)
- Quali dimensioni vogliamo supportare? Dimensioni native per l'efficienza? Numeri arbitrari di precisione? Una collezione di diversi tipi di larghezza? Vogliamo supportare i tipi non firmati?
La maggior parte di questi problemi può essere risolta quando si pensa a quali numeri verranno utilizzati nella lingua. Per esempio. i doppi sono inutili quando si indicizza un array, poiché non tutti i numeri naturali hanno una rappresentazione doppia. Gli integer sono inutili per domini non discreti come le misurazioni fisiche. I doppi sono inutili per i calcoli valutari (che sono discreti). L'overflow dei numeri interi è inutile, ma efficiente. E così via. Come sottolinea l'altra risposta, questa è una questione di progettazione del linguaggio, ma non è troppo difficile implementare qualsiasi scelta a cui arrivi.