Repository: PrefectHQ/prefect

Issue:
Task library: add Filter
When working with mapped tasks, it's common to have one or more children tasks skip or fail (for any reason). Anticipating these non-success states can complicate users who want to reduce over the result of a map, because they need to set custom trigger settings on the reducing task as well as handle the non-success results (`NoResult` and `Exceptions`).

A "Filter" task (open to any other name) would essentially be the following:

```python
class Filter(Task):
    def __init__(self, **kwargs):
        super().__init__(trigger=all_finished, skip_on_upstream_skip=False, **kwargs)

    def run(self, mapped_result):
        return [r for r in mapped_result if not isinstance(r, (NoResult, Exception))]
```

And used like this:

```python
numbers = [1, '2', 3]
numbers_plus_one = plus_one.map(numbers)
good_results = Filter()(numbers_plus_one)
```

good_results would return `[1, 3]` -- having filtered the map for only the useful tasks.


Discussion:
Additionally, we could allow for an optional `filter_func` kwarg, which defaults to `lambda r: not isinstance(r, (NoResult, Exception))`

Resolving patch:
diff --git a/src/prefect/tasks/control_flow/__init__.py b/src/prefect/tasks/control_flow/__init__.py
--- a/src/prefect/tasks/control_flow/__init__.py
+++ b/src/prefect/tasks/control_flow/__init__.py
@@ -1,3 +1,4 @@
 # Licensed under LICENSE.md; also available at https://www.prefect.io/licenses/alpha-eula
 
 from prefect.tasks.control_flow.conditional import ifelse, switch, merge
+from prefect.tasks.control_flow.filter import FilterTask
diff --git a/src/prefect/tasks/control_flow/filter.py b/src/prefect/tasks/control_flow/filter.py
new file mode 100644
--- /dev/null
+++ b/src/prefect/tasks/control_flow/filter.py
@@ -0,0 +1,46 @@
+# Licensed under LICENSE.md; also available at https://www.prefect.io/licenses/alpha-eula
+
+import warnings
+from typing import Any, Callable, List
+
+import prefect
+from prefect import Task
+from prefect.triggers import all_finished
+from prefect.engine.result import NoResultType
+
+
+class FilterTask(Task):
+    """
+    Task for filtering lists of results.  The default filter is to filter out `NoResult`s and
+    Exceptions for filtering out mapped results.  Note that this task has a default trigger of
+    `all_finished` and `skip_on_upstream_skip=False`.
+
+    Args:
+        - filter_func (Callable, optional): a function to use for filtering
+            results; this function should accept a single positional argument and return a boolean
+            indicating whether this result should be _kept_ or not.  The default is
+            to filter out `NoResult`s and Exceptions
+        - **kwargs (optional): additional keyword arguments to pass to the Task
+            constructor
+    """
+
+    def __init__(self, filter_func: Callable = None, **kwargs) -> None:
+        kwargs.setdefault("skip_on_upstream_skip", False)
+        kwargs.setdefault("trigger", all_finished)
+        self.filter_func = filter_func or (
+            lambda r: not isinstance(r, (NoResultType, Exception))
+        )
+        super().__init__(**kwargs)
+
+    def run(self, task_results: List[Any]) -> List[Any]:
+        """
+        Task run method.
+
+        Args:
+            - task_results (List[Any]): a list of results from upstream tasks,
+                which will be filtered using `self.filter_func`
+
+        Returns:
+            - List[Any]: a filtered list of results
+        """
+        return [r for r in task_results if self.filter_func(r)]


Test patch:
diff --git a/tests/tasks/test_control_flow.py b/tests/tasks/test_control_flow.py
--- a/tests/tasks/test_control_flow.py
+++ b/tests/tasks/test_control_flow.py
@@ -3,7 +3,8 @@
 import prefect
 from prefect import Flow, Task, task
 from prefect.engine.state import Skipped, Success
-from prefect.tasks.control_flow import ifelse, merge, switch
+from prefect.engine.result import NoResult
+from prefect.tasks.control_flow import ifelse, merge, switch, FilterTask
 from prefect.tasks.core.constants import Constant
 
 
@@ -184,3 +185,32 @@ def test_merge_with_list():
     with prefect.context(CONDITION=True):
         state = flow.run(return_tasks=flow.tasks)
         assert state.result[merge_task].result == [1, 2]
+
+
+class TestFilterTask:
+    def test_empty_initialization(self):
+        task = FilterTask()
+        assert task.name == "FilterTask"
+        assert task.skip_on_upstream_skip is False
+        assert task.trigger == prefect.triggers.all_finished
+
+    def test_skip_on_upstream_skip_can_be_overwritten(self):
+        task = FilterTask(skip_on_upstream_skip=True)
+        assert task.skip_on_upstream_skip is True
+
+    def test_trigger_can_be_overwritten(self):
+        task = FilterTask(trigger=prefect.triggers.manual_only)
+        assert task.trigger == prefect.triggers.manual_only
+
+    def test_default_filter_func_filters_noresults_and_exceptions(self):
+        task = FilterTask()
+        res = task.run([NoResult, NoResult, 0, 1, 5, "", ValueError()])
+        assert len(res) == 4
+        assert res == [0, 1, 5, ""]
+
+    def test_filter_func_can_be_changed(self):
+        task = FilterTask(filter_func=lambda r: r != 5)
+        exc = ValueError()
+        res = task.run([NoResult, NoResult, 0, 1, 5, "", exc])
+        assert len(res) == 6
+        assert res == [NoResult, NoResult, 0, 1, "", exc]