Repository: quantumlib/Cirq

Issue:
Stronger typing for cirq.resolve_parameters
**Is your feature request related to a use case or problem? Please describe.**

Currently the type of `cirq.resolve_parameters` is:
```python
def resolve_parameters(
        val: Any,
        param_resolver: 'cirq.ParamResolverOrSimilarType') -> Any:
```
Since the result is `Any`, we lose type information whenever we resolve parameters. However, for many important cirq objects (gates, operations, moments, circuits) it is typically the case that resolving parameters returns an object of the same type. To avoid losing type information it'd be nice to have a signature like:

```python
def resolve_parameters(
        val: T,
        param_resolver: 'cirq.ParamResolverOrSimilarType') -> T:
```

**Describe the solution you'd like**

Not sure the best way to do this. We probably need a separate top-level method for type-preserving parameter resolution, and a separate `_special_method_` that classes can implement to indicate that their type is preserved under parameter resolution. It would seem to me desirable to make type-preserving resolution be the default since as I said that is most common for cirq types; the exceptions are low-level things like sympy symbols whose type can obviously change when resolved, but which are not cirq types and typically don't need to be resolved directly. One possibility would be to leave `cirq.resolve_parameters` as it is and add `cirq.resolve` as a type-preserving resolution function.

**[optional] Describe alternatives/workarounds you've considered**

Can use type casts to recover type information after resolution, but this is ugly and error-prone. Can also have more specifically typed methods on particular classes that can be called instead of the generic `cirq.resolve_parameters` protocol, but this adds boilerplate and highlights the problem with the resolution protocol itself.

**What is the urgency from your perspective for this issue? Is it blocking important work?**

P3 - I'm not really blocked by it, it is an idea I'd like to discuss / suggestion based on principle

I think the actual amount of work needed to implement this will be modest, once we settle on a strategy.




Discussion:
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 30 days
Cirq Cynque discussion: consensus: let's do this, and let's just change the type signature 

Resolving patch:
diff --git a/cirq/ops/diagonal_gate.py b/cirq/ops/diagonal_gate.py
--- a/cirq/ops/diagonal_gate.py
+++ b/cirq/ops/diagonal_gate.py
@@ -65,7 +65,7 @@ def _gen_gray_code(n: int) -> Iterator[Tuple[int, int]]:
 class DiagonalGate(raw_types.Gate):
     """A gate given by a diagonal (2^n)\\times(2^n) matrix."""
 
-    def __init__(self, diag_angles_radians: List[value.TParamVal]) -> None:
+    def __init__(self, diag_angles_radians: Sequence[value.TParamVal]) -> None:
         r"""A n-qubit gate with only diagonal elements.
 
         This gate's off-diagonal elements are zero and it's on diagonal
diff --git a/cirq/ops/two_qubit_diagonal_gate.py b/cirq/ops/two_qubit_diagonal_gate.py
--- a/cirq/ops/two_qubit_diagonal_gate.py
+++ b/cirq/ops/two_qubit_diagonal_gate.py
@@ -17,7 +17,7 @@
 passed as a list.
 """
 
-from typing import AbstractSet, Any, Tuple, List, Optional, TYPE_CHECKING
+from typing import AbstractSet, Any, Tuple, Optional, Sequence, TYPE_CHECKING
 import numpy as np
 import sympy
 
@@ -26,7 +26,6 @@
 from cirq.ops import gate_features
 
 if TYPE_CHECKING:
-    # pylint: disable=unused-import
     import cirq
 
 
@@ -34,7 +33,7 @@
 class TwoQubitDiagonalGate(gate_features.TwoQubitGate):
     """A gate given by a diagonal 4\\times 4 matrix."""
 
-    def __init__(self, diag_angles_radians: List[value.TParamVal]) -> None:
+    def __init__(self, diag_angles_radians: Sequence[value.TParamVal]) -> None:
         r"""A two qubit gate with only diagonal elements.
 
         This gate's off-diagonal elements are zero and it's on diagonal
diff --git a/cirq/protocols/resolve_parameters.py b/cirq/protocols/resolve_parameters.py
--- a/cirq/protocols/resolve_parameters.py
+++ b/cirq/protocols/resolve_parameters.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 import numbers
-from typing import AbstractSet, Any, TYPE_CHECKING
+from typing import AbstractSet, Any, cast, TYPE_CHECKING, TypeVar
 
 import sympy
 from typing_extensions import Protocol
@@ -25,6 +25,9 @@
     import cirq
 
 
+T = TypeVar('T')
+
+
 class SupportsParameterization(Protocol):
     """An object that can be parameterized by Symbols and resolved
     via a ParamResolver"""
@@ -45,7 +48,7 @@ def _parameter_names_(self: Any) -> AbstractSet[str]:
         """
 
     @doc_private
-    def _resolve_parameters_(self: Any, param_resolver: 'cirq.ParamResolver', recursive: bool):
+    def _resolve_parameters_(self: T, param_resolver: 'cirq.ParamResolver', recursive: bool) -> T:
         """Resolve the parameters in the effect."""
 
 
@@ -130,8 +133,8 @@ def parameter_symbols(val: Any) -> AbstractSet[sympy.Symbol]:
 
 
 def resolve_parameters(
-    val: Any, param_resolver: 'cirq.ParamResolverOrSimilarType', recursive: bool = True
-):
+    val: T, param_resolver: 'cirq.ParamResolverOrSimilarType', recursive: bool = True
+) -> T:
     """Resolves symbol parameters in the effect using the param resolver.
 
     This function will use the `_resolve_parameters_` magic method
@@ -149,6 +152,12 @@ def resolve_parameters(
         replaced with floats or terminal symbols according to the
         given ParamResolver. If `val` has no `_resolve_parameters_`
         method or if it returns NotImplemented, `val` itself is returned.
+        Note that in some cases, such as when directly resolving a sympy
+        Symbol, the return type could differ from the input type; however,
+        for the much more common case of resolving parameters on cirq
+        objects (or if resolving a Union[Symbol, float] instead of just a
+        Symbol), the return type will be the same as val so we reflect
+        that in the type signature of this protocol function.
 
     Raises:
         RecursionError if the ParamResolver detects a loop in resolution.
@@ -160,10 +169,13 @@ def resolve_parameters(
 
     # Ensure it is a dictionary wrapped in a ParamResolver.
     param_resolver = study.ParamResolver(param_resolver)
+
+    # Handle special cases for sympy expressions and sequences.
+    # These may not in fact preserve types, but we pretend they do by casting.
     if isinstance(val, sympy.Basic):
-        return param_resolver.value_of(val, recursive)
+        return cast(T, param_resolver.value_of(val, recursive))
     if isinstance(val, (list, tuple)):
-        return type(val)(resolve_parameters(e, param_resolver, recursive) for e in val)
+        return cast(T, type(val)(resolve_parameters(e, param_resolver, recursive) for e in val))
 
     getter = getattr(val, '_resolve_parameters_', None)
     if getter is None:


Test patch: