Erweiterte For-Schleife mit Index - III

Man könnte angesichts der beiden Vorgänger-Beiträge vielleicht denken, dass das das Beste wäre, was man aktuell mit Java hinbekommt. Aber es heißt ja…

Zum Blog-Eintrag

Bei der Verwendung geht es für dieses Beispiel noch ein wenig kürzer mit ‘name’ anstatt ‘names.get(i)’.

Danke, das war ein Copy-Paste-Fehler, hab’s korrigiert.

Besser:

public static <A> void indexed2(Iterable<? extends A> iterable, BiConsumer<? super A, Integer> consumer) 

sonst

        List<Number> numbers = Arrays.asList(1, 2, 3);
        List<? extends Number> extendedNumbers = Arrays.asList(1, 2, 3);
        
        BiConsumer<Number, Integer> consumerA = null;
        BiConsumer<Object, Integer> consumerB = null;
        
        For.indexed(numbers, consumerA);
        //For.indexed(numbers, consumerB); // Geht nicht

        //For.indexed(extendedNumbers, consumerA); // Geht nicht
        //For.indexed(extendedNumbers, consumerB); // Geht nicht
        
        // Geht alles:
        For.indexed2(numbers, consumerA);
        For.indexed2(numbers, consumerB);

        For.indexed2(extendedNumbers, consumerA);
        For.indexed2(extendedNumbers, consumerB);

(wird manchmal als “PECS” bezeichnet: Producer extends, Consumer super)

Stimmt.

OT: Ich finde es so doof, dass man in Java die Varianzen nicht wie bei Scala bei der Definition angeben kann, dann gäbe es solche Probleme bei der Benutzung erst gar nicht.

Für mich ist das zwar “Varianzen bei der Definition angeben”, aber … demnach meinst du offenbar was anderes…

EDIT @nillehammer Hähsowas, jetzt hab’ ich tatsächlich nachgeschaut: Mir wäre nicht bewußt gewesen, dass es ObjIntConsumer gibt…

[quote=Marco13]EDIT
@nillehammer
Hähsowas, jetzt hab’ ich tatsächlich nachgeschaut: Mir wäre nicht bewußt gewesen, dass es ObjIntConsumer gibt…[/quote]
Wenn’s um Primitives geht, muss ich auch jedes mal in java.util.function nachschauen, was dort so definiert ist. Wir Primitiven werden halt immer stiefmütterlich behandelt :stuck_out_tongue_winking_eye:

'EDIT @nillehammer ’ bezieht sich also auf einen Kommentar im Blog-Eintrag…,
meine grenzenlose Intelligenz hat es erfasst :wink: , allgemein reichlich unkenntlich hier,

Zitieren schadet dazu nicht, geht natürlich im Blog nicht, aber manuell:

nillehammer - Gestern 16:29

Anstatt des Biconsumer<A, Integer> nimm besser einen ObjIntConsumer<? super A>. Dann sparst Du Dir das Autoboxing.

das Blog-System mit diesen RSS Reader-Themen und dann Kommentaren hier ist reichlich buggy,
wahrscheinlich besser die Themen hier gleich zu sperren,
andererseits niemand zu verdenken lieber hier zu posten als in irgendwelchen komischen Blogs unbekannter Sichtbarkeit…

In Scala kannst du bei der Klassendefinition Foo[+A] bzw Foo[-A] schreiben. Damit signalisierst du, dass ein Foo** auch als ein Foo[A] verwendet werden kann, wenn B eine Sub- bzw. Superklasse von A ist. Siehe Variances - Scala Documentation

wie würde das hier helfen?

aktueller Stand ist
public static <A> void indexed(Iterable<? extends A> iterable, ObjIntConsumer<? super A> consumer) {

ist hier eigentlich das ? super nicht verzichtbar?
welcher Einsatzzweck ist damit abzudecken?

für alle vier Beispiele von Marco13 (bzw. die drei die vorher nicht gingen) reicht ja wohl
public static <A> void indexed(Iterable<? extends A> iterable, ObjIntConsumer<A> consumer) {
der ObjIntConsumer bzw. vormals BiConsumer ist fest, will Number oder eben Object sehen

interessant ist, ob man mit Object-ObjIntConsumer eine Number-Liste durchlaufen kann, oder mit beiden eine extends-Liste


wie würde nun Variances helfen? wäre das Interface Iterable zu ändern, damit alle Listen/ Collections in Java?
klingt ein bisschen heftig,
welche Auswirkungen hätte es?

        List<Object> o = names;
        List<? extends Object> o2 = names;

dass die o-Definition nicht geht ist ja ein Glück, o2 geht auch jetzt schon

lieber doch nur diese eine Methode so variabel halten?
wie ließe sich dann diese Methode mit etwas a la Variances anders schreiben?

[quote=SlaterB]ist hier eigentlich das ? super nicht verzichtbar?
welcher Einsatzzweck ist damit abzudecken?[/quote]
Meist ja, manchmal nein. Wenn Du bspw. eine List bearbeitest, müsste ohne “? super” ein ObjIntConsumer übergeben werden. Übergibst Du hier einen Lambda-Ausdruck, bspw.

(obj, index) -> System.out.println(index + " " + obj);

dann ist es kein Problem. Obwohl der Lamda-Ausruck eigentlich ein ObjIntConsumer ist, wird er einfach als ObjIntConsumer genommen, weil’s passt. Das klappt sogar mit Methodenreferenzen (void accept(Object, int) ). Wenn Du allerdings den Lambda-Ausdruck (oder Methodenreferenz) einer Variablen mit explizit deklariertem Typ ObjectIntConsumer zuweist und dann versuchst, die Variable zu übergeben, dann braucht es “? super”.

das sind ja nun in etwa die Beispiele von Marco13, also schon behandelt,
wie gesagt geht das doch alles allein mit ? extends?

interessanterweise allein mit dem ? super auch:

    public static void main(String[] args)   {
        List<Number> numbers = Arrays.asList(1, 2, 3);
        List<? extends Number> extendedNumbers = Arrays.asList(1, 2, 3);
        ObjIntConsumer<Number> consumerA = null;
        ObjIntConsumer<Object> consumerB = null;
       
        For.indexedExtends(numbers, consumerA); // alle 4 gehen
        For.indexedExtends(numbers, consumerB); 
        For.indexedExtends(extendedNumbers, consumerA);
        For.indexedExtends(extendedNumbers, consumerB); 
        
        For.indexedSuper(numbers, consumerA);  // alle 4 gehen
        For.indexedSuper(numbers, consumerB); 
        For.indexedSuper(extendedNumbers, consumerA); 
        For.indexedSuper(extendedNumbers, consumerB);   
    }
}

class For {
    public static <A>void indexedExtends(Iterable<? extends A> iterable, ObjIntConsumer<A> consumer)  {
        int index = 0;
        for (A a : iterable)   {
            consumer.accept(a, index++);
        }
    }
    
    public static <A>void indexedSuper(Iterable<A> iterable, ObjIntConsumer<? super A> consumer)  {
        int index = 0;
        for (A a : iterable)    {
            consumer.accept(a, index++);
        }
    }
}

aber die Tooltips zu den gefundenen Typen für die indexedSuper-Variante mit teils ? super ? extends Number sind ja reichlich abenteuerlich, siehe Screenshot

für die indexedExtends-Variante ist es nüchtern ehrlich korrekt: entweder wird zu Number oder Object aufgelöst, je nachdem wie es passt,
der ObjIntConsumer steht fest und gibt den Typ vor, Iterable passt sich mit ? extends an, reicht?

Daran hatte ich mich noch dunkel erinnert. Aber ob diese Varianzen nun auf Klassen- oder Methodenebene festgetacktert werden erscheint mir erstmal nur wie eine Frage nach der Granularität - konzeptuell („Typtheoretisch“) sehe ich keinen so großen Unterschied.

Es ist verzichtbar. Auch bei dem „Student“-Beispiel würde ohne das ? super der Typ A auf „Student“ festgelegt, und die potentielle Freiheit (auch wenn es sie im konkreten Fall nicht gibt) über das extends A abgehandelt. Man hat, etwas verallgemeinert, die Wahl zwischen

static void <A> process(Producer<A> p, Consumer<A> c)
static void <A> process(Producer<A> p, Consumer<? super A> c)
static void <A> process(Producer<? extends A> p, Consumer<A> c)
static void <A> process(Producer<? extends A> p, Consumer<? super A> c)

Und wenn man den Typ „A“ nicht händisch festlegt (mit sowas wie Methods.<SomeType>process(p,c);), dann sind die letzten drei gleichwertig*: In allen Fällen gibt man eine Untere und eine Obere Grenze vor, zwischen der der Compiler den Typ „A“ einordnen darf. Der Unterschied ist lediglich, ob diese Grenzen „Inklusiv“ oder „Exklusiv“ sind.

*:
Echt gleichwertig?

Es gibt zumindest keinen Fall, wo nur EINER der letzten drei hier in Eclipse rot aufleuchtet:

package bytewelt;


class Producer<T> 
{
    T produce()
    {
        return null;
    }
}
class Consumer<T> 
{
    void consumer(T t)
    {
        
    }
}

public class ProducerConsumerVariances
{
    Producer<Object> po = null;
    Producer<Number> pn = null;
    Producer<Integer> pi = null;
    
    Producer<? extends Object> peo = null;
    Producer<? extends Number> pen = null;
    Producer<? extends Integer> pei = null;

    Consumer<Object> co = null;
    Consumer<Number> cn = null;
    Consumer<Integer> ci = null;
    
    Consumer<? super Object> cso = null;
    Consumer<? super Number> csn = null;
    Consumer<? super Integer> csi = null;
    
    void objectProducerObjectConsumer()
    {
        processA(po, co);
        processB(po, co);
        processC(po, co);
        processD(po, co);
    }
    void numberProducerObjectConsumer()
    {
        processA(pn, co);
        processB(pn, co);
        processC(pn, co);
        processD(pn, co);
    }
    void integerProducerObjectConsumer()
    {
        processA(pi, co);
        processB(pi, co);
        processC(pi, co);
        processD(pi, co);
    }
    
    void objectProducerNumberConsumer()
    {
        processA(po, cn);
        processB(po, cn);
        processC(po, cn);
        processD(po, cn);
    }
    void numberProducerNumberConsumer()
    {
        processA(pn, cn);
        processB(pn, cn);
        processC(pn, cn);
        processD(pn, cn);
    }
    void integerProducerNumberConsumer()
    {
        processA(pi, cn);
        processB(pi, cn);
        processC(pi, cn);
        processD(pi, cn);
    }
        
    void objectProducerIntegerConsumer()
    {
        processA(po, ci);
        processB(po, ci);
        processC(po, ci);
        processD(po, ci);
    }
    void numberProducerIntegerConsumer()
    {
        processA(pn, ci);
        processB(pn, ci);
        processC(pn, ci);
        processD(pn, ci);
    }
    void integerProducerIntegerConsumer()
    {
        processA(pi, ci);
        processB(pi, ci);
        processC(pi, ci);
        processD(pi, ci);
    }
        
    void extendsObjectProducerObjectConsumer()
    {
        processA(peo, co);
        processB(peo, co);
        processC(peo, co);
        processD(peo, co);
    }
    void extendsNumberProducerObjectConsumer()
    {
        processA(pen, co);
        processB(pen, co);
        processC(pen, co);
        processD(pen, co);
    }
    void extendsIntegerProducerObjectConsumer()
    {
        processA(pei, co);
        processB(pei, co);
        processC(pei, co);
        processD(pei, co);
    }

    void extendsObjectProducerNumberConsumer()
    {
        processA(peo, cn);
        processB(peo, cn);
        processC(peo, cn);
        processD(peo, cn);
    }
    void extendsNumberProducerNumberConsumer()
    {
        processA(pen, cn);
        processB(pen, cn);
        processC(pen, cn);
        processD(pen, cn);
    }
    void extendsIntegerProducerNumberConsumer()
    {
        processA(pei, cn);
        processB(pei, cn);
        processC(pei, cn);
        processD(pei, cn);
    }

    void extendsObjectProducerIntegerConsumer()
    {
        processA(peo, ci);
        processB(peo, ci);
        processC(peo, ci);
        processD(peo, ci);
    }
    void extendsNumberProducerIntegerConsumer()
    {
        processA(pen, ci);
        processB(pen, ci);
        processC(pen, ci);
        processD(pen, ci);
    }
    void extendsIntegerProducerIntegerConsumer()
    {
        processA(pei, ci);
        processB(pei, ci);
        processC(pei, ci);
        processD(pei, ci);
    }
    
    void objectProducerSuperObjectConsumer()
    {
        processA(po, cso);
        processB(po, cso);
        processC(po, cso);
        processD(po, cso);
    }
    void numberProducerSuperObjectConsumer()
    {
        processA(pn, cso);
        processB(pn, cso);
        processC(pn, cso);
        processD(pn, cso);
    }
    void integerProducerSuperObjectConsumer()
    {
        processA(pi, cso);
        processB(pi, cso);
        processC(pi, cso);
        processD(pi, cso);
    }

    void objectProducerSuperNumberConsumer()
    {
        processA(po, csn);
        processB(po, csn);
        processC(po, csn);
        processD(po, csn);
    }
    void numberProducerSuperNumberConsumer()
    {
        processA(pn, csn);
        processB(pn, csn);
        processC(pn, csn);
        processD(pn, csn);
    }
    void integerProducerSuperNumberConsumer()
    {
        processA(pi, csn);
        processB(pi, csn);
        processC(pi, csn);
        processD(pi, csn);
    }

    void objectProducerSuperIntegerConsumer()
    {
        processA(po, csi);
        processB(po, csi);
        processC(po, csi);
        processD(po, csi);
    }
    void numberProducerSuperIntegerConsumer()
    {
        processA(pn, csi);
        processB(pn, csi);
        processC(pn, csi);
        processD(pn, csi);
    }
    void integerProducerSuperIntegerConsumer()
    {
        processA(pi, csi);
        processB(pi, csi);
        processC(pi, csi);
        processD(pi, csi);
    }
    


    void extendsObjectProducerSuperObjectConsumer()
    {
        processA(peo, cso);
        processB(peo, cso);
        processC(peo, cso);
        processD(peo, cso);
    }
    void extendsNumberProducerSuperObjectConsumer()
    {
        processA(pen, cso);
        processB(pen, cso);
        processC(pen, cso);
        processD(pen, cso);
    }
    void extendsIntegerProducerSuperObjectConsumer()
    {
        processA(pei, cso);
        processB(pei, cso);
        processC(pei, cso);
        processD(pei, cso);
    }

    void extendsObjectProducerSuperNumberConsumer()
    {
        processA(peo, csn);
        processB(peo, csn);
        processC(peo, csn);
        processD(peo, csn);
    }
    void extendsNumberProducerSuperNumberConsumer()
    {
        processA(pen, csn);
        processB(pen, csn);
        processC(pen, csn);
        processD(pen, csn);
    }
    void extendsIntegerProducerSuperNumberConsumer()
    {
        processA(pei, csn);
        processB(pei, csn);
        processC(pei, csn);
        processD(pei, csn);
    }

    void extendsObjectProducerSuperIntegerConsumer()
    {
        processA(peo, csi);
        processB(peo, csi);
        processC(peo, csi);
        processD(peo, csi);
    }
    void extendsNumberProducerSuperIntegerConsumer()
    {
        processA(pen, csi);
        processB(pen, csi);
        processC(pen, csi);
        processD(pen, csi);
    }
    void extendsIntegerProducerSuperIntegerConsumer()
    {
        processA(pei, csi);
        processB(pei, csi);
        processC(pei, csi);
        processD(pei, csi);
    }


    public static <T> void processA(Producer<T> p, Consumer<T> c)
    {
        c.consumer(p.produce());
    }
    public static <T> void processB(Producer<? extends T> p, Consumer<T> c)
    {
        c.consumer(p.produce());
    }
    public static <T> void processC(Producer<T> p, Consumer<? super T> c)
    {
        c.consumer(p.produce());
    }
    public static <T> void processD(Producer<? extends T> p, Consumer<? super T> c)
    {
        c.consumer(p.produce());
    }

}

Der Grund, warum ich das ? super trotzdem hinschreiben würde, ist „Konsistenz“, „Konsequenz“, „Klarheit“. Man könnte es formulieren als „Weil das laut PECS** eben so sein sollte“, aber ich denke, dass es wirklich hilft, die Intention (bzw. das nicht-Vorhandensein einer speziellen Typanforderung) auszudrücken.

** Da der „Producer“ in Java eigentlich „Supplier“ heißt, müßte das eigentlich „SECS“ heißen. Hihihi.

Der Unterschied ist, ob ich **einmal ** (wie in Scala) bei der Definition sage: „Hier kann X oder eine Unterklasse von X kommen“, oder jedesmal (über Wildcards), wenn ich die Klasse benutze.

Zumal typtheoretisch die Art der Nutzung fast immer feststeht. Wenn z.B. irgendwo einmal Bla extends Comparator<? extends Bla> steht, ist das schlichtweg falsch, es gibt dafür keinen sinnvollen Anwendungsfall. Deshalb ist es sinnvoll, z.B. Comparator ein für allemal als contravariant zu definieren.

nochmal gefragt, wo könnte man das hier anwenden, wie würde es weiterhelfen?

bei der Methodendefinition
public static <+A> void indexed(Iterable<A> iterable, ObjIntConsumer<A> consumer) {
? ist das in Scala möglich oder auch für dort als Wunsch formuliert?


der Link Variances - Scala Documentation
spart ja auch nicht gerade mit Komplexität

Please note that this is an advanced example which combines the use of polymorphic methods, lower type bounds, and covariant type parameter annotations in a non-trivial fashion. Furthermore we make use of inner classes to chain the stack elements without explicit links.

für normalsterbliche Leser eher nicht gedacht, oder? :wink:

ganz am Ende wird es erst konkret:

e.g. it’s possible to push a string on an integer stack. The result will be a stack of type Stack[Any]; so only if the result is used in a context where we expect an integer stack, we actually detect the error. Otherwise we just get a stack with a more general element type.

so ist das also,
für Welten mit veränderlichen Objekten ohne neue Variablenzuweisung nach jeder Aktion aber eher nicht gedacht:

Stack[Integer] s = ..
s.push("string");
// alle Integer aus s auslesen -> boom

aber das gibt es ja eben nicht, insofern dort durchaus ok, auch für class List in scala eingesetzt

dann wäre ja wirklich Iterable<+A> der Gedanke hier für die indexed()-Methode bzw. Iterable-Definition und in indexed() nichts anzugeben?,
in Java mit zustandsbehafteten Objekten aber natürlich undenkbar


OT:
Covariance and Contravariance in Scala | Atlassian Blogs
fand ich gerade noch bei der Suche, sieht zunächst bekömmlicher aus,
aber dann Abschnittsüberschriften ‚A little bit of category theory‘, ‚Function Functors‘, ‚Back to Earth‘…

passend ein Kommentar darunter

I started reading but it became too confusing after a while… I even tried re-reading parts but got no luck.
I think it could be convenient to use examples to explain definitions.
Thanks anyway!

warum nur muss alles unsagbar kompliziert gemacht werden, so hat die Sprache ja nie eine Chance…

[QUOTE=SlaterB]nochmal gefragt, wo könnte man das hier anwenden, wie würde es weiterhelfen?

bei der Methodendefinition
public static <+A> void indexed(Iterable<A> iterable, ObjIntConsumer<A> consumer) {
? ist das in Scala möglich oder auch für dort als Wunsch formuliert?
[/quote]

Scala kann das nur auf Klassenebene soweit ich weiß, bei einzelnen Methoden scheint es auch nicht sonderlich nützlich zu sein.

für Welten mit veränderlichen Objekten ohne neue Variablenzuweisung nach jeder Aktion aber eher nicht gedacht:

Stack[Integer] s = ..
s.push("string");
// alle Integer aus s auslesen -> boom

aber das gibt es ja eben nicht, insofern dort durchaus ok, auch für class List in scala eingesetzt

Das wäre bei einem unveränderlichen, contravarianten Stack kein Problem. Kleinste gemeinsame Oberklasse von Integer und String wäre Object, dementsprechend wäre das Resultat nach dem push ein Stack[Object] - vollkommen typsicher (wenn auch nicht sehr nützlich in diesem Beispiel). Veränderliche Datenstrukturen sind dagegen meist invariant. Scala würde sich auch querstellen, wenn du versuchen würdest, eine selbige co- oder contravariant zu definieren.