Sqlite: caching prepared statements .... again

If "is static" here means that it can be referenced freely as it won't be ever moved / freed, then you cannot safely do this while you still own the object. If you own it, you're responsible for cleaning it up and it will be cleaned up as soon as it is dropped.
If you want to give this guarantee, you need to give up ownership. This is exactly why Box::leak exists -- it allows you to give up your ownership of the Box, leaking it forever, and gives you a &'a mut T for any lifetime you want, including 'static. It is yet unstable (stabilization FCP is underway), but it's just one line of unsafe code. And there's a crate for this, too.
However, I have to agree that these are not exactly discoverable.

This topic caught my interest, so I tried to create a safe global cache object and was unsuccessful. However I did manage to create an unsafe implementation (probably not the best, I'm no Rust expert). The DBCache object is created in main(), and a pointer to it is stored in thread local storage. Then a global cache_unsafe() function creates an &mut for the cache on demand. That implementation is below.

This is of course pretty dangerous, so I'd only use it as a last resort myself. Since there are no compile-time guarantees for the global, you have to be careful of re-entrant modifications. For instance: a list of objects where some function is being called on each object, and one of those tries to modify the list. A safer approach might be to return a mutex-like object that panics if some code tries to acquire a reference when one is already outstanding.

IMO, it does seem like a flaw that there is no way to safely describe this. There are contexts where you may be in a callback where you don't have a pointer/reference to application state. A static like this would be the only way to retrieve that. The futures lib seems to need this capability as well: https://github.com/rust-lang-nursery/futures-rs/blob/master/src/task_impl/std/mod.rs

extern crate sqlite;

use sqlite::{Connection, State, Statement};
use std::collections::HashMap;
use std::cell::RefCell;

struct DBCache<'a> {
    conn: Connection,
    stmts:HashMap<String,Statement<'a>>
}

thread_local! {
    // refcell that contains a pointer to the actual cache object.
    // only main should use this, everything else should use cache_unsafe()
    static CACHE: RefCell<*mut DBCache<'static>> = RefCell::new(std::ptr::null_mut());
}

fn cache_unsafe() -> &'static mut DBCache<'static> {
    let mut ptr:*mut DBCache<'static> = std::ptr::null_mut();
    CACHE.with(|cptr| {
        let x = cptr.borrow_mut();
        if *x == std::ptr::null_mut() {
            panic!("null cache pointer!")
        }
        ptr = *x;
    });
    unsafe { &mut *ptr }
}

fn process() {
    let cache = cache_unsafe();

    // first query
    let stmtkey = "first".to_owned();
    if cache.stmts.contains_key(&stmtkey) {
        let stmt = cache.stmts.get_mut(&stmtkey).unwrap();
        while let State::Row = stmt.next().unwrap() {
            println!("{}", stmt.read::<i64>(0).unwrap());
        }
    }

    // add second, then query
    let stmtkey = "second".to_owned();
    if !cache.stmts.contains_key(&stmtkey) {
        cache.stmts.insert(stmtkey.to_owned(), cache.conn.prepare("select 123456").unwrap());
    }
    let stmt = cache.stmts.get_mut(&stmtkey).unwrap();
    while let State::Row = stmt.next().unwrap() {
        println!("{}", stmt.read::<i64>(0).unwrap());
    }
}

pub fn main() {
    let mut _cache = {
        let db = Connection::open(":memory:").unwrap();
        let mut stmts = HashMap::new();

        let mut cache = DBCache {
            conn: db,
            stmts: stmts
        };
        Box::new(cache)
    };

    // init the cache pointer
    CACHE.with(|cptr| {
        let ptr:*mut DBCache = &mut *_cache;
        let mut x = cptr.borrow_mut();
        *x = ptr;
    });

    // add first statement
    let cache = cache_unsafe();
    let stmt = cache.conn.prepare("select 12345").unwrap();
    cache.stmts.insert("first".to_owned(), stmt);

    process();

    // since _cache is about to drop, clear the pointer
    CACHE.with(|cptr| {
        let mut x = cptr.borrow_mut();
        *x = std::ptr::null_mut();
    });

    // this will panic if called now, or after this function returns
    //cache_unsafe();
}
1 Like

This is exactly what the unsafe keyword does. Unsafe is there when you don't want the restrictions of proving your code to the compiler. It doesn't do anything more scary or magical than every single line of C code you already write, so if you're confident about writing it in C, you can do the same in unsafe Rust.

5 Likes

I know the original problem was solved without using a global (or rather, local) mutable static, but, if you still may want to use a mutable static, there's the state crate. Maybe that one will fit the bill?

1 Like

HadrienG

    February 14

I have read the thread, but somehow got something differently out of it. What I saw was someone desperately trying to shoehorn an architectural pattern (global state) which is considered archaic and undesirable in any modern coding style, into a problem which didnā€™t really need it, due to what looked like basic unability to fight old bad habits, and then complaining because Rust took extra steps to discourage it. Trying so hard to prove that this is the languageā€™s fault that the argumentation ended up self-defeating.

The bad habit you accuse me of is caching prepared statements in sqlite so that they may be re-used (postgresql provides a similar capability). What do you propose? Recompiling the statement on every use? Or perhaps my bad habit is using relational databases?

jmquigs

    February 19

This topic caught my interest, so I tried to create a safe global cache object and was unsuccessful. However I did manage to create an unsafe implementation (probably not the best, Iā€™m no Rust expert). The DBCache object is created in main(), and a pointer to it is stored in thread local storage. Then a global cache_unsafe() function creates an &mut for the cache on demand. That implementation is below.

This is of course pretty dangerous, so Iā€™d only use it as a last resort myself. Since there are no compile-time guarantees for the global, you have to be careful of re-entrant modifications. For instance: a list of objects where some function is being called on each object, and one of those tries to modify the list. A safer approach might be to return a mutex-like object that panics if some code tries to acquire a reference when one is already outstanding.

IMO, it does seem like a flaw that there is no way to safely describe this.

Yes, this is one of the key issues for me. But after taking a needed break from this, I've come back and re-read this thread and Vitaly, in his usual helpful fashion, suggested a minimally unsafe method that will likely work -- I haven't tested it yet. It's based on lazy_static and creating a new type in which to embed Statements which you unsafely claim implements the Send trait. And while it may be necessary to use the word 'unsafe' to get the compiler to step aside, it can be made thread-safe with a Mutex. While I have no plans for this application to become multi-threaded, I think it makes sense to preserve thread-safety if it's easy. I will have a better grasp of the details after I've tried this. I will report back after I have done so.

There are contexts where you may be in a callback where you donā€™t have a pointer/reference to application state. A static like this would be the only way to retrieve that. The futures lib seems to need this capability as well: https://github.com/rust-lang-nursery/futures-rs/blob/master/src/task_impl/std/mod.rs

> 
> extern crate sqlite;
> use sqlite::{Connection, State, Statement};
> use std::collections::HashMap;
> use std::cell::RefCell;
> struct DBCache<'a> {
> conn: Connection,
> stmts:HashMap<String,
> Statement<'a>>
> }
> thread_local! {
> // refcell that contains a pointer to the actual cache object.
> // only main should use this, everything else should use cache_unsafe()
> static CACHE: RefCell<*mut DBCache<'static>> = RefCell::new(std::ptr::null_
> mut());
> }
> fn cache_unsafe() -> &'static mut DBCache<'static> {
> let mut ptr:*mut DBCache<'static> = std::ptr::null_mut();
> CACHE.with(|cptr| {
> let x = cptr.borrow_mut();
> if *x == std::ptr::null_mut() {
> panic!("null cache pointer!")
> }
> ptr = *x;
> });
> unsafe { &mut *ptr }
> }
> fn process() {
> let cache = cache_unsafe();
> // first query
> let stmtkey = "first".to_owned();
> if cache.stmts.contains_key(&        stmtkey) {
> let stmt = cache.stmts.get_mut(&stmtkey).        unwrap();
> while let State::Row = stmt.next().unwrap() {
> println!("{}", stmt.read::<i64>(0).unwrap());
> }
> }
> // add second, then query
> let stmtkey = "second".to_owned();
> if !cache.stmts.contains_key(&        stmtkey) {
> cache.stmts.insert(stmtkey.to_    owned(), cache.conn.prepare("select 123456").unwrap());
> }
> let stmt = cache.stmts.get_mut(&stmtkey).    unwrap();
> while let State::Row = stmt.next().unwrap() {
> println!("{}", stmt.read::<i64>(0).unwrap());
> }
> }
> pub fn main() {
> let mut _cache = {
> let db = Connection::open(":memory:").        unwrap();
> let mut stmts = HashMap::new();
> let mut cache = DBCache {
> conn: db,
> stmts: stmts
> };
> Box::new(cache)
> };
> // init the cache pointer
> CACHE.with(|cptr| {
> let ptr:*mut DBCache = &mut *_cache;
> let mut x = cptr.borrow_mut();
> *x = ptr;
> });
> // add first statement
> let cache = cache_unsafe();
> let stmt = cache.conn.prepare("select 12345").unwrap();
> cache.stmts.insert("first".to_
>     owned(), stmt);
> process();
> // since _cache is about to drop, clear the pointer
> CACHE.with(|cptr| {
> let mut x = cptr.borrow_mut();
> *x = std::ptr::null_mut();
> });
> // this will panic if called now, or after this function returns
> //cache_unsafe();
> }

You misunderstand my criticism. What I was pointing out here is not your use of prepared statements, but your obsession with passing this data around using implicit global state, instead of using more modern explicit state-passing mechanisms.

There are just too many problems with code that use implicit state, without even getting into threading matters. Just some examples:

  • Information flow throughout the program becomes unnecessarily obscure (as it is not clearly spelled out in function signatures anymore).
  • There is no referential transparency. If there is a bug in the cache, calling the same function twice with the same parameters can lead very different results.
  • As a direct consequence, code which relies on implicit state is a nightmare to test.
  • Global state-based code is deeply intertwined into the host application and nearly impossible to extract into a dedicated library. So if you need to use the same caching pattern later your only option is copy and paste.
  • There is no locality. Any function can affect the behaviour of any other part of the program.
  • There is no escape. If sqlite optimizes its query engine in a later version, to the point where your prepared statement cache becomes unnecessary and just slows thing down, removing that cache from the program will be a more complex refactoring.

This is why usage of global state is nowadays a deprecated practice. It is one of these ideas that sound reasonable when writing 100-lines scripts, but make larger programs unmaintainable.

Be smarter than the inventors of errno. Be nice to people who will read and use your code later on, including your future self. Say no to implicit state.

1 Like

I understand all of that and disagree with none of it. The problem is that your well-intentioned theory is difficult to put into practice in Rust in this case. I would prefer to do it as I would in Haskell, which is what you are advocating, by creating a type that holds the connection and Option<Statement>s and passing an instance of that type to the routines that need them and have them record their prepared statements in it, rather than using global statics. But, as has been discussed in this and other threads on this subject, those attempts have always ended up in lifetime hell. So at this point, I'm simply trying to find something that works -- deprecated or not.

1 Like

Can you point me towards your latest attempt in this direction? I would like to start from there, but the last time I had a look at your code, you were already very keen on using statics/thread-locals...

EDIT: To clarify, I suspect that last time, you were stopped by the Rust borrow checker's current inability to understand self-borrowing types, which is being worked on, but has known workarounds meanwhile (the rental crate, Rc/RefCell...).

Right now, I am busy with some other things. But when I am able, I will extract an example from my Haskell-like attempt.

1 Like

I was able to spend some time on this yesterday and will describe that work here.

First I should clarify one point. In my actual application (a personal financial management application written in C, using gtk3 and sqlite3, and which I would like to port to a more modern language), there are many sql queries. In each invocation of the program, depending on what you do, only a fraction of those queries get prepared and used. Preparing all of them in advance to store in a struct is not an acceptable solution, though I think that would likely address the problems I've observed in trying to do this in Rust.

I should also acknowledge the post by gwenn on 2/15, which points out that rusqlite is built on top of sqlite's tcl api, which caches prepared statements for you. Yes, I could make this problem go away by taking that approach, but at this point, it's a language issue for me. What I am trying to do is standard operating procedure with database systems and is easy to do in C and any of the other languages I would consider using for this, such as Haskell, Nim or Go. While I admit to not being a world-class Rust programmer, I'm a pretty skilled, deeply experienced programming, foregoing any undue modesty. The question that is troubling me is that if this is this difficult in Rust, what other traps might lurk? So I would like to pursue this until a solution is found, so I can distinguish between my lack of Rust experience vs inherent problems in the language.

Ok, here's a little example program written in Haskell that exercises statement caching in the way HadrienG and I discussed a few days ago:

module Main (main) where

import Database.SQLite3
import Data.IORef
import Text.Printf

data Globals = NewGlobals {db :: Database, statement :: IORef (Maybe Statement)}

test :: Globals -> IO ()
test globals = let testHelper :: Statement -> IO ()
                   testHelper statement = 
                        do result <- step statement
                           case result of
                                Done -> return ()
                                Row -> do first <- columnInt64 statement 0
                                          second <- columnInt64 statement 1
                                          third <- columnInt64 statement 2
                                          printf "%d, %d, %d\n" first second third
                                          testHelper statement
               in do maybeStatement <- readIORef (statement globals)
                     statement <- case maybeStatement of
                                    Nothing -> do s <- prepare (db globals) "select 1,2,3"
                                                  writeIORef (statement globals) (Just s)
                                                  return s
                                    Just s -> return s
                     testHelper statement

main = do db <- open ":memory:"
          statement <- newIORef Nothing
          let globals = NewGlobals{db=db, statement=statement}
          test globals
          test globals

And in Nim:

import sqlite3 as s
import strutils
import os

type MaybeStatement = tuple [valid:bool, statement:Pstmt]
type Globals = tuple [db:PSqlite3, maybeStatement:MaybeStatement]

proc openDB():PSqlite3 =
    if s.open(":memory:", result) != SQLITE_OK:
        echo "Failed to open database"
        quit(QuitFailure)
    
proc prepareStmt(db:s.PSqlite3, sql:string):Pstmt =
    if s.prepare_v2(db, sql, -1, result, nil) != SQLITE_OK:
        echo("prepareStmt failed")
        quit(QuitFailure)

proc test(globals:var Globals) = 
    if not globals.maybeStatement.valid:
        globals = (globals.db, (true, prepareStmt(globals.db, "select 1,2,3")))
    while s.step(globals.maybeStatement.statement) == SQLITE_ROW:
        echo "$1, $2, $3" % [$column_int64(globals.maybeStatement.statement, 0),
                                $column_int64(globals.maybeStatement.statement, 1),
                                $column_int64(globals.maybeStatement.statement, 2)]
# Open the database
var globals:Globals = (openDB(), (false, nil))
test(globals)
test(globals)

Both of the above compile without error and run correctly.

Now to Rust. I naturally wrote my first attempt in the Rust equivalent of the above, meaning without lifetime annotations. As expected, that did not please the compiler. So I followed the suggestions in the error messages, and after a number of iterations, arrived at this:

extern crate sqlite;

use sqlite::{Connection, Statement, State};
use std::process;

pub struct Globals<'l> {
    connection:Connection,
    statement:Option<Statement<'l>>,
}

fn test<'l>(globals:&'l mut Globals<'l>) {
    match globals.statement {
        None => {
                    let statement = globals.connection.prepare("select 1,2,3").unwrap();
                    globals.statement = Some(statement);
                },
        Some(_) => (),
    };
    let statement = match globals.statement {
                        Some(ref mut s) => s,
                        None => panic!("Impossible!"),
                    };

    while let State::Row = statement.next().unwrap() {
        let first:i64 = statement.read(0).unwrap();
        let second:i64 = statement.read(1).unwrap();
        let third:i64 = statement.read(2).unwrap();
        println!("{}, {}, {}", first, second, third);
    }
}

fn main() {
    let globals = Globals {
        connection:sqlite::open(":memory:").unwrap(),
        statement:None,
    };

    test(&mut globals);
    test(&mut globals);
    process::exit(0);
}

Compiling this produces:

Compiling cached_statements_example v0.1.0 (file:///home/dca/Software/cached_statements_example_rust)
error[E0597]: `globals` does not live long enough
  --> src/main.rs:38:15
   |
38 |     test(&mut globals);
   |               ^^^^^^^ borrowed value does not live long enough
...
41 | }
   | - `globals` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

error[E0597]: `globals` does not live long enough
  --> src/main.rs:39:15
   |
39 |     test(&mut globals);
   |               ^^^^^^^ borrowed value does not live long enough
40 |     process::exit(0);
41 | }
   | - `globals` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

error: aborting due to 2 previous errors

error: Could not compile `cached_statements_example`.

To learn more, run the command again with --verbose.

I think the issue is that the sqlite crate's prepare method takes a reference to the db connection (&self) and returns the statement. It requires that both the connection reference and the returned statement have the same lifetime. I see two possible issues with this, if my understanding of lifetime annotation is correct (unlikely!). One is that I think the authors of the sqlite crate really mean that the statement should live as long as the connection, not the reference to it. The other is that it seems to me that the constraint should be that the statement not live longer than the connection, which would be analogous to a dangling reference. But I could easily be completely wrong here, as my understanding of lifetime annotation, despite a lot of reading of Klabnik and Blandy, is shaky.

I have made some attempts to fix this, none of them successful. So if someone has a solution (HadrienG?), I'd be very interested in seeing it.

This issue is exactly what led me to try to solve this with statics (not necessarily global in scope; in the C version, in virtually every case, the cached statements are used by only one function and are therefore stored in statics whose scope is local to that function). I do think there is a solution along these lines, which Vataly was pointing me towards, but I'd be very interested to see if there is a way to do this in Rust using the Haskell approach.

2 Likes

@donallen, I see you're still plugging away on this - that's good to see! :slight_smile:

The problem you have is you're creating a self-referential struct, which is not allowed in Rust (and there's no easy way that you'll like to make it happen).

I took your code and here's how I'd structure it (very stripped down, but hopefully enough to get the idea):

extern crate sqlite;

use sqlite::State;

pub struct GlobalConn(sqlite::Connection);

impl std::ops::Deref for GlobalConn {
    type Target = sqlite::Connection;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub struct CachedStmt<'c> {
    conn: &'c GlobalConn,
    stmt: Option<sqlite::Statement<'c>>,
}

impl<'c> CachedStmt<'c> {
    pub fn get_or_prepare<S: AsRef<str>>(&mut self, sql: S) -> &mut sqlite::Statement<'c> {
        match self.stmt {
            Some(ref mut s) => s,
            None => {
                self.stmt = self.conn.prepare(sql.as_ref()).unwrap().into();
                self.stmt.as_mut().unwrap()
            }
        }
    }
}

fn test(cached_stmt: &mut CachedStmt) {
    let statement = cached_stmt.get_or_prepare("select 1,2,3");
    while let State::Row = statement.next().unwrap() {
        let first: i64 = statement.read(0).unwrap();
        let second: i64 = statement.read(1).unwrap();
        let third: i64 = statement.read(2).unwrap();
        println!("{}, {}, {}", first, second, third);
    }
}

fn main() {
    // global connection - needs to be kept alive for the duration of your program, or
    // until you have attached Statements live
    let global_conn = GlobalConn(sqlite::open(":memory:").unwrap());
    // CachedStmt will reference the global connection, and can lazily create the statement
    let mut stmt = CachedStmt { conn: &global_conn, stmt: None };
    
    test(&mut stmt);
    test(&mut stmt);
}

If you have questions on this, let me know.

2 Likes

Yes, I have a question, 9 minutes after your post :slight_smile: Why is my structure self-referential? Obviously the issue is the attempt to store the statement in the globals struct, but what I am not understanding is your use of the term 'self-referential'. Yes, something from the struct (globals.connection) is use to generate the statement. But 'prepare' returns a statement that I then try to store in the struct. That statement doesn't refer to the struct. Perhaps the self-referentiality is in the relationships of the lifetimes?

I will take a careful look at your code. Thanks for doing this.

1 Like

In your attempt, Globals owns the Connection. It also owns the Statement. The Statement, however, has a reference back to the Connection it came out of. Note that "has a reference" doesn't necessarily mean there's a physical & or &mut - it's a conceptual reference, which can be expressed via lifetimes. And in fact if you look at the definition of Statement, we'll see:

/// A prepared statement.
pub struct Statement<'l> {
    raw: (*mut ffi::sqlite3_stmt, *mut ffi::sqlite3),
    phantom: PhantomData<(ffi::sqlite3_stmt, &'l ffi::sqlite3)>,
}

The "reference" relationship is expressed with a PhantomData - there's no actual reference (it's raw ptrs).

So, this ends up being a self-referential struct. It's one "big blob" with (semantic) references between the parts.

2 Likes

So having looked at your code, the key to breaking the self referential path is to separate the ownership of the connection and the statement. Using this insight, a minimal change to my code would be:

extern crate sqlite;

use sqlite::{Connection, Statement, State};
use std::process;

pub struct Globals<'l> {
    connection:&'l Connection,
    statement:Option<Statement<'l>>,
}

fn test(globals:&mut Globals) {
    match globals.statement {
        None => {
                    let statement = globals.connection.prepare("select 1,2,3").unwrap();
                    globals.statement = Some(statement);
                },
        Some(_) => (),
    };
    let statement = match globals.statement {
                        Some(ref mut s) => s,
                        None => panic!("Impossible!"),
                    };

    while let State::Row = statement.next().unwrap() {
        let first:i64 = statement.read(0).unwrap();
        let second:i64 = statement.read(1).unwrap();
        let third:i64 = statement.read(2).unwrap();
        println!("{}, {}, {}", first, second, third);
    }
}

fn main() {
    let db = sqlite::open(":memory:").unwrap();
    let mut globals = Globals {
        connection: &db,
        statement:None,
    };

    test(&mut globals);
    test(&mut globals);
    process::exit(0);
}

This does work, as does your version.

I think the difficulty of finding a solution (including understanding obscure inner workings of what is supposed to be a Black Box -- the sqlite crate) compared to the ease of writing this in other languages brings me back to my early points about the cost-benefit proposition Rust presents. Providing memory safety without a GC is a hard problem and I think in many ways Rust is a brilliant solution. But sometimes that solution is in search of a problem, as the Rust project itself acknowledges by describing it as a "systems" language. GC-less memory safety imposes a considerable cost on the user and that cost must be justified by the benefit. In the case of my personal finance suite, it is clear that I could write whatever I'm considering porting to Rust in Haskell, Nim, or Go far more easily and the performance of the result would be fine.

But sounding like an economist: on the other hand, I do like the rigor that Rust imposes, with the resulting knowledge that once you've gotten your code past the compiler, there is a high likelihood that any bugs are logical or algorithmic, not instability due to low-level oversights. This is mostly true of Haskell, but even Haskell doesn't provide quite the level of rigor that Rust does (if you use IORefs in multi-threaded code, are they properly interlocked?). And Haskell has other issues (most real-world Haskell code is largely imperative and its monad-based imperative language isn't great).

So, armed with my cached statements, I'll give this some thought and perhaps continue with this.

And again, thanks for the help. I hope others will benefit from this discussion.

/Don

5 Likes

That's right. The pit you fell into is trying to have a lazily-created one, and storing both in the same "bag of state" struct. You can easily imagine a bunch of different structs that want to encapsulate some particular prepared statement - they'll all have a reference back to the Connection, but otherwise live independent of each other. The compiler ensures that you don't end up with a statement referencing a connection that's been closed. In turn, however, you need to have more rigor (as you said later in your post) around the structure of the code and play by the compiler's rules.

That's correct. Rust being GC-free and having top-notch performance is awesome. But there's more to this pony than that one trick. I personally really like the rigor, discipline, pickiness, and overall focus on correctness. I'd rather explain what I'm doing to the compiler, and have it verify me 100% of the time than have to document code and then spend time explaining why things work to myself and others. Do I particularly care for it for some quick'n dirty piece of code? No. But I'm not going to write that code in Rust in the first place.

A lot of people take runtime errors in other languages as a given, and don't really complain about the language allowing it in the first place - it's just a tax they pay without really questioning it. One has to truly appreciate the rigor and paranoia the compiler approaches your code with. It truly is a front-loaded language - you will spend more (a lot more if just starting out with it) time getting the code to compile than some other languages. But you will spend less time troubleshooting runtime errors, performance issues, concurrency-induced nightmares, and so on.

It's all about tradeoffs and what you value ... like all things in life! :slight_smile:

Good luck with your project.

2 Likes

@donallen: So, it looks like @vitalyd, being his usual awesome self, has already guided you through most of the underlying self-referentiality issue before I got the chance to read this topic's updates. As a result, all I will be able to propose is some extra discussions on top of what he already said.


Is it clear to you, at this point in time, why self-referential structs are probablematic in current Rust, and why allowing them would require some changes to Rust's ownership and borrowing design? If not, this is something we can discuss more. I don't know about you, but personally I like to know why these kinds of limitations exist as opposed to just suffering them.


Another topic which we may discuss more if you like: how the "half-full glass" sides of Rust's ownership and borrowing model extend beyond GC-less memory management and an excellent multi-threading story. For example, it seems to me that taking great steps to avoid shared mutability, while still allowing for both sharing and mutability in isolation, is what allows Rust to provide one of the best middle grounds available today between the functional and imperative schools of thoughts. Whereas the notion of ownership also has some very interesting applications in API design, from modeling the notion of consuming data (e.g. Iterator::count(), sending an object into an mpsc channel...) to a more correct handling of data transformations (e.g. state machine transitions, Builder pattern...).

2 Likes