Skip to content

Instantly share code, notes, and snippets.

@graydon
Created January 18, 2024 23:07
Show Gist options
  • Save graydon/42778d8efdf1d3f1ae405e37086c4ef8 to your computer and use it in GitHub Desktop.
Save graydon/42778d8efdf1d3f1ae405e37086c4ef8 to your computer and use it in GitHub Desktop.
Ownership passing vs. borrowing
// This is just an elaboration of an off-hand toot I made earlier today
// concerning a coding pattern I find myself doing whenever possible: passing
// and returning owned values instead of borrowing references.
//
// I find it works well for long-lived values especially since the resulting
// composite objects have no lifetime qualifiers, so I don't have to plumb
// lifetimes through to all the code that uses them.
// Assumption: assume we have a few objects like this: a network connection, a
// database, some commands, etc. and we want to make a session type that uses
// both the network and the database...
use std::{collections::HashMap, net::TcpStream, io::Read, env};
enum Command {
Get { key: String },
Set { key: String, value: String },
}
#[derive(Default)]
struct Database {
data: HashMap<String,String>,
}
struct Network {
stream: TcpStream,
}
impl Network {
fn new(stream: TcpStream) -> Self {
Self { stream }
}
fn next_command(&mut self) -> Option<Command> {
let mut buffer = [b' '; 64];
if self.stream.read(&mut buffer).is_err() {
return None;
}
let Ok(buffer) = std::str::from_utf8(&buffer) else { return None };
let parts: Vec<&str> = buffer.trim().splitn(3, ' ').collect();
match &parts[..] {
["GET", k] => {
Some(Command::Get { key: k.to_string() })
}
["SET", k, v] => {
Some(Command::Set { key: k.to_string(), value: v.to_string() })
}
_ => None
}
}
}
// Now we compare two approaches to composing these objects into a session.
// Approach 1: use borrows for composition
mod borrow {
use std::io::Write;
use super::*;
pub struct Session<'db, 'net> {
database: &'db mut Database,
network: &'net mut Network,
}
impl<'db, 'net> Session<'db, 'net> {
pub fn new(database: &'db mut Database, network: &'net mut Network) -> Self {
Self { database, network }
}
pub fn process_commands(&mut self) {
loop {
match self.network.next_command() {
Some(Command::Get { key }) => {
if let Some(value) = self.database.data.get(&key) {
self.network.stream.write_all(value.as_bytes()).unwrap();
} else {
self.network.stream.write_all(b"NOT_FOUND").unwrap();
}
}
Some(Command::Set { key, value }) => {
self.database.data.insert(key, value);
self.network.stream.write_all(b"OK").unwrap();
}
None => return,
}
}
}
}
}
// Approach 2: use owned values for composition
mod owned {
use std::io::Write;
use super::*;
pub struct Session {
database: Database,
network: Network,
}
impl Session {
pub fn new(database: Database, network: Network) -> Self {
Self { database, network }
}
pub fn process_commands(&mut self) {
loop {
match self.network.next_command() {
Some(Command::Get { key }) => {
if let Some(value) = self.database.data.get(&key) {
self.network.stream.write_all(value.as_bytes()).unwrap();
} else {
self.network.stream.write_all(b"NOT_FOUND").unwrap();
}
}
Some(Command::Set { key, value }) => {
self.database.data.insert(key, value);
self.network.stream.write_all(b"OK").unwrap();
}
None => return,
}
}
}
pub fn finish(self) -> (Database, Network) {
(self.database, self.network)
}
}
}
// The two approaches are nearly identical in method bodies. They both _have
// exclusive ownership_ of the resources they're using, but one has taken that
// ownership in the form of a somewhat pointlessly noisy `&mut` along with
// lifetime qualifiers on the struct, its fields and impl, and the other has
// taken ownership in the form of a value moved-in that must later be moved-out
// to recover it. Given the option, I find I often prefer writing the one
// additional `finish` function to recover the values I want to move-out, and
// have no `&mut` in sight.
fn main() {
let mut database = Database::default();
let mut network = Network::new(TcpStream::connect("localhost:8080").unwrap());
if env::args().any(|a| a == "--borrowed") {
let mut session = borrow::Session::new(&mut database, &mut network);
session.process_commands();
} else {
let mut session = owned::Session::new(database, network);
session.process_commands();
let (database, network) = session.finish();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment