Skip to content

Instantly share code, notes, and snippets.

@cb372
Created June 5, 2017 21:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cb372/50808adfc8bbdb2e42225f8ef4a48daa to your computer and use it in GitHub Desktop.
Save cb372/50808adfc8bbdb2e42225f8ef4a48daa to your computer and use it in GitHub Desktop.
Random thoughts on writing my first Rust app

Preamble

  • Background: I've been writing mostly Scala for the last few years, mostly in FP style. I also have experience writing Java, Ruby, Python, bash and a few other languages.

  • Caveat: anything below which sounds like a grumble about the language is probably due to ignorance on my part. Please advise/correct me in the comments.

  • For a more eloquent writeup of a similar topic by somebody who can actually write Rust, see Lloyd's excellent blogpost

Amble

  • Tools like rustup and cargo are a breath of fresh air after working with sbt for so long!

  • clap is one of the best command line parsing libraries I've used in any language.

  • I got tripped up by the borrowing semantics a couple of times but I managed to clone() my way out of trouble. I understand that this is cheating. Need to learn more about borrowing and lifecycles.

  • Structs needing to be a fixed size was confusing at first, but you just need to stick dynamic-sized things in boxes to get around it. I wonder why it's ok for the final field to have a dynamic size?

  • Semicolons are annoying. The semantics of a function change depending on the absence or presence of a semicolon on the final line. Include a semicolon -> return (). Luckily the compiler gives you a useful warning or error.

  • If you take the time to read the compiler errors properly, they are really helpful.

  • Getting compiler warnings for code style (e.g. using camel case instead of snake case) and dead code is quite nice.

  • The ? syntax for chaining Results monadically is nice. But I want to learn more about how it's implemented. I hope it's not hardcoded to work with only the Result type. Also having to wrap the final result you want to yield in Ok() is a bit clunky. I wonder if you could build a Haskell/Scala-style for comprehension with a macro?

  • The automagic conversion using the From trait is a bit troubling. Feels quite magical, and works inconsistently.

    e.g. if I write

    struct AWSError {
        message: String
    }
    
    impl <A: Error> From<A> for AWSError {
        fn from(err: A) -> AWSError { 
            let message = String::from(err.description());
            AWSError { 
                message
            }
        }
    }
    
    fn does_table_exist(&self) -> Result<bool, AWSError> {
        ...
        let output = self.dynamo_client.describe_table(&describe_table_input)?; // returns Result<..., DescribeTableError>
        Ok(output.table.is_some())
    }
    

    then the implicit conversion from DescribeTableError to AWSError works fine.

    But if I write

    fn does_table_exist(&self) -> Result<bool, AWSError> {
        ...
        self.dynamo_client
            .describe_table(&describe_table_input)
            .map(|output| output.table.is_some())
    }
    

    then the implicit conversion doesn't kick in and it fails to compile.

  • When chaining operations that return different error types, what should my return type be?

        fn new(profile: Option<String>, region: String, table_name: String) -> Result<AWS, ???> {
        let provider = AWS::build_creds_provider(profile)?; // returns Result<...,  CredentialsError>
        let reg = Region::from_str(region.as_str())?;       // returns Result<..., ParseRegionError>
        let tls_client = default_tls_client()?;             // returns Result<..., TlsError>
        ...
    }
    

    The error types don't match, but they do all implement the Error trait. I made my own error struct AWSError and defined a conversion from A: Error to AWSError. It works, but it feels like there must be an easier way.

  • Why doesn't Option have a foreach method for performing a side-effect? Apparently this is the most idiomatic way to write it:

    if let Some(p) = profile {
        profile_provider.set_profile(p);
    }
    
  • The naming conventions in Rust are a bit different from functional languages, so I have to keep looking up method names. e.g. Option has a map method called map but a flatmap/bind method called and_then.

  • Why can't child modules hide anything from their parent? I want a private modifier.

  • I understand why there are two ways of representing a string, but it's still annoying. It seems like whenever I have a string and I need to pass it to a function, it's the wrong type and I have to convert it.

  • After a few evenings of writing Rust, I've got past the frustrating "I know what I want to write but I can't get the syntax right" stage, and I'm starting to be productive. The next step will be to take the code I've written so far, which I know is ugly and verbose, and start looking at ways to refactor it.

  • I haven't written any tests yet. I don't think you can claim to be proficient at a language until you can write tests in it.

  • I've had a quick look at setting up Travis CI to build on multiple platforms. Looks like some kind soul has already done all the hard work and made a template to follow.

@lloydmeta
Copy link

lloydmeta commented Jun 6, 2017

Nice ! You covered a bunch of stuff I forgot to mention in my blogpost :)

Re: ?, my understanding is that it may have been hardcoded, but in the future, will be available for anything that implements the Try trait, since the extend ? to work over other types PR has been merged. See this RFC for more details.

Just copying from the RFC:

  • The desugaring of ? is
match Try::into_result(expr) {
    Ok(v) => v,

    // here, the `return` presumes that there is
    // no `catch` in scope:
    Err(e) => return Try::from_error(From::from(e)),
}
  • Impl for Result is
impl<T,E> Try for Result<T, E> {
    type Ok = T;
    type Error = E;

    fn into_result(self) -> Self {
        self
    }
    
    fn from_ok(v: T) -> Self {
        Ok(v)
    }

    fn from_error(v: E) -> Self {
        Err(v)
    }
}
  • Impl for Option is
mod option {
    pub struct Missing;

    impl<T> Try for Option<T>  {
        type Ok = T;
        type Error = Missing;

        fn into_result(self) -> Result<T, Missing> {
            self.ok_or(Missing)
        }
    
        fn from_ok(v: T) -> Self {
            Some(v)
        }

        fn from_error(_: Missing) -> Self {
            None
        }
    }
} 

I've seen one or two crates out there that port do/for too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment