Perché i tipi in Java sono considerati meno "forti" di haskell?

4

Ho chiesto questo domanda qualche tempo fa - le risposte erano davvero utili, e mentre le leggevo e le domande che erano collegate - ho anche visto this , e la prima risposta che penso si rivolge veramente a ciò che pensavo fosse l'essenza del più potente sistema di tipi.

Stavo cercando di capire veramente la pseudo-implementazione del functor che l'autore dà nel suo esempio - e mi chiedevo se qualcuno potesse darmi una spiegazione leggermente più semplice di questa parte della sua risposta.

Direttamente citato dalla risposta: sono questi bit che non riesco a ottenere.

per questo blocco di codice.

interface Functor<A> {
    Functor<B> map(Function<A, B> f);
}
  1. The type system doesn't allow us to express the invariant that the map method always returns the same Functor subclass as the receiver.
  2. Therefore, there's no statically type-safe manner to invoke a non-Functor method on the result of map.

È un modo semplice per vedere che in un'implementazione concreta di Functor , puoi dichiarare ciò che vuoi usare come A , ma non quello che vuoi usare come B, o anche il B è uguale su entrambi i lati della funzione mappa?

Più altro, la mia interpretazione un po 'semplicistica di questo è che i tipi single-type significano veramente che limita quanto lontano è possibile "raggiungere" o "specificare" il contratto che si sta tentando di specificare con i propri tipi. Con tipi di tipi più elevati, ottieni molta più flessibilità su come specifichi i tuoi tipi, cioè puoi vincolare e legare le tue funzioni in modo più specifico che puoi con semplici generici di java.

... o davvero potrei essere che non capisco cosa sia un Type Constructor o perché sia utile!

    
posta phatmanace 28.04.2015 - 23:34
fonte

2 risposte

4

Riguardo al punto 1., discutiamolo con un esempio:

class Maybe<A> implements Functor<A> {
  public <B> Functor<B> map(Function<A, B> f) {
    // implementation....
  }
}

class List<A> implements Functor<A> {
  public <B> Functor<B> map(Function<A, B> f) {
    return new Maybe<B>(); // error, but I can do this
  }
} 

Vedi il problema? L'implementazione map di List restituisce Maybe solo perché Maybe implementa Functor. Si tratta della potenza del sistema di tipi: voglio dire che map non restituisce solo un Functor ma restituisce un Functor molto specifico.

Riguardo al punto 2., digita safety, penso che il problema descritto dal post sia il seguente: immaginiamo che tu voglia chiamare il metodo get(0) sulla lista e vuoi chiamarlo dopo aver chiamato map

list.map(...).get(0) // error because Functor doesn't have add

Devi in realtà downcast il ritorno della mappa a List .

((List<B>)list.map(...)).get(0)

In una lingua come Haskell, la funzione fmap prende un Functor F e restituisce un'altra istanza di quel Functor F, quindi si può fare:

 head (fmap (...) list)
    
risposta data 29.04.2015 - 00:03
fonte
4

È utile dare un'occhiata al codice Haskell dalla risposta originale:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

Qui f indica lo stesso tipo in f a (argomento) e f b (risultato).

Nella versione Java, Functor<B> nel risultato non garantisce che sia lo stesso Functor di Functor<A> . Iniziamo con l'esempio corretto:

// This functor is totally OK: map returns the same type
class ContainerFunctor<A> implements Functor<A> {
    private final A element;

    public ContainerFunctor(A element) {
        this.element = element;
    }

    public ContainerFunctor<B> map(Function<A, B> f) {
        return new ContainerFunctor<B>(f.apply(element)); 
    }
}

// Usage:

ContainerFunctor<Integer> intF = new ContainerFunctor<Integer>(123);
ContainerFunctor<String> strF = intF.map(new Function<Integer, String> {
    public String apply(Integer i) {
        return i.toString();
    }
});

Qui intF e strF sono dello stesso tipo di funtori, ContainerFunctor , che è il comportamento previsto di un funtore.

Ora vediamo come può essere violato:

class BrokenFunctor implements Functor<A> {
    private final A element;

    public BrokenFunctor(A element) {
        this.element = element;
    }

    // it SHOULD return BrokenFunctor, but type system can't force it
    Functor<B> map(Function<A, B> f) {
        return new ContainerFunctor<B>(f.apply(element));
    }
}

// Usage:

BrokenFunctor<Integer> intF = new BrokenFunctor<Integer>(123);

// Broken!
BrokenFunctor<String> strF = (BrokenFunctor<String>) intF.map(new Function<Integer, String> {
    public String apply(Integer i) {
        return i.toString();
    }
});

Il sistema di tipi di Java è più debole in questo caso: non c'è modo di forzare BrokenFunctor.map a restituire un altro BrokenFunctor come risultato. Questo non va bene perché non possiamo stabilire con quale tipo di functor ci ritroveremo dopo aver chiamato map . Possiamo ancora fare in modo che per disciplina (che dica a tutti del contratto implicito aggiuntivo di un'interfaccia), ma non c'è modo di forzarlo. Pertanto, se crei un'interfaccia come Functor , non puoi forzare tutte le possibili implementazioni a essere corrette.

The type system doesn't allow us to express the invariant that the map method always returns the same Functor subclass as the receiver

Vero: nell'esempio sopra, BrokenFunctor.map non restituisce BrokenFunctor .

Therefore, there's no statically type-safe manner to invoke a non-Functor method on the result of map.

Anche vero: dobbiamo convertire il risultato di BrokenFunctor.map in ContainerFunctor per farlo funzionare:

// not type-safe!
ContainerFunctor<String> strF = (ContainerFunctor<String>) intF.map(new Function<Integer, String> {
    public String apply(Integer i) {
        return i.toString();
    }
});

E ancora, in caso di ContainerFunctor il cast non è necessario, ma non c'è modo di forzare tutte le implementazioni a comportarsi in questo modo.

    
risposta data 29.04.2015 - 00:34
fonte

Leggi altre domande sui tag