Closed danieldietrich closed 8 years ago
PECS does not help here :(
Only an Object argument helps in contains
and indexOf
.
Moving back contains from Value to Traversable...
Maybe this could affect even more methods, e.g. let's take a look at java.util.Map
:
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);
...
@ruslansennov @netzwerg @RobWin I think this goes in the wrong direction. We loose type safety, which is no good idea. In Rahels example we just need to cast the List<Double>
to a List<Number>
and all is fine. But Java says we can only get a List<? extends Number>
, which is useless. Even if contains
is fixed, we still have no working prepend
etc.
I've sketched a type safe solution, which introduces a type-safe cast:
import javaslang.collection.List;
import java.math.BigDecimal;
public class Test {
public static void main(String[] args) {
//
// Initial situation:
// We have a type 'Double' which is in between other types
// within a specific type hierarchy.
//
List<Double> doubles = List.of(1d);
//
// Our intuition says that a List<Double> is a List<Number>
//
// error: "Incompatible types"
List<Number> numbers1 = doubles;
// error: "Inconvertible types"
List<Number> numbers2 = (List<Number>) doubles;
// Works, but captured generics are useless
// as we will see in the following examples
List<? extends Number> numbers3 = doubles;
// prepend (capture<? extends Number>) in List cannot be applied to (BigDecimal)
numbers3.prepend(new BigDecimal("0"));
// prepend (capture<? extends Number>) in List cannot be applied to (Number)
numbers3.prepend((Number) new BigDecimal("0"));
//
// In a mutable world it would be possible to
//
// List<Double> doubles = List.of(1d);
// List<Number> numbers = cast(doubles);
// numbers.add(new BigDecimal("0"));
// // now doubles contains a BigDecimal, what is wrong
//
// In an immutable world it is pretty eligible to assume
// that a List<Double> is a List<Number>. Looking at the
// example above, we now have:
//
// List<Double> doubles = List.of(1d);
// List<Number> numbers = cast(doubles);
// List<Number> newNumbers = numbers.add(new BigDecimal("0"));
// // doubles remains the same
//
List<Number> numbers4 = (List<Number>) (Object) doubles;
numbers4.contains(42d);
numbers4.contains(new BigDecimal("1"));
numbers4.prepend(42d);
final List<Number> newNumbers4 = numbers4.prepend(new BigDecimal("1"));
//
// We abstract over (List<Number>) (Object) doubles and provide
// a _safe_ API to perform correct casts:
//
List<Number> test = List.empty();
// does not compile: "inference variable U has incompatible bounds"
List<Double> wrong = cast(test);
// does compile!
List<Number> right = cast(doubles);
}
@SuppressWarnings("unchecked")
static <T extends U, U> List<U> cast(List<T> list) {
return (List<U>) list;
}
}
What do you think?
It will be part of each collection type. E.g.
interface List<T> {
@SuppressWarnings("unchecked")
static <T extends U, T> List<U> cast(List<T> list) {
return (List<U>) list;
}
}
Note: it would be nice to have an instance method but we can't express a lower-bound generic like this:
interface List<T> {
// <U super T> does not compile
@SuppressWarnings("unchecked")
default <U super T> List<U> cast() {
return (List<U>) this;
}
}
Is there a better name for cast
?
Maybe we already have that method, it is ofAll
and we just need to fix the generics, e.g.
interface List<T> {
@SuppressWarnings("unchecked")
static <T extends U, T> List<U> ofAll(Iterable<? extends T> elements) {
Objects.requireNonNull(elements, "elements is null");
if (elements instanceof List) {
return (List<U>) elements;
} else if (elements instanceof java.util.List) {
List<U> result = Nil.instance();
final java.util.List<T> list = (java.util.List<U>) elements;
final ListIterator<T> iterator = list.listIterator(list.size());
while (iterator.hasPrevious()) {
result = result.prepend(iterator.previous());
}
return result;
} else if (elements instanceof NavigableSet) {
List<U> result = Nil.instance();
final java.util.Iterator<T> iterator = ((NavigableSet<T>) elements).descendingIterator();
while (iterator.hasNext()) {
result = result.prepend(iterator.next());
}
return result;
} else {
List<U> result = Nil.instance();
for (T element : elements) {
result = result.prepend(element);
}
return result.reverse();
}
}
}
A few months ago I started a discussion about this topic on twitter: https://twitter.com/danieldietrich/status/669534660561629185
The replies are interesting, in particular those of Peter Lawrey (this), Jean-Baptiste Giraudeau (this) and Brian Goetz (this).
Interestingly the binarySearch was also mentioned there :-)
No need to fix anything, RC3 works fine?
List<Double> slangDoubles = List.of(42d, 99d);
List<Number> slangNumbers = List.ofAll(slangDoubles);
slangNumbers.contains(42d);
Oohh, you're right - awesome :smile: I love this library - everything just works™
I am not completely convinced yet, so just to be sure:
You're suggesting to call List.ofAll
in order to get rid of the upper bound for slangNumbers
, right? I am aware that this is safe & fast, but the readability suffers if you're doing this in real-world code.
Looking at it from a different angle: I am in the process of convincing my dev team that Javaslang is awesome. One of many arguments is that we no longer need defensive copying upon construction, and here I come suggesting this "fix":
class SimpleComposition {
private final List<Number> numbers;
private SimpleComposition(List<? extends Number> numbers) {
this.numbers = List.ofAll(numbers); // Smell
}
boolean contains(Number number) {
return numbers.contains(number);
}
}
This doesn't feel 100% right...
It certainly is a good workaround until we have a proper contains
though:
boolean contains(Number number) {
return List.ofAll(this.numbers).contains(number);
}
In think the idea is not to copy the list, but cast. See ofAll
if (elements instanceof List) {
return (List<U>) elements;
Or am I wrong?
@netzwerg @RobWin I see, List.ofAll(...) is not a good name for our use-case.
What we really need in order to use the type-safe variants contains(T)
, prepend(T)
, ... is a unsound type system like that of Dart: Why Dart Types Are Optional and Unsound (scroll down to Why are generics covariant?).
The essence of that document is: a ReadOnlyReference
Java does not have a unsound type system. The best we can do is: ReadOnlyReference
So we have to choices:
ofAll(...)
seems to be confusing. Is cast(...)
or safeCast(...)
better? The Method needs to be static because the generic bound is super
, which cannot be expressed in an instance method. And because we do not have higher-kinded types/type constructors, we need to implement that methods for each type.Note: It is not sufficient to use contains(Object)
and indexOf(Object)
. There are plenty of more methods which need to take Object instead of T because a (capture of ? extends T)
does not allow us to insert subtypes of T. Example: java.util.List.add(1.0d)
is not possible on a List<? extends Number>
Tx for your excellent explanation, @danieldietrich!
IMHO, cast(...)
sounds scary while safeCast(...)
sounds suspicious :-) But in our case, the cast is not risky at all, so we don't want the method to sound scary.
What about something along the lines of narrow(...)
?
We have List<Double> doubles = ...
.
We want a List<Number> numbers = doubles
.
But the best Java offers us is List<? extends Number> numbers = doubles
.
About the types:
List<Number> numbers = doubles;
does not compileList<Number> numbers = (List<Number>) doubles;
does not compileList<Number> numbers = (List<Number>) (Object) doubles;
does compile but is unsafeIt is not possbile to
cast
with List<Number> numbers = doubles.cast();
because we are not allowed to express lower bound generics:interface List<T> {
// not possible
<U super T> List<U> cast() {
return (List<U>) this;
}
// also not possible
<U> List<U super T> cast() {
return (List<U>) this;
}
}
cast
methods because of the lack of higher-kinded types/type constructors:interface Predef {
// not possible:
static <M<T>> M<T> cast(M<? extends T> type) { ... }
// workaround, but return type not helpful here
static <M extends Kind<M, ?>, T> Kind<M, T> cast(Kind<M, ? extends T> type) { ... }
}
This leads to a static cast
method per type.
Example: List
interface List<T> {
static <T> List<T> cast(List<? extends T> list) { ... }
}
Note: List.ofAll(...)
does exactly what cast
does (+ additional things with other inputs). But ofAll
is not convenient/comprehensible. The API should exactly describe what happens.
@netzwerg please read my previous comment, it clarifies the higher-kinded stuff etc.
Yes, cast
is a negative pattern / anti-idiom.
We use narrow
in the context of Monads/higher-kinded types. A second use case of the same keyword would be confusing.
Are there other names? Maybe
cast
, alternatives: https://www.powerthesaurus.org/castnarrow
, alternatives: https://www.powerthesaurus.org/narrowunbound
, alternatives: https://www.powerthesaurus.org/unboundunbounded
, alternatives: https://www.powerthesaurus.org/unboundedunsound
, alternatives: https://www.powerthesaurus.org/unsoundIt is all about types, why not
typed
, alternatives: https://www.powerthesaurus.org/typedRe-opening this issue to find a better name...
We use narrow in the context of Monads/higher-kinded types. A second use case of the same keyword would be confusing.
Good point...
Inspired by your links, confine
or restrict
could work?
Anything with bound
can be confusing because it blurs the intention i.e. it is no longer clear that we're going from ? extends T
(wide) to T
(narrow).
I like restrict
:+1:
I also like infer
. restrict
sounds like loosing something. But we gain type-safe collection operations that are otherwise not possible in Java. Java can't infer the right type, but we know better because List is immutable.
List<Number> numbers = List.infer(doubles);
Note: We also see sorted
instead of sort
or curried
instead of curry
. Maybe inferred
is better than infer
? In the sense of give me an inferred version instead of infer that list. On the other hand there is sum
and zip
instead of summed
and zipped
. Never understood the difference. I think most of the ...ed methods take no arguments. The static infer
/inferred
method does also take no arguments other than the instance itself, i.e. it is an extension method. That would indicate inferred
.
Oh my gosh - naming things is so hard... :)
I like infer
if it's on the RHS of an assignment, like in your example:
List<Number> numbers = List.infer(doubles);
I prefer restrict
in a method chain:
return List.restrict(doubles).contains(...);
But probably I am focusing too much on my use case...
What about List.typeSafe(...)
? Has a positive connotation and works for both.
What about List.typeSafeUpCast() ?
What did you say?
List.whatAboutTypeSafeUpCast() ? ;)
I like typeSafe, too. Maybe it has to be List.typeSafe™() :-)
List.surprise or Traumschiff.surprise()
Just upCast?
upCast is like cast, which is a smell because it is unsafe in general.
Mmhh, narrow
fits very good. I think it does not collide with Monad.narrow(). javaslang has no dependency to javaslang-algebra.
Check?
Collections got narrow(). Do other types also need narrow()? E.g. Option, Try, Either, Future, Validation, Lazy, ... ???
Need to add tests for these:
Done with #1077 and #1081. Moved pending tests to #477
We need to change the
contains(T)
and*indexOf(T)
methods tocontains(Object)
and*indexOf(Object)
.Here are some example of code that currently does not work:
However,
javaNumbers.add(42d)
does also not work. But it is quite eligible and also intuitive to sayAnother example:
Using
<U extends T> boolean contains(U that)
does also not work.As @RobWin says
Producer extends and Consumer super "PECS" is from the collection's point of view. If you are only pulling items from a generic collection, it is a producer and you should use extends; if you are only stuffing items in, it is a consumer and you should use super. (from stackoverflow)
@netzwerg great finding!!