1

Some Background (feel free to skip):

I'm very new to Rust, I come from a Haskell background (just in case that gives you an idea of any misconceptions I might have).

I am trying to write a program which, given a bunch of inputs from a database, can create customisable reports. To do this I wanted to create a Field datatype which is composable in a sort of DSL style. In Haskell my intuition would be to make Field an instance of Functor and Applicative so that writing things like this would be possible:

type Env = [String]
type Row = [String]

data Field a = Field
    { fieldParse :: Env -> Row -> a }

instance Functor Field where
    fmap f a = Field $
        \env row -> f $ fieldParse a env row

instance Applicative Field where
    pure = Field . const . const
    fa <*> fb = Field $
        \env row -> (fieldParse fa) env row
            $ (fieldParse fb) env row

oneField :: Field Int
oneField = pure 1

twoField :: Field Int
twoField = fmap (*2) oneField

tripleField :: Field (Int -> Int)
tripleField = pure (*3)

threeField :: Field Int
threeField = tripleField <*> oneField

Actual Question:

I know that it's quite awkward to implement Functor and Applicative traits in Rust so I just implemented the appropriate functions for Field rather than actually defining traits (this all compiled fine). Here's a very simplified implementation of Field in Rust, without any of the Functor or Applicative stuff.

use std::result;
use postgres::Row;
use postgres::types::FromSql;

type Env = Vec<String>;

type FieldFunction<A> = Box<dyn Fn(&Env, &Row) -> Result<A, String>>;

struct Field<A> {
    field_parse: FieldFunction<A>
}

I can easily create a function which simply gets the value from an input field and creates a report Field with it:

fn field_good(input: u32) -> Field<String> {
    let f = Box::new(move |_: &Env, row: &Row| {
        Ok(row.get(input as usize))
    });

    Field { field_parse: f }
}

But when I try to make this polymorphic rather than using String I get some really strange lifetime errors that I just don't understand:

fn field_bad<'a, A: FromSql<'a>>(input: u32) -> Field<A> {
    let f = Box::new(move |_: &Env, row: &Row| {
        Ok(row.get(input as usize))
    });

    Field { field_parse: f }
}
error[E0495]: cannot infer an appropriate lifetime for autoref due to conflicting requirements
  --> src/test.rs:36:16
   |
36 |         Ok(row.get(input as usize))
   |                ^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #2 defined on the body at 35:22...
  --> src/test.rs:35:22
   |
35 |       let f = Box::new(move |_: &Env, row: &Row| {
   |  ______________________^
36 | |         Ok(row.get(input as usize))
37 | |     });
   | |_____^
note: ...so that reference does not outlive borrowed content
  --> src/test.rs:36:12
   |
36 |         Ok(row.get(input as usize))
   |            ^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the function body at 34:14...
  --> src/test.rs:34:14
   |
34 | fn field_bad<'a, A: FromSql<'a>>(input: FieldId) -> Field<A> {
   |              ^^
note: ...so that the types are compatible
  --> src/test.rs:36:16
   |
36 |         Ok(row.get(input as usize))
   |                ^^^
   = note: expected `FromSql<'_>`
              found `FromSql<'a>`

Any help explaining what this error is actually getting at or how to potentially fix it would be much appreciated. I included the Haskell stuff so that my design intentions are clear, that way if the problem is that I'm using a programming style that doesn't really work in Rust, then that could be pointed out to me.

EDIT: Forgot to include a link to the docs for postgres::Row::get in case it's relevant. They can be found here.

Aplet123
  • 33,825
  • 1
  • 29
  • 55
James Burton
  • 746
  • 3
  • 12

2 Answers2

1

Higher-ranked trait bounds

I'm not familiar with the postgresql crate being used, so I can't give any general guidance on the approach the code is taking.

But anyway, sticking to the immediate problem with lifetimes in the current implementation, higher-ranked trait bounds might be workable. I would need a working example to confirm this though.

This is a new one for me. I tried out your code, and got some interesting error message indicating use of for in declaring bounds. Which brought me here in the Rust Reference.

So I gave it a try, and it compiles.

fn field_bad<A>(input: u32) -> Field<A> 
where A: for<'a> FromSql<'a>
{
    let f = Box::new(move |_: &Env, row: &Row| {
        Ok(row.get(input as usize))
    });

    Field { field_parse: f }
}

This section in the Rustnomicon talks about HRTB's.

And this SO post talks about how lifetimes and higher-ranked trait bounds differ.

What it looks like is happening here is field_bad() isn't returning any FromSql instances - either directly or indirectly via Field. FromSql is just part of the Ok(T) return type of the closure, which exists as a field in Field. So the for<..> syntax is allowing us to provide the lifetime that applies to the type the closure will return when invoked.

Todd
  • 4,669
  • 1
  • 22
  • 30
  • That did indeed compile, thanks! I'm not going to pretend that I understand why but I'll give the other docs you linked a read and hopefully then it will make sense. – James Burton Jun 11 '21 at 09:47
  • Ok so having read the SO post you liked on HRTBs I think I am starting to understand (sort of) what's going on. – James Burton Jun 11 '21 at 10:04
  • @JamesBurton were you able to get your code working with this approach? BTW, I don't think it started making a whole lot of sense for me until I read over parts of each of the refs I linked. – Todd Jun 11 '21 at 10:27
  • The code compiles but I can't confirm that it 100% works as intended until I sort out a few other things but I'll be sure to leave an update here. – James Burton Jun 11 '21 at 11:27
  • Can confirm that the code runs as required. – James Burton Jun 16 '21 at 09:14
0

So I seem to have fixed it, although I'm still not sure I understand exactly what I've done...

type FieldFunction<'a, A> = Box<dyn Fn(&Env, &'a Row) -> Result<A, String>>;

struct Field<'a, A> {
    field_parse: FieldFunction<'a, A>
}

fn field_bad<'a, A: FromSql<'a>>(input: u32) -> Field<'a, A> {
    let f = Box::new(move |_: &Env, row: &'a Row| {
        Ok(row.get(input as usize))
    });

    Field { field_parse: f }
}

I also swear I tried this several times before but there we are...

James Burton
  • 746
  • 3
  • 12