Repository: quantumlib/Cirq

Issue:
Sweepable is actually a recursive type
The current type signature of Sweepable is
```
Sweepable = Union[Dict[str, float], ParamResolver, Sweep, Iterable[
    Union[Dict[str, float], ParamResolver, Sweep]], None]
```
But there is no logical reason to only allow one level of iteration over Sweep. And in fact, `cirq.to_sweeps` is a recursive function, so it already assumes that Sweepable is a recursive type. So as it stands, Sweepable should be defined as a recursive type.

But I would like to ask whether Sweepable really needs to be this recursive type, rather than just something that can be converted to a single Sweep. Do people actually use lists of sweeps, rather than just plain sweeps? It seems like a rather confusing thing to attempt, given that one would need to keep track of how results correspond to the individual parameter resolvers.


Discussion:
It sounds like a good idea to make Sweepable recursive - it is not trivial though, we need to do some mypy trickery.
We can follow the trick we use with a mypy Protocol to make `OP_TREE` recursive: https://github.com/quantumlib/Cirq/blob/master/cirq/ops/op_tree.py#L26.
Is this done? It looks like `Sweepable` is now defined in terms of `SweepLike`, which is the closest we can get to a recursive type definition until mypy supports them directly. One last possible improvement in this regard: it looks like the cast in https://github.com/quantumlib/Cirq/blob/19497154b627d5cf97455585d33494fb4962b1cf/cirq/study/sweepable.py#L64 the cast could be simplified to `SweepLike`?

Resolving patch:
diff --git a/cirq/study/sweepable.py b/cirq/study/sweepable.py
--- a/cirq/study/sweepable.py
+++ b/cirq/study/sweepable.py
@@ -16,17 +16,27 @@
 
 from typing import Dict, Iterable, Iterator, List, Sequence, Union, cast
 import warnings
+from typing_extensions import Protocol
 
 from cirq._doc import document
 from cirq.study.resolver import ParamResolver, ParamResolverOrSimilarType
 from cirq.study.sweeps import ListSweep, Points, Sweep, UnitSweep, Zip, dict_to_product_sweep
 
 SweepLike = Union[ParamResolverOrSimilarType, Sweep]
-document(SweepLike, """An object similar to an iterable of parameter resolvers.""")  # type: ignore
+document(SweepLike, """An object similar to an iterable of parameter resolvers.""")
 
-Sweepable = Union[SweepLike, Iterable[SweepLike]]
+
+class _Sweepable(Protocol):
+    """An intermediate class allowing for recursive definition of Sweepable,
+    since recursive union definitions are not yet supported in mypy."""
+
+    def __iter__(self) -> Iterator[Union[SweepLike, '_Sweepable']]:
+        pass
+
+
+Sweepable = Union[SweepLike, _Sweepable]
 document(
-    Sweepable,  # type: ignore
+    Sweepable,
     """An object or collection of objects representing a parameter sweep.""",
 )
 
@@ -58,11 +68,7 @@ def to_sweeps(sweepable: Sweepable) -> List[Sweep]:
         product_sweep = dict_to_product_sweep(sweepable)
         return [_resolver_to_sweep(resolver) for resolver in product_sweep]
     if isinstance(sweepable, Iterable) and not isinstance(sweepable, str):
-        return [
-            sweep
-            for item in sweepable
-            for sweep in to_sweeps(cast(Union[Dict[str, float], ParamResolver, Sweep], item))
-        ]
+        return [sweep for item in sweepable for sweep in to_sweeps(item)]
     raise TypeError(f'Unrecognized sweepable type: {type(sweepable)}.\nsweepable: {sweepable}')
 
 


Test patch: