|
| 1 | +--- |
| 2 | +title: "Partial application in Q#" |
| 3 | +author: filipw |
| 4 | +date: 2021-12-14 |
| 5 | +categories: |
| 6 | + - blog |
| 7 | +tags: |
| 8 | + - contributed-post |
| 9 | + - language |
| 10 | + - compiler |
| 11 | +--- |
| 12 | + |
| 13 | +🎄 This post is part of the [Q# Advent Calendar 2021](https://devblogs.microsoft.com/qsharp/q-advent-calendar-2021/). 🎅 |
| 14 | + |
| 15 | +One of the very useful, though not very well known, language features of Q# is its support for partial application of callables. In general, we can say that a partially applied function is a function that is manually derived from another function by supplying fewer parameters than the original function expects. This concept is an integral part of almost every functional programming language, though it might be a bit exotic for developers with other backgrounds, especially as many languages of more object-oriented nature, such as C#, do not have native support for partial function application. |
| 16 | + |
| 17 | +### Basics of partial application |
| 18 | + |
| 19 | +Consider the following basic Q# function, which defined three parameters of `Int`, `String` and `Result` type. The body of the function is irrelevant for this discussion, and we only need to focus on its signature: |
| 20 | + |
| 21 | +``` |
| 22 | +function ThreeArgumentFunction(number : Int, text : String, result : Result) : Unit { |
| 23 | + Message($"{number}"); |
| 24 | + Message(text); |
| 25 | + Message($"{result}"); |
| 26 | +} |
| 27 | +``` |
| 28 | + |
| 29 | +With the regular function invocation process, we'd need to always supply all three argument values when the function is called. However, using partial application, the function can be transformed, without being evaluated, into another function which accepts less parameters than the original one. At this point we have a function pointer that may be invoked with a different parameter set than the source function, or that could be passed around to other functions. |
| 30 | + |
| 31 | +More practically speaking, this new function is created when the calling code passes only the subset of function arguments - those, whose values it can provide already at that point. The rest of the arguments have to be replaced with the discard symbol `_` and the new function only expects parameters corresponding to those for which the discard was used. |
| 32 | + |
| 33 | +For example by passing a specific `Result` value such as `Zero` as positional argument three, we can convert our `ThreeArgumentFunction` into a new two argument function, which defines two parameters of `Int` and `String` type only, and which internalizes the pass-in argument `Zero`. |
| 34 | + |
| 35 | +``` |
| 36 | +let twoArgumentFunction = ThreeArgumentFunction(_, _, Zero); |
| 37 | +``` |
| 38 | + |
| 39 | +Using the same methodology, by taking the newly created `twoArgumentFunction` and passing a specific value for one of the arguments and using a discard for the other, the function can be converted into a single argument one: |
| 40 | + |
| 41 | +``` |
| 42 | +let oneArgumentFunction = twoArgumentFunction(100, _); |
| 43 | +``` |
| 44 | + |
| 45 | +At that point we may call the resulting function `oneArgumentFunction` with just a `String` parameter: |
| 46 | + |
| 47 | +``` |
| 48 | +oneArgumentFunction("hello!"); |
| 49 | +``` |
| 50 | + |
| 51 | +### Partial application with tuples |
| 52 | + |
| 53 | +Things get even more interesting with tuples, in particular due to singleton tuple equivalence in Q#. Let us first consider a function accepting two parameters, both of them tuples of different kind. |
| 54 | + |
| 55 | +``` |
| 56 | +function TwoArgumentFunctionWithTuples( |
| 57 | + numbers : (Int, Int, Int), |
| 58 | + results : (Result, Result, Result)) : Unit { |
| 59 | + Message($"{numbers}"); |
| 60 | + Message($"{results}"); |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +The typical way of calling such a function might look like this: |
| 65 | + |
| 66 | +``` |
| 67 | +TwoArgumentFunctionWithTuples((100, 200, 300), (Zero, Zero, Zero)); |
| 68 | +``` |
| 69 | + |
| 70 | +With partial application, similarly to what we did before, we may choose to supply one tuple upfront, and create a partially applied function that accepts just one tuple. At that point, because of the singleton tuple equivalence, the new function can be called as if it accepted three top-level parameters, instead of a single three-element tuple - sparing us from the explicit brackets. An example of that is shown next. |
| 71 | + |
| 72 | +``` |
| 73 | +let fn = TwoArgumentFunctionWithTuples((100, 200, 300), _); |
| 74 | +
|
| 75 | +// both of these work now! |
| 76 | +fn((Zero,Zero,Zero)); |
| 77 | +fn(Zero,Zero,Zero); |
| 78 | +``` |
| 79 | + |
| 80 | +We can also reach into an even more extravagant example, where nested tuples are used inside tuples. An example of such function definition might be (notice the composition of the `data` parameter, as tuple of tuples): |
| 81 | + |
| 82 | +``` |
| 83 | + function TwoArgumentFunctionWithNestedTuples( |
| 84 | + numbers : (Int, Int, Int), |
| 85 | + data : ((String, String), (Result, Result, Result))) : Unit { |
| 86 | + Message($"{numbers}"); |
| 87 | + Message($"{data}"); |
| 88 | + } |
| 89 | +``` |
| 90 | + |
| 91 | +In such situation Q# still allows any subset of any of the tuple parameters to be left unspecified in any configuration, which is a very powerful tool. For example we could choose to use a discard in each of the three (one top level, and two nested ones) tuples: |
| 92 | + |
| 93 | +``` |
| 94 | +let fn1 = TwoArgumentFunctionWithNestedTuples((10, _, 30), (("item1", _), (_, One, Zero))); |
| 95 | +``` |
| 96 | + |
| 97 | +The resulting partially applied function now takes a single `Int` and a tuple `(String, Result)` as parameters. To take this concept further, we could, for example, supply just the `String` now, thus creating yet another function, which can now be called with two top level arguments only - `Int` and `Result`: |
| 98 | + |
| 99 | +``` |
| 100 | +let fn2 = fn1(_, ("item2", _)); |
| 101 | +fn2(20, Zero); |
| 102 | +``` |
| 103 | + |
| 104 | +The end result is pretty cool, because we reduced a massive function signature with nested tuples into something very simple to call. |
| 105 | + |
| 106 | +### Usage with built-in function |
| 107 | + |
| 108 | +Partial application allows you to not only massage your own callables into various variants to facilitate specific workflows, but also - naturally - to combine built-in QDK functions and operations in useful and innovative ways. For example, there is a core Q# function `IsCoprimeI(a : Int, b : Int) : Bool`. You could partially apply one of the two parameters to be able to use it as predicate in array filtering. For example, the following code filters an input array of integers based on the fact whether the given element is a co-prime with `5`: |
| 109 | + |
| 110 | +``` |
| 111 | +function FilterByFiveAsCoPrime(numbers : Int[]) : Int[] { |
| 112 | + let isFiveCoPrime = IsCoprimeI(_, 5); |
| 113 | + return Filtered(isFiveCoPrime, numbers); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +### Quantum operations |
| 118 | + |
| 119 | +Of course partial application can be used not just with Q#'s functions, but also with operations. Consider the following operation which prepares the entangled state of two qubits, and allows to run some arbitrary preparation code on either of them: |
| 120 | + |
| 121 | +``` |
| 122 | +operation InitEntangledState( |
| 123 | + q1 : Qubit, q2 : Qubit, |
| 124 | + q1Preparation : (Qubit => Unit is Adj + Ctl), |
| 125 | + q2Preparation : (Qubit => Unit is Adj + Ctl)) : Unit is Adj + Ctl { |
| 126 | + q1Preparation(q1); |
| 127 | + q2Preparation(q2); |
| 128 | + H(q1); |
| 129 | + CNOT(q1, q2); |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +This is a very basic example, but it shows the general direction in which partial application can be used. We can use this generic operation to create partially applied variants that will be our shortcuts for creating the four Bell states: |
| 134 | + |
| 135 | +``` |
| 136 | +let initPhiPlus = InitEntangledState(_, _, I, I); |
| 137 | +// we can now invoke initPhiPlus(qubit1, qubit2)... |
| 138 | +
|
| 139 | +let initPhiMinus = InitEntangledState(_, _, X, I); |
| 140 | +// we can now invoke initPhiMinus(qubit1, qubit2)... |
| 141 | +
|
| 142 | +let initPsiPlus = InitEntangledState(_, _, I, X); |
| 143 | +// we can now invoke initPsiPlus(qubit1, qubit2)... |
| 144 | +
|
| 145 | +let initPsiMinus = InitEntangledState(_, _, X, X); |
| 146 | +// we can now invoke initPsiMinus(qubit1, qubit2)... |
| 147 | +``` |
| 148 | + |
| 149 | +A common scenario would be to mark the generic operation as `internal` to hide it, and then expose only the partial variants as visible callables. Notice we can do that using `functions`, even though we are wrapping an `operation`, because partial application has no effect on the quantum state. |
| 150 | + |
| 151 | +``` |
| 152 | +function InitPhiPlus() : ((Qubit, Qubit) => Unit is Adj + Ctl) { |
| 153 | + return InitEntangledState(_, _, I, I); |
| 154 | +} |
| 155 | +
|
| 156 | +function InitPhiMinus() : ((Qubit, Qubit) => Unit is Adj + Ctl) { |
| 157 | + return InitEntangledState(_, _, X, I); |
| 158 | +} |
| 159 | +
|
| 160 | +function InitPsiPlus() : ((Qubit, Qubit) => Unit is Adj + Ctl) { |
| 161 | + return InitEntangledState(_, _, I, X); |
| 162 | +} |
| 163 | +
|
| 164 | +function InitPsiMinus() : ((Qubit, Qubit) => Unit is Adj + Ctl) { |
| 165 | + return InitEntangledState(_, _, X, X); |
| 166 | +} |
| 167 | +
|
| 168 | +internal operation InitEntangledState( |
| 169 | + q1 : Qubit, q2 : Qubit, |
| 170 | + q1Preparation : (Qubit => Unit is Adj + Ctl), |
| 171 | + q2Preparation : (Qubit => Unit is Adj + Ctl)) : Unit is Adj + Ctl { |
| 172 | + q1Preparation(q1); |
| 173 | + q2Preparation(q2); |
| 174 | + H(q1); |
| 175 | + CNOT(q1, q2); |
| 176 | +} |
| 177 | +``` |
| 178 | + |
| 179 | +Those can now be called in the following way: |
| 180 | + |
| 181 | +``` |
| 182 | +InitPhiPlus()(qubit1, qubit2); |
| 183 | +InitPhiMinus()(qubit1, qubit2); |
| 184 | +InitPsiPlus()(qubit1, qubit2); |
| 185 | +InitPsiMinus()(qubit1, qubit2); |
| 186 | +``` |
| 187 | + |
| 188 | +Of course in this simple example, a semantically equivalent result could be achieved by simply adding relevant Bell state specific overloads to `InitEntangledState`. However, in general, partial application really shines when we are passing around functions as first-class values or when using this methodology to craft new operations from existing ones. One example could be if we needed to use four Bell state preparation callables as oracles conforming to the signature `(Qubit, Qubit) => Unit is Adj + Ctl`. |
0 commit comments