microsoft / qsharp

Azure Quantum Development Kit, including the Q# programming language, resource estimator, and Quantum Katas
https://microsoft.github.io/qsharp/
MIT License
407 stars 82 forks source link

Not all data types can be converted to String using string interpolation #1083

Open tcNickolas opened 7 months ago

tcNickolas commented 7 months ago

Describe the bug

Not all data types can be converted to String using string interpolation. I've noticed this for UDTs, with both named and unnamed items, there might be other types affected. This also means that certain types of type-parameterized functions can no longer be implemented, compared to Q# 0.*.

To Reproduce

The following code doesn't work for UDTs:

    newtype AnonymousPair = (Int, Bool);
    newtype NamedPair = (First : Int, Second : Bool);

    let ap = AnonymousPair(1, false);
    let np = NamedPair(2, true);
    Message($"UDT with anonymous items = {ap}");
    Message($"UDT with named items = {np}");

(If I unwrap them, though, using ap! and np!, the resulting tuples will be printed)

The following type-parameterized function used to work in Q# 0.* but fails to compile now.

/// # Summary
/// A function that converts an argument to a string
/// and appends a prefix to it.
function ToStringWithPrefix<'T>(
    a : 'T,
    prefix : String
) : String {
    return $"{prefix} {a}";
}

Expected behavior

I expect any Q# type to be convertible to String via string interpolation.

System information

swernli commented 7 months ago

This was a source of some debate when string interpolation was first added to the new stack, as we weren't sure there was value in being able to convert all types into strings. Notably, callables and type-parametrized variables are not convertible to strings, and UDTs were added later and not explicitly given support for string conversion. I definitely think it's worth decided how to display UDTs (possibly via printing them purely as tuples the same way they would be if unwrapped). But that doesn't address the second point of being able to support generics in string interpolation. Perhaps that warrants its own separate issue for discussion?

tcNickolas commented 7 months ago

There absolutely is value in converting data types into strings! In another project (I haven't updated that to Modern QDK yet) I'm using string representation of a callable (its name) in a demo where I run the same code for different operations and their eigenvectors/eigenvalues and print the name to show which one I'm doing right now. ToStringWithPrefix is admittedly a very contrived example that I came up with to illustrate generics, I don't need it for real work, but the demo that prints operation name has a real-life practical use.

I'm not sure about lambdas and qubits, but other than those two, I think other data types have natural representations and should be convertible to strings.

tcNickolas commented 5 months ago

Any updates on converting callables into strings? I ended up using a workaround for Complex and CompexPolar types in the ComplexArithmetic kata, though I still want to convert UDTs into strings the way we do with tuples, but I can't think of a nice workaround for callables.

swernli commented 5 months ago

We haven't worked on this yet, sorry @tcNickolas. UDTs and callables that are defined globally should be possible to handle in a coherent way, but the complexity comes in with lambdas, which are lifted into globals during compilation but don't have any well known names. Do you have any suggestions for how you would want those to be displayed?

tcNickolas commented 5 months ago

In my scenarios I need callables that are defined globally, I don't really expect a reasonable name printed for a lambda. In Classic QDK, an internal name was printed:

    operation HelloQ () : Unit {
        let a = q => H(q);
        Message($"{a}");
    }

would yield _10a961aab4994242bcee705de78cc704_HelloQ{_}

I can imagine two ways to approach this:

  1. A descriptive (but fake) name that says it's a lambda and lists its signature, for this example, something like lambda (Qubit => Unit is Adj + Ctl)
  2. Lambda expressions are short, so we could print its definition itself as its name, possibly with a prefix lambda: q => H(q) or lambda: q => H(q)

I like the second one more, it's more informative.

minestarks commented 3 months ago

CONTRIBUTORS PLEASE READ

Welcome! Please read carefully if you'd like to contribute a fix to this issue.

For this issue we're looking for a way to support callables inside interpolated strings in Message() s. The below code sample should run without errors.

Q# playground link

namespace MyQuantumProgram {

    @EntryPoint()
    operation Main() : Unit {
        mutable x = MyType;
        Message($"current value of x = {x} should match {MyType}");

        set x = MyFunction;
        Message($"current value of x = {x} should match {MyFunction}");

        mutable y = MyOperation;
        Message($"current value of y = {y} should match {MyOperation}");

        set y = (Int, Bool) => MyType(1, false);
        let z = y;
        Message($"current value of y = {y} should match {z}");
    }

    newtype MyType = (Int, Bool);

    operation MyOperation(i : Int, b : Bool) : MyType { MyType(i, b) }

    function MyFunction(i : Int, b : Bool) : MyType { MyType(i, b) }
}

Getting started

Take a look through our README to orient yourself in the repo and find instructions on how to build.

For this issue, you'll want to have a working knowledge of Rust and compilers.

The first part of the fix is changing the type system so that callables are accepted inside interpolated strings. In other words, making the code above compile without errors. The below is a starting point: https://github.com/microsoft/qsharp/blob/cfab56cc00d1415566c70b10765f1e0b4a71ad81/compiler/qsc_frontend/src/typeck/infer.rs#L1007

The second part would be coming up with an actual string representation for the callable used in the string, so that the Message outputs in the above sample look correct. For functions and operations, the fully qualified name could be a good idea. Lambda expressions might be more complicated, but any unique and consistent name would work. (See the discussion above for thoughts on reasonable names). For this part, you may want to take a look at how string components are evaluated at runtime: https://github.com/microsoft/qsharp/blob/cfab56cc00d1415566c70b10765f1e0b4a71ad81/compiler/qsc_eval/src/lib.rs#L790

Testing

You can demonstrate that the feature works by running the playground locally (see the code example in the issue description).

Please add unit tests verifying the functionality you implemented.

Before you submit a pull request please run python ./build.py to ensure the project builds cleanly. See README for details.

Reviews

Please ensure your code is tested, clean and well refactored before opening a pull request. If you're not sure, feel free to open a draft PR for preliminary feedback.

Once you have published your PR, the codeowners will automatically get tagged and we'll review shortly.

If you need clarification on an issue please tag @minestarks with your questions.