Functions¶
Introduction¶
Functions in Guppy are one of the fundamental departures from the traditional circuit model of quantum computation. Conceptually, functions can be used like in most other modern programming languages: to decompose larger programs into smaller, reusable subroutines that are easier to reason about and can be used to reduce repetition.
Guppy functions can be defined much like normal Python functions:
from guppylang import guppy
from guppylang.std.quantum import qubit, h, x, measure
@guppy
def my_subroutine(q: qubit) -> None:
h(q)
x(q)
With the exception that the @guppy decorator is required, as are type annotations (covered in our section on type checking).
Entrypoint functions¶
Any Guppy function that doesn’t take arguments can be the execution entrypoint for the program, it can be compiled directly in to an executable package.
@guppy
def entrypoint() -> None:
q1 = qubit()
q2 = qubit()
my_subroutine(q1)
my_subroutine(q2)
measure(q1)
measure(q2)
my_hugr_program = entrypoint.compile()
Built-in functions¶
Guppy defines a standard library of functions that can be imported and used in your programs.
For a full list of these please review the guppylang Standard Library Reference.
# Import quantum operation functions from the guppylang standard library
from guppylang.std.quantum import measure, h, qubit
Guppy functions are first-class values¶
Guppy functions are first-class, meaning that we can treat them in our program like a value. For instance, we can bind a function to a variable and pass it around in our program.
@guppy
def a_function(n: int) -> int:
return n + 1
@guppy
def function_is_a_value() -> None:
# bind a variable to our function
my_function = a_function
my_function(100)
function_is_a_value.check()
By default, the Guppy compiler will try to track the identity of a function value at compile time through its type. Consider the following program:
@guppy
def b_function(n: int) -> int:
return 2 * n
@guppy
def call_a_or_b(flag: bool) -> None:
if flag:
f = a_function
else:
f = b_function
f(100)
call_a_or_b.check()
Error: Different types (at <In[5]>:11:4)
|
9 | else:
10 | f = b_function
11 | f(100)
| ^ Variable `f` may refer to different types
Notes:
|
7 | if flag:
8 | f = a_function
| - This is of type `def a_function(n: int) -> int`
9 | else:
10 | f = b_function
| - This is of type `def b_function(n: int) -> int`
Help: Consider adding type annotations to coerce them into opaque function
values: `f: Function[[int], int] = ...`
Guppy compilation failed due to 1 previous error
Here, we tried to assign either a_function or b_function to the variable f, but the compiler tells us that these values have different types.
This is because the default type assigned to a function value statically tracks the corresponding definition it came from.
These are the def a_function(n: int) -> int and def b_function(n: int) -> int types that showed up in the error message above.
To fix this, we can follow the compiler’s suggestion and annotate f with a Function type.
Function is a built-in type that can be imported from the standard library:
from guppylang.std.builtins import Function
Function denotes an opaque function value that is not known at compile-time.
Its syntax is similar to the Callable protocol available in Python.
For example, Function[[int], int] denotes an opaque function that takes and returns an int.
The concrete function definition types def a_function(n: int) -> int and def b_function(n: int) -> int that the compiler assigned to our functions above automatically coerce to the same Function type:
from guppylang.std.builtins import Function
@guppy
def call_a_or_b(flag: bool) -> None:
if flag:
f: Function[[int], int] = a_function
else:
f: Function[[int], int] = b_function
f(100)
call_a_or_b.check()
Now, both branches assign the same type to f, so the function is accepted.
By adding the Function annotation, we have effictively told the compiler that we are willing to give up the static knowledge of which function f corresponds to and are fine with treating it as an opaque function value instead.
Guppy functions can be higher-order functions¶
Since Guppy treats functions as values, we can also define function that take other functions as arguments or return a function as a result.
These are known as higher-order functions.
The preferred method to take functions as arguments is via the Callable protocol already available in Python:
from collections.abc import Callable
For example, in the Guppy snippet below, we define a higher-order function any that checks if a given function f returns True for any elements in an array:
from guppylang.std.builtins import array
@guppy
def any(f: Callable[[int], bool], xs: array[int, 3]) -> bool:
for i in range(3):
if f(i):
return True
return False
We can test our any function by passing in different Guppy functions:
@guppy
def is_even(n: int) -> bool:
return n % 2 == 0
@guppy
def is_positive(n: int) -> bool:
return n > 0
@guppy
def test_any() -> bool:
xs = array(1, 2, 3)
return any(is_even, xs) and any(is_positive, xs)
test_any.check()
Of course, we are only allowed to pass functions with the correct signature:
@guppy
def misuse_any() -> int:
def wrong_types(n: int) -> tuple[str, int]:
return ("Hello", n)
return any(wrong_types, array(1, 2, 3))
misuse_any.check()
Error: Missing protocol implementation (at <In[11]>:7:15)
|
5 | return ("Hello", n)
6 |
7 | return any(wrong_types, array(1, 2, 3))
| ^^^^^^^^^^^ Type `int -> (str, int)` does not implement protocol
| `Callable[[int], bool]`
Guppy compilation failed due to 1 previous error
The main limintation of Callable values is that is currently not possible to annotate a function return type as Callable:
@guppy
def return_callable() -> Callable[[int], bool]:
return is_even
return_callable.check()
Error: Protocols are not allowed as return types (at <In[12]>:2:25)
|
1 | @guppy
2 | def return_callable() -> Callable[[int], bool]:
| ^^^^^^^^^^^^^^^^^^^^^ `Callable` is a protocol, which is not allowed as a return
| type
Help: Consider returning a `Function` type instead
Guppy compilation failed due to 1 previous error
We want to lift this restriction in a future version of Guppy, however, for now the best work around is to use the Function type introduced in the previous section instead of Callable`:
@guppy
def return_function() -> Function[[int], bool]:
return is_even
return_function.check()
The main downside of using Function over Callable is that Function is only valid for actual function definitions, whereas Callable is intended to accept any argument that can be called.
Examples of callable values that are not functions include possible future language features like closures or partial function applications.
Function overloading & static dispatch¶
Function overloading is the ability to create multiple functions with the same name but with different implementations.
Any language that supports function overloading must have a way of selecting which function implementation to use (or dispatch). Guppy uses the form of static dispatch, which means that this selection is resolved at compile time.
@guppy
def plus_state() -> qubit:
q = qubit()
h(q)
return q
@guppy
def apply_h(q: qubit) -> None:
h(q)
@guppy.overload(plus_state, apply_h)
def apply_h_to_something(): ...
@guppy
def use_overloaded_function() -> None:
q = qubit()
# compiler dispatches apply_h() to be used here
apply_h_to_something(q)
# compiler dispatches plus_state() to be used here
other_q = apply_h_to_something()
measure(q)
measure(other_q)
use_overloaded_function.check()
Another example of static dispatch in Guppy is discussed in our section on Generics.