Repository: PrefectHQ/prefect

Issue:
De-dupe constant tasks
Probably not a high priority (and maybe not feasible for some reason I haven't thought of) but I find this behavior a bit irritating:
```
from prefect import task, Flow
from prefect.engine.executors import DaskExecutor

@task
def add(x, y):
    return x + y

with Flow('flow') as flow:
    for i in range(10):
        add(i, 0)

flow.run(executor=DaskExecutor('localhost:8786'))
```
Result: the constant `0` task is repeated every time, as is the corresponding log:
<img width="952" alt="Screen Shot 2019-09-17 at 9 38 29 PM" src="https://user-images.githubusercontent.com/903655/65112244-d9435800-d993-11e9-9ee1-58d19a0c9dd3.png">
```
[2019-09-18 04:37:19,486] INFO - prefect.TaskRunner | Task '0': Starting task run...
[2019-09-18 04:37:19,486] INFO - prefect.TaskRunner | Task '0': Starting task run...
[2019-09-18 04:37:19,489] INFO - prefect.TaskRunner | Task '0': Starting task run...
[2019-09-18 04:37:19,492] INFO - prefect.TaskRunner | Task '0': finished task run for task with final state: 'Success'
[2019-09-18 04:37:19,493] INFO - prefect.TaskRunner | Task '0': finished task run for task with final state: 'Success'
[2019-09-18 04:37:19,497] INFO - prefect.TaskRunner | Task '0': finished task run for task with final state: 'Success'
...
```
Is there any kind of simple de-duping scheme that might work within a flow, maybe just for Python literals...?

Edit: possibly kind of a dupe of #891 which was already declined...? 🤷‍♂ 


Discussion:
This is a really good call out; the original reason for this behavior is that we needed a place to track the values that are being used as Task inputs, and simply reusing Prefect's Task machinery was the most natural way to do that.  In addition, if we see a non-Prefect Task as an input and simply stored its value without further introspection, we might lose dependency relationships such as:

```python
with Flow("example") as flow:
    my_task(inputs=[task_a, task_b])
```

However, I fully agree that this is not ideal and something we should improve upon.  In the meantime, one way of at least de-duping some constants is to wrap them in a Prefect Constant task:

```python
from prefect import task, Flow
from prefect.tasks.core.constants import Constant
from prefect.engine.executors import DaskExecutor

@task
def add(x, y):
    return x + y

with Flow('flow') as flow:
    zero = Constant(0)
    for i in range(10):
        add(i, zero)

flow.run(executor=DaskExecutor('localhost:8786'))
```

I'll revisit the logic for this and see how we might improve it.
Makes sense, thanks! One more note: the same behavior would be nice for parameters. Currently:
```
with Flow('flow') as flow:
    for i in range(10):
        add(i, Parameter('x'))

flow.run(parameters={'x': 1}, executor=DaskExecutor('localhost:8786'))


ValueError: A task with the slug "x" already exists in this flow.
```

Resolving patch:
diff --git a/src/prefect/engine/flow_runner.py b/src/prefect/engine/flow_runner.py
--- a/src/prefect/engine/flow_runner.py
+++ b/src/prefect/engine/flow_runner.py
@@ -395,6 +395,10 @@ def get_flow_run_state(
             for task in self.flow.sorted_tasks():
 
                 task_state = task_states.get(task)
+                if task_state is None and isinstance(
+                    task, prefect.tasks.core.constants.Constant
+                ):
+                    task_states[task] = task_state = Success(result=task.value)
 
                 # if the state is finished, don't run the task, just use the provided state
                 if (


Test patch:
diff --git a/tests/engine/cloud/test_cloud_flow_runner.py b/tests/engine/cloud/test_cloud_flow_runner.py
--- a/tests/engine/cloud/test_cloud_flow_runner.py
+++ b/tests/engine/cloud/test_cloud_flow_runner.py
@@ -333,15 +333,13 @@ def raise_me(x, y):
     ]
     assert [type(s).__name__ for s in flow_states] == ["Running", "Failed"]
 
-    assert (
-        client.set_task_run_state.call_count == 6
-    )  # (Pending -> Running -> Finished) * 3
+    assert client.set_task_run_state.call_count == 2  # (Pending -> Running -> Finished)
 
     task_states = [
         call[1]["state"] for call in client.set_task_run_state.call_args_list
     ]
-    assert len([s for s in task_states if s.is_running()]) == 3
-    assert len([s for s in task_states if s.is_successful()]) == 2
+    assert len([s for s in task_states if s.is_running()]) == 1
+    assert len([s for s in task_states if s.is_successful()]) == 0
     assert len([s for s in task_states if s.is_failed()]) == 1
 
 
diff --git a/tests/engine/test_flow_runner.py b/tests/engine/test_flow_runner.py
--- a/tests/engine/test_flow_runner.py
+++ b/tests/engine/test_flow_runner.py
@@ -1538,3 +1538,57 @@ def log_stuff():
         "prefect.CustomFlowRunner",
         "prefect.Task: log_stuff",
     }
+
+
+def test_constant_tasks_arent_submitted(caplog):
+    calls = []
+
+    class TrackSubmissions(LocalExecutor):
+        def submit(self, *args, **kwargs):
+            calls.append(kwargs)
+            return super().submit(*args, **kwargs)
+
+    @prefect.task
+    def add(x):
+        return x + 1
+
+    with Flow("constants") as flow:
+        output = add(5)
+
+    runner = FlowRunner(flow=flow)
+    flow_state = runner.run(return_tasks=[output], executor=TrackSubmissions())
+    assert flow_state.is_successful()
+    assert flow_state.result[output].result == 6
+
+    ## only add was submitted
+    assert len(calls) == 1
+
+    ## to be safe, ensure '5' isn't in the logs
+    assert len([log.message for log in caplog.records if "5" in log.message]) == 0
+
+
+def test_constant_tasks_arent_submitted_when_mapped(caplog):
+    calls = []
+
+    class TrackSubmissions(LocalExecutor):
+        def submit(self, *args, **kwargs):
+            calls.append(kwargs)
+            return super().submit(*args, **kwargs)
+
+    @prefect.task
+    def add(x):
+        return x + 1
+
+    with Flow("constants") as flow:
+        output = add.map([99] * 10)
+
+    runner = FlowRunner(flow=flow)
+    flow_state = runner.run(return_tasks=[output], executor=TrackSubmissions())
+    assert flow_state.is_successful()
+    assert flow_state.result[output].result == [100] * 10
+
+    ## only add and the List task were submitted
+    assert len(calls) == 2
+
+    ## to be safe, ensure '5' isn't in the logs
+    assert len([log.message for log in caplog.records if "99" in log.message]) == 0
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
@@ -134,8 +134,14 @@ def test_merge_with_upstream_skip_arg_raises_error():
 
 def test_merge_diamond_flow_with_results():
     condition = Condition()
-    true_branch = Constant(1)
-    false_branch = Constant(0)
+
+    @task
+    def true_branch():
+        return 1
+
+    @task
+    def false_branch():
+        return 0
 
     with Flow(name="test") as flow:
         ifelse(condition, true_branch, false_branch)
@@ -156,8 +162,14 @@ def test_merge_diamond_flow_with_results():
 
 def test_merge_can_distinguish_between_a_none_result_and_an_unrun_task():
     condition = Condition()
-    true_branch = Constant(None)
-    false_branch = Constant(0)
+
+    @task
+    def true_branch():
+        return None
+
+    @task
+    def false_branch():
+        return 0
 
     with Flow(name="test") as flow:
         ifelse(condition, true_branch, false_branch)
@@ -169,11 +181,16 @@ def test_merge_can_distinguish_between_a_none_result_and_an_unrun_task():
 
 
 def test_merge_with_list():
+    @task
+    def false_branch():
+        return 0
+
+    @task
+    def true_branch():
+        return [1, 2]
+
     with Flow(name="test") as flow:
         condition = Condition()
-        true_branch = prefect.utilities.tasks.as_task([Constant(1), Constant(2)])
-        false_branch = Constant(0)
-
         ifelse(condition, true_branch, false_branch)
         merge_task = merge(true_branch, false_branch)