Repository: Unleash/unleash-client-java (java)

Issue:
Why use a `BiFunction` when the desired interface was a `BiPredicate`?
### Describe the feature request

In the `Unleash` interface, there are some methods with a `BiFunction<String, UnleashContext, Boolean>` argument. But the availables implementations for `Unleash` (`DefaultUnleash`, `FakeUnleash`) both have no safe measures against this function returning a `null`. Bot will yield a `NullPointerException`:

[`DefaultUnleash.java#L123-L127`](https://github.com/Unleash/unleash-client-java/blob/8525a8f56e2198bebcd893e169ae92b556cc8b07/src/main/java/io/getunleash/DefaultUnleash.java#L123-L127)

```java
        boolean enabled;
        UnleashContext enhancedContext = context.applyStaticFields(config);

        if (featureToggle == null) {
            enabled = fallbackAction.apply(toggleName, enhancedContext);
```

[`FakeUnleash.java#L43-L46`](https://github.com/Unleash/unleash-client-java/blob/8525a8f56e2198bebcd893e169ae92b556cc8b07/src/main/java/io/getunleash/FakeUnleash.java#L43-L46):

```java
    public boolean isEnabled(
            String toggleName, BiFunction<String, UnleashContext, Boolean> fallbackAction) {
        if (!features.containsKey(toggleName)) {
            return fallbackAction.apply(toggleName, UnleashContext.builder().build());
```

Also, it is more semantic to use `Predicate<T>` or similar instead of `Function<T, Boolean>` or similar.

### Background

Using `Predicate<T>` instead of `Function<T, Boolean>` is a lot more semantic, and also avoids (some) random null pointer exceptions that the java type system allows.

If the null pointer exception happens for some reason, it will be a violation of contract of the API consumer, not of the API provider, as it is now.

### Solution suggestions

Change the API to accept a `BiPredicate` instead of a `BiFunction`.

It is wildly expected that it shall be ok with the use of lambdas. It will break compatibility if someone passes directly an instance of `BiFunction<String, UnleashContext, Boolean>`, as the tests within here have broken.

Trying to accept both `BiPredicate<String, UnleashContext>` and `BiFunction<String, UnleashContext, Boolean>` will cause the compiler to get confused, as both will satisfy the lambda signature. For example, in `DefaultUnleash` will yield a compiler error:

```java
    @Override
    public boolean isEnabled(
            final String toggleName, final UnleashContext context, final boolean defaultSetting) {
        return isEnabled(
                toggleName,
                context,
                (n, c) -> defaultSetting);
    }
```

![ambiguous method call, both functional interfaces match](https://user-images.githubusercontent.com/6178379/196252564-4c1261f9-a6b7-4961-9b6f-9524b3953442.png)

This will require a manual action of the maintainer to specify that it is a `BiPredicate` that it is wanted:

```java
    @Override
    public boolean isEnabled(
            final String toggleName, final UnleashContext context, final boolean defaultSetting) {
        return isEnabled(
                toggleName,
                context,
                (BiPredicate<String, UnleashContext>) (n, c) -> defaultSetting);
    }
```

Pull request:
chorus: using more semantic java interface
using more semantic java functional interface `BiPredicate` instead of `BiFunction`.

<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ -->

## About the changes
<!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. -->

Has potential to break compatibility with previous versions (see [Discussion points](#Discussion-points)), but makes the code more semantic.

<!-- Does it close an issue? Multiple? -->

Closes #176.

<!-- (For internal contributors): Does it relate to an issue on public roadmap (https://github.com/orgs/Unleash/projects/5)? -->

<!-- Relates to roadmap item:  -->

### Important files

- src/main/java/io/getunleash/Unleash.java
- src/test/java/io/getunleash/UnleashTest.java

## Discussion points

This might break some API consumers if they are using directly the interface `BiFunction` instead of a lambda.

Patch:
diff --git a/src/main/java/io/getunleash/DefaultUnleash.java b/src/main/java/io/getunleash/DefaultUnleash.java
index bfb5a5aa..15b6f9d8 100644
--- a/src/main/java/io/getunleash/DefaultUnleash.java
+++ b/src/main/java/io/getunleash/DefaultUnleash.java
@@ -15,7 +15,7 @@ import io.getunleash.util.ConstraintMerger;
 import io.getunleash.util.UnleashConfig;
 import io.getunleash.variant.VariantUtil;
 import java.util.*;
-import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.stream.Collectors;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -99,7 +99,7 @@ public class DefaultUnleash implements Unleash {
     @Override
     public boolean isEnabled(
             final String toggleName,
-            final BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            final BiPredicate<String, UnleashContext> fallbackAction) {
         return isEnabled(toggleName, contextProvider.getContext(), fallbackAction);
     }
 
@@ -107,7 +107,7 @@ public class DefaultUnleash implements Unleash {
     public boolean isEnabled(
             String toggleName,
             UnleashContext context,
-            BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            BiPredicate<String, UnleashContext> fallbackAction) {
         boolean enabled = checkEnabled(toggleName, context, fallbackAction);
         count(toggleName, enabled);
         eventDispatcher.dispatch(new ToggleEvaluated(toggleName, enabled));
@@ -117,14 +117,14 @@ public class DefaultUnleash implements Unleash {
     private boolean checkEnabled(
             String toggleName,
             UnleashContext context,
-            BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            BiPredicate<String, UnleashContext> fallbackAction) {
         checkIfToggleMatchesNamePrefix(toggleName);
         FeatureToggle featureToggle = featureRepository.getToggle(toggleName);
         boolean enabled;
         UnleashContext enhancedContext = context.applyStaticFields(config);
 
         if (featureToggle == null) {
-            enabled = fallbackAction.apply(toggleName, enhancedContext);
+            enabled = fallbackAction.test(toggleName, enhancedContext);
         } else if (!featureToggle.isEnabled()) {
             enabled = false;
         } else if (featureToggle.getStrategies().size() == 0) {
diff --git a/src/main/java/io/getunleash/FakeUnleash.java b/src/main/java/io/getunleash/FakeUnleash.java
index 23245fac..96af451a 100644
--- a/src/main/java/io/getunleash/FakeUnleash.java
+++ b/src/main/java/io/getunleash/FakeUnleash.java
@@ -4,7 +4,7 @@ import static java.util.Collections.emptyList;
 
 import io.getunleash.lang.Nullable;
 import java.util.*;
-import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import java.util.stream.Collectors;
 
 public class FakeUnleash implements Unleash {
@@ -35,15 +35,15 @@ public class FakeUnleash implements Unleash {
     public boolean isEnabled(
             String toggleName,
             UnleashContext context,
-            BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            BiPredicate<String, UnleashContext> fallbackAction) {
         return isEnabled(toggleName, fallbackAction);
     }
 
     @Override
     public boolean isEnabled(
-            String toggleName, BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            String toggleName, BiPredicate<String, UnleashContext> fallbackAction) {
         if (!features.containsKey(toggleName)) {
-            return fallbackAction.apply(toggleName, UnleashContext.builder().build());
+            return fallbackAction.test(toggleName, UnleashContext.builder().build());
         }
         return isEnabled(toggleName);
     }
diff --git a/src/main/java/io/getunleash/Unleash.java b/src/main/java/io/getunleash/Unleash.java
index 2c663bc9..efe633e0 100644
--- a/src/main/java/io/getunleash/Unleash.java
+++ b/src/main/java/io/getunleash/Unleash.java
@@ -1,7 +1,7 @@
 package io.getunleash;
 
 import java.util.List;
-import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 
 public interface Unleash {
     boolean isEnabled(String toggleName);
@@ -17,14 +17,14 @@ public interface Unleash {
     }
 
     default boolean isEnabled(
-            String toggleName, BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            String toggleName, BiPredicate<String, UnleashContext> fallbackAction) {
         return isEnabled(toggleName, false);
     }
 
     default boolean isEnabled(
             String toggleName,
             UnleashContext context,
-            BiFunction<String, UnleashContext, Boolean> fallbackAction) {
+            BiPredicate<String, UnleashContext> fallbackAction) {
         return isEnabled(toggleName, context, false);
     }
 


Test patch:
diff --git a/src/test/java/io/getunleash/UnleashTest.java b/src/test/java/io/getunleash/UnleashTest.java
index 6ec4fb47..a0cfd28f 100644
--- a/src/test/java/io/getunleash/UnleashTest.java
+++ b/src/test/java/io/getunleash/UnleashTest.java
@@ -13,7 +13,7 @@ import io.getunleash.util.UnleashScheduledExecutor;
 import io.getunleash.variant.Payload;
 import io.getunleash.variant.VariantDefinition;
 import java.util.*;
-import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -79,33 +79,33 @@ public class UnleashTest {
     @Test
     public void fallback_function_should_be_invoked_and_return_true() {
         when(toggleRepository.getToggle("test")).thenReturn(null);
-        BiFunction<String, UnleashContext, Boolean> fallbackAction = mock(BiFunction.class);
-        when(fallbackAction.apply(eq("test"), any(UnleashContext.class))).thenReturn(true);
+        BiPredicate<String, UnleashContext> fallbackAction = mock(BiPredicate.class);
+        when(fallbackAction.test(eq("test"), any(UnleashContext.class))).thenReturn(true);
 
         assertThat(unleash.isEnabled("test", fallbackAction)).isTrue();
-        verify(fallbackAction, times(1)).apply(anyString(), any(UnleashContext.class));
+        verify(fallbackAction, times(1)).test(anyString(), any(UnleashContext.class));
     }
 
     @Test
     public void fallback_function_should_be_invoked_also_with_context() {
         when(toggleRepository.getToggle("test")).thenReturn(null);
-        BiFunction<String, UnleashContext, Boolean> fallbackAction = mock(BiFunction.class);
-        when(fallbackAction.apply(eq("test"), any(UnleashContext.class))).thenReturn(true);
+        BiPredicate<String, UnleashContext> fallbackAction = mock(BiPredicate.class);
+        when(fallbackAction.test(eq("test"), any(UnleashContext.class))).thenReturn(true);
 
         UnleashContext context = UnleashContext.builder().userId("123").build();
 
         assertThat(unleash.isEnabled("test", context, fallbackAction)).isTrue();
-        verify(fallbackAction, times(1)).apply(anyString(), any(UnleashContext.class));
+        verify(fallbackAction, times(1)).test(anyString(), any(UnleashContext.class));
     }
 
     @Test
     void fallback_function_should_be_invoked_and_return_false() {
         when(toggleRepository.getToggle("test")).thenReturn(null);
-        BiFunction<String, UnleashContext, Boolean> fallbackAction = mock(BiFunction.class);
-        when(fallbackAction.apply(eq("test"), any(UnleashContext.class))).thenReturn(false);
+        BiPredicate<String, UnleashContext> fallbackAction = mock(BiPredicate.class);
+        when(fallbackAction.test(eq("test"), any(UnleashContext.class))).thenReturn(false);
 
         assertThat(unleash.isEnabled("test", fallbackAction)).isFalse();
-        verify(fallbackAction, times(1)).apply(anyString(), any(UnleashContext.class));
+        verify(fallbackAction, times(1)).test(anyString(), any(UnleashContext.class));
     }
 
     @Test
@@ -115,11 +115,11 @@ public class UnleashTest {
                         new FeatureToggle(
                                 "test", true, asList(new ActivationStrategy("default", null))));
 
-        BiFunction<String, UnleashContext, Boolean> fallbackAction = mock(BiFunction.class);
-        when(fallbackAction.apply(eq("test"), any(UnleashContext.class))).thenReturn(false);
+        BiPredicate<String, UnleashContext> fallbackAction = mock(BiPredicate.class);
+        when(fallbackAction.test(eq("test"), any(UnleashContext.class))).thenReturn(false);
 
         assertThat(unleash.isEnabled("test", fallbackAction)).isTrue();
-        verify(fallbackAction, never()).apply(anyString(), any(UnleashContext.class));
+        verify(fallbackAction, never()).test(anyString(), any(UnleashContext.class));
     }
 
     @Test