Repository: PennyLaneAI/pennylane

Issue:
Raise exception or warning when `qml.adjoint` is used on operation list instead of function.
The newer `qml.adjoint` function does not have any effect when acting on operation lists like in the following example:
```python
params = list(range(4))
qml.adjoint(qml.templates.AngleEmbedding(params))
```
Users might try this, because it worked like this with `qml.inv` which `qml.adjoint` is replacing. Therefore, we should raise
an exception whenever this is attempted by checking for the input to be `callable`. Alternatively, a warning could be raised, but 
the behaviour differs fundamentally from the expected, an exception seems more appropriate.




Discussion:
Is there no way to make the above work?  I remember that there was a good reason, but it would be so nice and intuitive! 

For example, the transform could make a function for the gate?
> Is there no way to make the above work? I remember that there was a good reason, but it would be so nice and intuitive!

Unfortunately I don't think so, since Python always evaluates 'inside out' - it will evaluate `qml.templates.AngleEmbedding(params)` before any `adjoint` logic is ever executed. So this requires `adjoint` to go back and 'hack' inside the queue, which can often go wrong.

This is one of the reasons that `qml.inv()` was so nasty internally; it was performing a lot of queuing hacks to make the above work. In fact, `qml.inv()` showing up in the 'files changed' tab in a recent refactor was one of the reasons it got marked as priority for removal 😆 

> but it would be so nice and intuitive!

My hope is that with good documentation and consistent usage, the above _does_ become intuitive to PL users. The same way that `qml.grad(fn)(params)` has become intuitive 🙂 
@josh, by "the above" do you mean something like 
```
qml.adjoint(qml.templates.AngleEmbedding)(params)
```
?
@co9olguy, yes, exactly. This is the UX that is consistent across other functional libraries, c.f. both Autograd and JAX, and slowly also TF, since it results in very intuitive composability due to the transform acting on _functions_, rather than objects:

```python
>>> autograd.grad(fn)(params)
>>> jax.vmap(fn)(batched_params)
>>> tf.function(fn)(tensor)
>>> np.vectorize(fn)(params)
```

It is also a UX that is not out of place in PennyLane (`qml.grad(fn)(params)`, `qml.draw(qnode)(params)`).

Here, we just need to get used to treating quantum operations _themselves_ as (quantum) functions, rather than objects:

```python
def qfunc(params):
    qml.templates.AngleEmbedding(params, wires=[0, 1])

@qml.qnode(dev)
def circuit(params):
    qml.adjoint(qfunc)(params)
    qml.adjoint(qml.templates.AngleEmbedding)(params, wires=[0, 1])
```
Just a crazy thought - could we turn operations into functions or wrap them in functions?

Resolving patch:
diff --git a/pennylane/transforms/adjoint.py b/pennylane/transforms/adjoint.py
--- a/pennylane/transforms/adjoint.py
+++ b/pennylane/transforms/adjoint.py
@@ -108,6 +108,12 @@ def circuit():
         >>> print(qml.draw(circuit)())
         0: --RX(0.123)--RX(-0.123)--| <Z>
     """
+    if not callable(fn):
+        raise ValueError(
+            f"The object {fn} of type {type(fn)} is not callable. "
+            "This error might occur if you apply adjoint to a list "
+            "of operations instead of a function or template."
+        )
 
     @wraps(fn)
     def wrapper(*args, **kwargs):


Test patch:
diff --git a/tests/transforms/test_adjoint.py b/tests/transforms/test_adjoint.py
--- a/tests/transforms/test_adjoint.py
+++ b/tests/transforms/test_adjoint.py
@@ -59,7 +59,7 @@ def test_barrier_adjoint():
 
     @qml.qnode(dev)
     def my_circuit():
-        qml.adjoint(qml.Barrier)(wires=0)
+        adjoint(qml.Barrier)(wires=0)
         return qml.state()
 
     assert my_circuit()[0] == 1.0
@@ -99,6 +99,26 @@ def my_circuit():
     )
 
 
+with qml.tape.JacobianTape() as tape:
+    qml.PauliX(0)
+    qml.Hadamard(1)
+
+noncallable_objects = [
+    qml.RX(0.2, wires=0),
+    qml.AngleEmbedding(list(range(2)), wires=range(2)),
+    [qml.Hadamard(1), qml.RX(-0.2, wires=1)],
+    tape,
+]
+
+
+@pytest.mark.parametrize("obj", noncallable_objects)
+def test_error_adjoint_on_noncallable(obj):
+    """Test that an error is raised if qml.adjoint is applied to an object that
+    is not callable, as it silently does not have any effect on those."""
+    with pytest.raises(ValueError, match=f"{type(obj)} is not callable."):
+        adjoint(obj)
+
+
 class TestOutsideOfQueuing:
     """Test that operations and templates work with the adjoint transform when
     created outside of a queuing context"""
@@ -109,7 +129,7 @@ class TestOutsideOfQueuing:
     def test_single_op_non_param_adjoint(self, op, wires):
         """Test that the adjoint correctly inverts non-parametrized
         operations"""
-        op_adjoint = qml.adjoint(op)(wires=wires)
+        op_adjoint = adjoint(op)(wires=wires)
         expected = op(wires=wires).adjoint()
 
         assert type(op_adjoint) == type(expected)
@@ -121,7 +141,7 @@ def test_single_op_non_param_adjoint(self, op, wires):
     def test_single_op_param_adjoint(self, op, par, wires):
         """Test that the adjoint correctly inverts operations with a single
         parameter"""
-        param_op_adjoint = qml.adjoint(op)(*par, wires=wires)
+        param_op_adjoint = adjoint(op)(*par, wires=wires)
         expected = op(*par, wires=wires).adjoint()
 
         assert type(param_op_adjoint) == type(expected)
@@ -136,7 +156,7 @@ def test_single_op_param_adjoint(self, op, par, wires):
     @pytest.mark.parametrize("template, par, wires", template_ops)
     def test_templates_adjoint(self, template, par, wires):
         """Test that the adjoint correctly inverts templates"""
-        res = qml.adjoint(template)(*par, wires=wires)
+        res = adjoint(template)(*par, wires=wires)
         result = res if hasattr(res, "__iter__") else [res]  # handle single operation case
         expected_ops = template(*par, wires=wires)
 
@@ -150,7 +170,7 @@ def test_templates_adjoint(self, template, par, wires):
     def test_cv_template_adjoint(self):
         """Test that the adjoint correctly inverts CV templates"""
         template, par, wires = qml.templates.Interferometer, [[1], [0.3], [0.2, 0.3]], [2, 3]
-        result = qml.adjoint(template)(*par, wires=wires).expand().operations
+        result = adjoint(template)(*par, wires=wires).expand().operations
         expected_ops = template(*par, wires=wires).expand().operations
 
         for o1, o2 in zip(result, reversed(expected_ops)):