Problems templetizing an "easy" function

Context: I just wanted to understand a little bit more about lifetimes and template types and came up with an easy function family which does not make much sense, I was not able to create a template from.
First the function (one for non reference type, and one for the reference type) without a template

fn first_string(first: String, _second: String) -> String {
    first
}

fn first_string_ref<'a>(
    first: &'a String,
    _second: &String,
) -> &'a String {
    first
}

The function(s) do not make much sense and are only used for learning. They seem to look quite equal for the version using the String and the &String.

I tried to following function to make a template from both:

fn first_template<T>(first: T, _second: T) -> T {
    first
}

but this fails with the following test code:

fn test_first_template() {
    let first = "first".to_owned();
    print!("first_string: {first}, ");
    let result = {
        let second = "second".to_owned();
        print!("{second} is ");
        first_template(&first, &second)
    };
    println!("{result}");
}

The error message is completly understood and can be fixed, if I add another template type U for the second parameter. However the following code shall not compile (types of the first and second paramter shall be equal despite of their lifetime):

fn test_first_template_not_compile() {
    let first = "hallo".to_owned();
    print!("first_string: {first}, ");
    let result = {
        let second = vec![0];
        print!("{second:?} is ");
        first_template(&first, &second)
    };
    println!("{result}");
}

So here are my questions (related to stable rust):

  1. How to change first_template, so that it fulfills all the restrictions and compiles test_first_template?
  2. If it is not possible with one template function: Is it possible with two functions (one for reference Arguments and one for move arguments? Here we have another restriction: The reference function shall not compile, if not reference arguments are given (I think this is easy), the non reference function shall not compile if reference arguments are given to it.

You probably want to simply replace the String with the type parameter, if you then unconditionally pass references anyway:

fn first_generic<'a, T>(first: &'a T, _second: &'_ T) -> &'a T {
    first
}

Use "&str" instead of "&String".

Sorry this is not a solution: The template shall also work on non reference types like a simple String.

You can't do that, then. You either allow the arguments to be of different types, or you don't. (Maybe it's possible using specialization, but I wouldn't recommend that, either. It's a weird requirement. You should probably redesign your calling code so that you don't need it.)

You are absolutly right: For the second non templetized function first_string_ref I should have used &str instead of &String. This however does not help solving the problem.

Hallo H2CO3:
The problem has nothing to do with design, only with learning rust. I do not understand your second sentence here: Both arguments are alway required to be the same type. Is there something here I do not understand?

In your test code, the two arguments aren't the same type because they have different lifetimes, and the lifetime of a reference is part of its type. Thus, your test code doesn't fulfil the requirements you've stated and is correctly rejected by the compiler.

4 Likes

The issue isn't with your generic function per se, it's this piece there. The compiler can't know (doesn't seek to know) which of the two arguments is discarded. The two must have the same lifetime, which isn't true here since second is scoped to that block.

4 Likes

Terminology note: "templates" are a C++ thing, Rust uses the word "generic" to refer to this kind of code. Your first_template is a "generic function", T is a "(generic) type parameter", and 'a in first_string_ref is a "(generic) lifetime parameter".

6 Likes

:disappointed_relieved:. Sorry for the inconvenience (my typical programming language is C++ and not rust).

@2e71828 , @erelde : Yes I understand your point and totally agree. The question that I have now: How can I specify two arguments for the generic with the same generic type parameters but different lifetime parameters? Is there a possibility? Something like this, which is not rust code:

fn first_template<'a, T, U>(first: T, _second: U) -> T
where
    T: 'a,
    U == T // U is T except for lifetime
{
    first
}

This feels like an XY problem. Why do you need first and second to have the same type except a lifetime? And which lifetime? Can they be Foo<'a, 'b> and Foo<'c, 'd>? Or only Foo<'a, 'b> and Foo<'c, 'b>? And what do you use that information for?

2 Likes

Please note: This is not a real problem. It is only used to get more information about Rust, especially about what cannot be expressed, even if it seems to be a real easy problem.
For your first question: See above.
For your second question: The lifetime for the parameters of cause (sorry if this is not the answer you expeced, maybe I missed some points here)
For your third question and fourth question: It can be the first or the second alternative. The function shall be a static selector choosing always the first alternative as return value. I know this does not make much sense and it is not requested to make much sense besides to learn about Rust.
Your last question: Just for learning and having some fun :slight_smile:!

To translate your question out of Rust-specific jargon: you're looking for a function which requires two generic types to have the same memory layout, with possibly differing regions of validity, and returns one of them. This is not, in general, supported by the Rust type system because, in your words, "it does not make much sense."

The closest thing I can think of would be something like this:

fn first<'a, T, U, V>(first: U, _second: V) -> impl AsRef<T> + 'a
where
    U: AsRef<T> + 'a,
    V: AsRef<T> + 'a,
{
    first
}

Instead of forcing the two types to be the same, it instead requires them to have a common trait AsRef<T>, and hides which of the two is being returned behind an opaque type that can only be used through the common interface.

1 Like

Thank you for the proposal. However I found out, it has the same problems as before: Both parameters depend on T which has a specific lifetime (lets say it is 't) leading to the problem that the result of first cannot be evaluated in the outer context, because 't is bound to the inner context :frowning: . So the only solution I found, is to have two generic functions:

fn first_by_val<T>(first: T, _second: T) -> T {
    first
}

fn first_by_ref<'a, T>(first: &'a T, _second: &T) -> &'a T {
    first
}

It seems to be impossible to find one generic function because of the fact, that the lifetime is not an attribute of a type but is part of the type which was new to me (but I am a Rust newbe). Thank you for the enlightment in one of your former comments. It was really helpful!
I however fear that this is not the complete story, especially if I think about the Foo<...> types mentioned by @SkiFire13 , but feel a little horror if I dig deeper into the rabbit hole.

I think you're running into trouble with your unusual example because we can't see any reason why you would benefit from defining a function that has these properties. What is the second argument there for? Why does in not matter what the lifetime of the second argument is? But if you want a function like this is seems this would work:

fn first<'a, 'b, T, U, V>(first: U, _second: V) -> U
where
    U: AsRef<T> + 'a,
    V: AsRef<T> + 'b,
{
    first
}

But again, it would be a cleaner function if it accepted only one argument. Another implementation would be

fn first<T, U>(first: U, _second: V) ->T {
    first
}

which would be simpler by far. To get a sensible sense of limitations, you need to express an actual programming scenario where your goal has a purpose.

Edit: I've identified better what bothers me about this example: you're asking how to create an API that has exactly one possible implementation. That in itself isn't inherently a problem, it can be wonderful to express the implementation of a function in its type signature. But this particular function isn't ever going to be a useful one.

Postscript edit: Actually there are multiple possible implementations if you either panic or interact with global variables. But those aren't behaviors I'd want from a generic function...

2 Likes

I don't really understand what you're trying to accomplish with your experiments, but I suspect you still have some ground-level misunderstanding. Let's look at these two functions.

fn first_by_val<T>(first: T, _second: T) -> T {
    first
}

This takes two parameters with the same type and returns the first one. From the API, we only know that it returns something of the same type, and as there are no bounds that let you create the type, it must be the first or second parameter [1].

fn first_by_ref<'a, T>(first: &'a T, _second: &T) -> &'a T {
    first
}

Before we describe this one, let me note that there is some lifetime elision going on here, and this same signature can be made more explicit like so:

fn first_by_ref<'a, 'b, T>(first: &'a T, _second: &'b T) -> &'a T {
    first
}

So, here we have a function that takes two references to the same type, but which may have separate lifetimes, and returns the first one. This time the API guarantees it's the first one, because if we take a step back, the two parameters have distinct types (due to the lifetime) and again there is no bounds that would let us convert between them [2].

Thus, your two functions are quite different; one takes a type once monomorphized, and the other takes two types once monomorphized.

If you wanted the first_by_val to be more like first_by_ref, you could have two type parameters:

// The `U` is new
fn first_by_val<T, U>(first: T, _second: U) -> T {
    first
}

// This is unchanged in signature, but in the body you could...
fn first_by_ref<'a, 'b, T>(first: &'a T, second: &'b T) -> &'a T {
    first_by_val(first, second)
}

And if you wanted first_by_ref to be more like first_by_val instead, you could make the lifetimes (and thus the types) the same:

// This one is unchanged this time
fn first_by_val<T>(first: T, _second: T) -> T {
    first
}

// We've put the same lifetime on both references here so the
// types of the parameters are the same
fn first_by_ref<'a, T>(first: &'a T, second: &'a T) -> &'a T {
    first_by_val(first, second)
}

This last example is not as inflexible as you might think, because references are covariant in their lifetime -- in more practical terms, if you call the latest first_by_ref with two references of different lifetimes, they will be reborrowed to have the same lifetime for the sake of the call.

// Compiles
fn demonstrate_covariant_reborrowing<'x, T>(s: &'static T, x: &'x T) {
    let _ = first_by_ref(s, x);
}

I think you may also benefit from reading about some common lifetime misconceptions, such as

  • T only contains owned types
  • &'a T and T: 'a are the same thing
  • my code isn't generic and doesn't have lifetimes

  1. or unsound, but let's just ignore dirty tricks ↩ī¸Ž

  2. still ignoring ways to do it unsoundly / with UB ↩ī¸Ž

9 Likes

@droundy
Some points to the solution using AsRef:

  1. It works fine, if the type is a String, but
    1. You have to specifiy some of the template arguments during the call: first::<str, _, _>(first, second), because there is more than on implementation of AsRef for String.
    2. The result is not a string but an AsRef, so an additonal call is necessary to get the String back: let result = {...}.as_ref().to_owned();
      This may not be a big thing but it looks a little bit awkward to me.
  2. It does not work for &String arguments: The following code fails to compile:
fn test_first_as_ref() {
    let first = "hallo".to_owned();
    print!("first_string: {first}, ");
    let result = {
        let second = "du".to_owned();
        print!("{second} is ");
        first::<str, _, _>(&first, &second) // Error is located here!
    }
    .as_ref();
    println!("{result}");
}

The error message is:

error[E0597]: `second` does not live long enough
   --> src\main.rs:105:43
    |
102 |       let result = {
    |  __________________-
103 | |         let second = "du".to_owned();
104 | |         print!("{second} is ");
105 | |         first::<str, _, _>(&first, &second)
    | |                                     ^^^^^^^ borrowed value does not live long enough
106 | |     }
    | |     - `second` dropped here while still borrowed
107 | |     .as_ref();
    | |_____________- borrow later used here

This may be astonishing at the first look, but it has something to do with lifetimes be part of the type itself (this is what I assume an read between the lines in the reference). You introduced a generic type T parameter of the function and this type parameter has an associated lifetime: It is the shortest lifetime of the parameters first and second and so in the test function it is the lifetime delimited by the inner block.

Your second funtion

where you most probably mean:

fn first<T, U>(first: T, _second: U) ->T {
    first
}

does work, but it does not request T and U be the same type abstracting away the lifetime. So this requirement is broken.

So I am absolutly convinced that there is no solution with just one generic function! But however there is a solution using two different generic functions, one for arguments without & and one reference version.

Thanks for your answer.

That is exactly, what I do not want to express: The API of the requested function is not "select one of two arguments" its more like "take the first argument and return this (unchanged) and make some calculations using the second argument". So I want an API, that expresses that the lifetime of the second argument will never influence the lifetime of the first argument. This is the first requirement that shall be achieved.

The second requirement is to force the second argument to have the same type as the first argument (abstracting away the lifetimes of the arguments).

The third requirement is that code that does not comply with the first two requirements shall not compile.

This is what I want to express with one generic function: But it is not possible.
Important Note: That it is not possible does not make Rust as a programming language more or less valuable. It only shows the limits of what can be expressed and that is what I want to learn.

So the second try is to achieve this with two functions: One for references and one for non reference arguments: The version accepting only references is easy: my version first_by_ref. The version acceptiong only non references (first_by_val) has the deficiency that it does not only accept non references, because as you mentioned a generic type T may also be a reference type. But that is ok, because I get a compiler error, if I use this function in the wrong context:

fn test_first_by_val_with_ref() {
    let first = "hallo".to_owned();
    print!("first_string: {first}, ");
    let result = {
        let second = "du".to_owned();
        print!("{second} is ");
        first_by_val(&first, &second)
    };
    println!("{result}");
}

The message states that second does not live long enough. Exactly what I wanted to learn. Of cause this is not real world code.

Now lets go to your suggetions for the two functions:

Here first_by_val does not restrict to the same type and breaks requirement two.

The second one

breaks the requirement of independent lifetimes. The following code does not compile any more:

fn test_first_by_ref_same_lifetime() {
    let first = "hallo".to_owned();
    print!("first_string: {first}, ");
    let result = {
        let second = "du".to_owned();
        print!("{second} is ");
        first_by_ref(&first, &second) // Error: second does not live long enough
    };
    println!("{result}");
}

as expected. So livetime elision here does not help here.

Closing note: I know that this is not an example for code relevant in practise. This code is only used for me to learn what is possible to express in Rust and where the limitations are!