Home » Java » Guava why toStringFunction not a generic function?

Guava why toStringFunction not a generic function?

Posted by: admin December 28, 2021 Leave a comment

Questions:

the Guava toStringFunction() has the following declaration:

public static Function<Object, String> toStringFunction() { ... } 

All non-primitive roots at Object, so the function works well.

but when I try to compose it with another function, such as:

Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                 Functions.toStringFunction());

where someMap variable is a map< String, Double>, So i expect toStringFunction converts Integer to String and then forMap converts String to Double.
But I get a compiler error:

    Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                    ^
  required: Function<Integer,Double>
  found:    Function<Object,Double>
2 errors

My two questions are:

1.how to specifically tell the compiler that toStringFunction should be Function< Integer, String>? A simple cast will not work, and I am looking for a real composition of the two functions.

Function< Integer, String> g1 = (Function< Integer, String>)Functions.toStringFunction(); 

// cause error 

2.Had the toStringFunction written as the following:

 @SuppressWarnings("unchecked") 
  public static <E> Function<E, String> toStringFunction(){
    return (Function<E, String>) ToStringFunction.INSTANCE;
  }

  // enum singleton pattern
  private enum ToStringFunction implements Function<Object, String>{
    INSTANCE;


    // @Override public String toString(){
    //   return "Functions.toStringFunction()";
    // }

    @Override public String apply(Object o){
      return o.toString();
    }
  }

I can specify the type:

Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                 Functions.<Integer>toStringFunction());

and this works fine. But what is the motivation for the version Guava team have right now?

Answers:

The reason toStringFunction() is a Function<Object, String> and not a Function<T, String> is, quite simply, because you can call toString() on any Object. It is literally a function that takes an object and returns a string. Guava avoids introducing type parameters (and wildcard generics) to methods where they serve no purpose.

The issue in your example is that you are trying to type your f1 function as Function<Integer, Double> when it is in fact a Function<Object, Double>: it can take any object, call toString() on it, pass that string to your forMap function and get a result.

Furthermore, assuming proper use of generics, there shouldn’t be any actual necessity for your function’s type to be Function<Integer, Double> rather than Function<Object, Double>, since most code that accepts a function and uses it to do something should accept something like Function<? super F, ? extends T>. So the real question here is “why do you want to refer to a Function<Object, Double> as a Function<Integer, Double>“?

###

After discussion in comments:

To fix the compilation error:

Function<Object, Double> f1 = 
    Functions.compose(Functions.forMap(someMap), Functions.toStringFunction());

Your question implies you want a Function<Integer, Double> instead of Function<Object, Double>. Based on the discussion in the comments, and trying to think of other scenarios, the only reason you will want this is for validation. The code in the toStringFunction is out of your control, so validation in that function is out of question. As far as validation in the forMap function, that code is also out of your control. If you want to protect against IllegalArgumentException, you will have to perform this validation whether the input you provide is an Object or an Integer. So that validation too is out of scope of the forMap, toStringFunction, and compose, because none of these functions provide that kind of validation and you have to write your own code to do this (either catch the exception and handle it or pre-validate in some way before calling this composed Function).

For the specific compose, you gain nothing by changing the input parameter of the Function from Object to Integer, because the Function only needs the methods present in Object, so it is a good decision as it simplifies things in this case and makes it more widely usable. Say you were able to enforce the parameter type as Integer, you still have gained absolutely no benefit whatsoever, whether it is validation for IllegalArgumentException or something else you are attempting.

When designing an API, you want it to be usable by as many consumers as possible, without having to go out of your way, i.e. you should use the highest level class that satisfies your requirements. By using Object in toStringFunction, the Guava function is satisfying this principle. This makes the function more generic and more widely usable. That is the reason for choosing Object instead of parameterizing this Function.

If the Function took a parameter, it would do nothing differently as it is doing right now, so why use a parameter when it is not required. This approach simplifies the design greatly instead of adding something that is not required.

Original answer:

Every class is a child of Object, and the Function is only calling toString on the input which is present in the Object class. So in this case, declaring a Function is equivalent to declaring Function. You will gain nothing by changing the input parameter of the Function from Object to Integer, because the Function only needs the methods present in Object, so it is a good decision as it simplifies things in this case.

As far as possible, you should use the highest level class that satisfies your requirements. This makes the function more generic and more widely usable. That is the reason for choosing Object instead of parameterizing this Function.

To fix the compilation error: Function f1 = Functions.compose(Functions.forMap(someMap), Functions.toStringFunction());

###

The reason why Guava does not declare it as Function<T, String> (although it could be done) is explained by one of the Guava team members in this comment to a similar question in their bug tracker:

We decided that the purpose of type parameters and wildcards in a library API is to ensure that the method can be called with any parameter types that make logical sense. Once that condition is met, we stop, instead of continuing to also add type params/wildcards that serve only to let you “massage” the specific result type you want to get.

So basically they decided that it is not worth it. Instead of changing methods like toStringFunction to return a Function<T, String>, consumers of generic types should instead to be changed such that they allow to be given a Function<? super F, ? extends T>.

###

Regarding Guava team’s motivation for this behavior, I really don’t know. Maybe one of them should respond that.

Regarding the compilation error, you need a helper method in order to perform the cast:

public class Sample {

    @SuppressWarnings("unchecked")
    private static <T> Function<T, String> checkedToStringFunction() {
        return (Function<T, String>) Functions.toStringFunction();
    }

    public static void main(String[] args) {
        Map<String, Double> someMap = new HashMap<>();

        Function<Integer, Double> f1 = Functions.compose(
            Functions.forMap(someMap), 
            checkedToStringFunction()); // compiles fine
    }
}

This way, the compiler can safely infer the parameter types.

EDIT:

I was told that this technique might lead to heap pollution. While this might be true from a theoretical point of view, in practice, I fail to see how this might lead to a ClassCastException, or to any other runtime error, since the helper method is private and only used to infer the Function type parameters.

If I forced the generic type, i.e:

Function<Integer, Double> f1 = Functions.compose(
    Functions.forMap(someMap), 
    Sample.<BigInteger> checkedToStringFunction()); // compilation error

I’d get a compilation error. To fix it, I’d need to change the first type parameter of the f1 Function to:

Function<BigInteger, Double> f1 = Functions.compose(
    Functions.forMap(someMap), 
    Sample.<BigInteger> checkedToStringFunction()); // compiles fine

Note:

I know what’s heap pollution:

private static <S, T> S convert(T arg) { 
    return (S) arg;  // unchecked warning 
} 

Number n = convert(new Long(5L)); // fine 
String s = convert(new Long(5L)); // ClassCastException 

In this example I’m casting to the given type variable, while in the solution I’m suggesting, I’m casting to a parameterized type.

Specifficaly, I’m casting from Function<Object, Double> to Function<Integer, Double>. I see it as a narrowing type conversion, so I believe it’s legal and safe. Please let me know if I’m OK with this and if it’s worth taking the risk.