Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rust structs with nullable Option fields

In python, I might have a class that looks like this:

class ClientObject(object):

    def __init__(self):
        connection = None
        connected = False

    def connect(self):
        self.connection = new Connection('server')
        self.connected = True

    def disconnect(self):
        self.connection.close()
        self.connection = None
        self.connected = False

I am trying to do something similar in rust. First off, I question if this is even a good pattern in rust--would you implement a client class with a connection this way? Second, I am getting an error I don't understand with my implementation.

pub struct Client {
    seq: int,
    connected: bool,
    socket: Option<UdpSocket>
}

impl Client {

    pub fn connect(&mut self, addr: &SocketAddr) -> ClientConnectionResult {
        match self.socket {
            Some(_) => self.disconnect(),
            None => ()
        };

        self.socket = match UdpSocket::bind(*addr) {
            Ok(s) => Some(s),
            Err(e) => return Err(to_client_error(e))
        };

        self.connected = true;

        Ok(())
    }

    pub fn disconnect(&mut self) {
        match self.socket {
            None => (),
            Some(s) => drop(s)
        };

        self.socket = None;

        self.connected = false;
    }
}

In the disconnect function, the match generates a compile error because it attempts to move the ownership of self.socket. What I want to do is to set self.socket to None and allow it to be later reassigned to something if connect is called. How would I do that?

like image 519
Harley Holt Avatar asked Aug 13 '14 04:08

Harley Holt


2 Answers

It may be not applicable for your use case, but Rust move semantics and strong typing allow cheap "state machines", when a method consumes self (current state) and returns another object which represents another state. That's how TcpListener is implemented: it has listen() method which returns TcpAcceptor, consuming original listener in process. This approach has advantage of typing: you just cannot call methods which does not make sense when the object is in invalid state.

In your case it could look like this:

use std::kinds::markers::NoCopy;

pub struct DisconnectedClient {
    seq: int,
    _no_copy: NoCopy
}

impl DisconnectedClient {
    #[inline]
    pub fn new(seq: int) -> DisconnectedClient {
        DisconnectedClient { seq: seq, _no_copy: NoCopy }
    }

    // DisconnectedClient does not implement Copy due to NoCopy marker so you need 
    // to return it back in case of error, together with that error, otherwise 
    // it is consumed and can't be used again.
    pub fn connect(self, addr: &SocketAddr) -> Result<ConnectedClient, (DisconnectedClient, IoError)> {
        match UdpSocket::bind(*addr) {
            Ok(s) => Ok(ConnectedClient { seq: self.seq, socket: s }),
            Err(e) => Err((self, e))
        }
    }
}

pub struct ConnectedClient {
    seq: int,
    socket: UdpSocket
}

impl ConnectedClient {
    #[inline]
    pub fn disconnect(self) -> DisconnectedClient {
        // self.socket will be dropped here
        DisconnectedClient::new(self.seq)
    }

    // all operations on active connection are defined here
}

You first create DisconnectedClient using DisconnectedClient::new() method. Then, when you want to connect to something, you use connect() method, which consumes DisconnectedClient and returns new object, ConnectedClient, which represents an established connection. When you finished working with this connection, disconnect() method turns ConnectedClient back to DisconnectedClient.

This approach may be more complex, but it has advantage of static checks for incorrect state. You don't have to have connected()-like methods; if your variable is of type ConnectedClient, then you already know it is connected.

like image 77
Vladimir Matveev Avatar answered Nov 14 '22 10:11

Vladimir Matveev


If in your Python code self.connected is equivalent to self.connection is not None, then the boolean is a complete waste of time and space; you’d be best omitting it or if it is valuable having it as a property:

@property
def connected(self):
    return self.connection is not None

Similarly in Rust you can go storing the boolean separately, but there is absolutely no point.

impl Client {
    pub fn connect(&mut self, addr: &SocketAddr) -> ClientConnectionResult {
        self.disconnect();
        self.socket = Some(try!(UdpSocket::bind(*addr)));
        Ok(())
    }

    pub fn disconnect(&mut self) {
        self.socket = None;
    }
}

As far as the moving self.socket part is concerned, you can’t really do it the way you’ve done it there, because it is moving it. You must put something in its place. This can be achieved with something like .take().

drop is unnecessary; destructors are called when the value goes out of scope or is otherwise replaced, which self.socket = None will cause to happen. Similarly, because disconnect() works fine if socket is already None there is no need for an if self.socket.is_some() check (or equivalent pattern matching) around the call of self.disconnect() in connect.

If you want someone to be able to check whether it’s connected, make a simple method for it:

    pub fn connected(&self) -> bool {
        self.socket.is_some()
    }
like image 24
Chris Morgan Avatar answered Nov 14 '22 09:11

Chris Morgan