1. 25
  1.  

  2. 3

    Interesting! Now, can this all be moved to compile-time in case of all parameters being available at compile-time?

    1. 12

      Yes. While a bit messy to read if you are not used to telling the Rust debug machinery from the rest, here’s an example: https://godbolt.org/g/uSuu59

      Basically, this:

      fn main() {
          let call = OutboundCallBuilder::new("tom", "jerry", "http://www.example.com")
          .with_fallback_url("http://fallback.com")
          .build();
      
         println("{:?}", call);
      }
      

      ends up as printing 4 static strings through the debug machinery. Note that you obviously need to compile with optimizations (rustc -O or cargo build --release) for that.

    2. 3

      I prefer keeping it simple and general by using self rather than &mut self. If you make the builder’s members public (which doesn’t mean the final struct’s members need to be public), then you can handle more complex cases with direct member access:

      let mut builder = OutboundCallBuilder::new("tom", "jerry", "http://www.example.com");
      if (need_fallback) {
          builder.fallback_url = Some("http://www.fallback.com");
      }
      let call = builder.build();
      
      1. 1

        This makes me appreciate zero values in Go. Instead of having to write a builder, I’d just declare a variable and set fields on it. I realize Rust insists on explicit initialization, but you could at least approximate that here: just write a function that returns a struct and set fields on that. What’s the advantage of the with_whatever methods?

        1. 14

          If this is your use case, Rust has a protocol around the Default trait and field initialization syntax.

          #[derive(Default)]
          struct Foo {
            field1: u32,
            field2: u32
          }
          
          fn main() {
            let foo = Foo {
              field1: 1,
              .. Foo::default()
            };
          }
          

          Alternatively, you can implement Default on that.

          impl Default for Foo {
            fn default() -> Foo {
              Foo { field1: 3, field2: 2 }
            }
          }
          

          The advantage of Builders is that they are lazy and can be passed around. So, for example, I can have a library that pre-builds requests in a certain fashion and then hand them off to a user defined function that sets additional data.

          1. 1

            I don’t get it. I can pass around an object and set fields on it too, what’s the advantage?

            1. 10

              Struct fields in Rust are private by default, so having it leave scope might lead to people not being allowed to assign those fields.

              Additionally, if forgot this: Rust is a generic language and patterns like the following aren’t uncommon.

              fn with_path<P: AsRef<Path>>(&mut self, pathlike: P) {
               ....
              }
              

              Which means that the method takes anything that can be turned into a (filesystem)-Path. A string, a path, an extensible pathbuffer, etc.

              It enables you strictly more things to do. Yes, at the cost of some verbosity, which you can avoid in simple cases.

              1. 5

                A TBuilder doesn’t type check as a valid T object. This is the real value of the builder pattern (in Rust and Go and Java and…Haskell): one can write a fairly strict definition for T, and wherever one has a function that accepts a T, one can be sure that the T is fully constructed and valid to use. The TBuilder is there for your CLI parser, web API, or chatbot to use, while stitching together a full object from defaults+input, or some other combination of sources.

                Distinguishing between T and TBuilder prevents a partial object from masquerading as a full object.

            2. 7

              This is an orthogonal thing for the most part. For example, sometimes I use builders in Go when the initialization logic is more complicated.