Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用类型来表达约束 #66

Open
BruceChen7 opened this issue May 8, 2023 · 0 comments
Open

使用类型来表达约束 #66

BruceChen7 opened this issue May 8, 2023 · 0 comments

Comments

@BruceChen7
Copy link
Owner

BruceChen7 commented May 8, 2023

参考资料

原因

  • 更好的的进行领域驱动设计,使用类型来明确表达需求
  • 让 API 更好用,在编译的时候,就能发现运行时一些的错误调用
  • 因为用代码表达需求,能让代码可读性更强

例子

  • 在写一个 HTTP server,返回响应时,包含如下几个部分:
    • 恰好一个状态行
    • 0 个或更多的 header
    • 返回或不返回的 body

简单的尝试

fn a_simple_response(r: HttpResponse) {
    r.status_line(200, "OK")
      .header("X-Unexpected", "Spanish-Inquisition")
      .header("Content-Length", "6")
      .body("Hello!")
}

几个错误的返回:

fn broken_response_1(r: HttpResponse) {
    r.header("X-Unexpected", "Spanish-Inquisition")
      // error: forgot to call status_line
}

fn broken_response_2(r: HttpResponse) {
    r.status_line(200, "OK")
      .body("Hello!")
      .header("X-Unexpected", "Spanish-Inquisition")
        // error: tried to send a header after body
}
  • 这些判定必须在运行时的判定
  • 能通过类型系统的设计,来帮助解决这类问题,强化一些设计,表达真正的需求

第一次尝试

struct HttpResponse { ... }
struct HttpResponseAfterStatus { ... }

impl HttpResponse {
    fn status_line(self, code: u8, message: &str)
        -> HttpResponseAfterStatus
    {
        // ...body omitted
    }
}

impl HttpResponseAfterStatus {
    fn header(self, key: &str, value: &str) -> Self {
        // ...body omitted
    }

    fn body(self, text: &str) {
        // ...body omitted
    }
}
  • 每个操作都会消耗自身并产生处于某种状态的新对象,或者(在 body 的情况下)不产生任何东西,结束该过程。
  • 不能在状态行之前发送标头,因为该操作根本没有定义
  • 不能在 header之后发送状态行,因为同样地,在类型上也没有定义它。
  • 无法在发送 body 后执行任何操作,因为对象已经被吞掉了

第二次尝试

  • 第一次尝试问题是是什么?
    • self 在方法里面被吞掉,如果 struct 很大,那么开销会比较大,
    • 上面的设计的问题,意味着如果有多个 header 要发送,必须这样写:
    • 也就是需要 r = r.header(h.key, h.value),这种写法比较丑
      struct HttpResponse(Box<ActualResponseState>);
      struct HttpResponseAfterStatus(Box<ActualResponseState>);
      
      struct ActualResponseState { ... }
      fn many_headers(r: HttpResponse, headers: Vec<Header>) {
          let mut r = r.status_line(200, "OK");
          for h in headers {
              // Having to do this is kind of annoying:
              r = r.header(h.key, h.value);
              // Fortunately, if you forget the `r =` part,
              // the compile will fail.
          }
          r.body("hello!")
      }
  • 改变函数签名
    // Let's try this again:
    impl HttpResponseAfterStatus {
        fn header(&mut self, key: &str, value: &str) {
            // ...
        }
    }
    
    fn many_headers(r: HttpResponse, headers: Vec<Header>) {
        let mut r = r.status_line(200, "OK");
        for h in headers {
            // Ahhhh, much better.
            r.header(h.key, h.value);
        }
        r.body("hello!")
    }
  • 考虑返回 response reference to self,来让用户自己选择是否采用链式的写法

第三次尝试

  • 将状态建模为单个通用结构的类型参数,而不是为每个状态使用单独的结构。与完全独立的类型相比,这通常更少样板化并且更强大

  • 缺点代码更难读

    // S is the state parameter. We require it to impl
    // our ResponseState trait (below) to prevent users
    // from trying weird types like HttpResponse<u8>.
    struct HttpResponse<S: ResponseState> {
        // This is the same field as in the previous example.
        state: Box<ActualResponseState>,
        // This reassures the compiler that the parameter
        // gets used.
        marker: std::marker::PhantomData<S>,
    }
    
    // State type options.
    enum Start {} // expecting status line
    enum Headers {} // expecting headers or body
    
    trait ResponseState {}
    impl ResponseState for Start {}
    impl ResponseState for Headers {}
  • 希望 HttpResponse<S> 类型仅与 S 的两个值一起使用: HttpResponse<Start> 和 HttpResponse<Headers> 。

  • 从技术上讲,如所写,用户能为自定义类型实现 ResponseState 并尝试将其与 HttpResponse 一起使用。

  • 感到困扰,能使用**【sealed trait pattern】**来修复它。

  • State 和 Headers 是仅作为【类型而不作为值存在的类】,使用zero-sized 枚举模式来确保这一点。

  • 像这样的类型被广泛称为【幻影类型】,这也是 std::marker::PhantomData 得名的地方。

    /// Operations that are valid only in Start state.
    impl HttpResponse<Start> {
        fn new() -> Self {
            // ...
        }
    
        fn status_line(self, code: u8, message: &str)
            -> HttpResponse<Headers>
        {
            // ...
        }
    }
    
    /// Operations that are valid only in Headers state.
    impl HttpResponse<Headers> {
        fn header(&mut self, key: &str, value: &str) {
            // ...
        }
    
        fn body(self, contents: &str) {
            // ...
        }
    }
  • 有点像第一个版本,

  • HttpResponse 上的所有这些操作都显示在为 HttpResponse 生成的同一个 rustdoc 上,但在不同的标题下,每个 impl 块一个。

  • 将文档注释附加到每个 impl 块,如上所示,以帮助用户跟进。

  • 第一个版本每个状态一个类型的示例中,这些方法分布在多个页面上,使它们更难理解

  • 要添加在任何状态下都有效的操作,只需让 S 不受约束:

    /// These operations are available in any state.
    impl<S> HttpResponse<S> {
        fn bytes_so_far(&self) -> usize { /* ... */ }
    }

第四次尝试

  • 在第一个版本中,能给每一个子状态添加自定义字段,这样不同的状态存储不同的信息
  • 第三个版本中,在所有状态下都使用一个通用结构,已经失去了这种能力
  • 用作状态类型参数的状态类型是无法在运行时实例化的幻像类型。这通常很有用,但并非必须如此。
  • 如果状态类型是具体的,能在其中存储内容;通过在的公共结构中存储一个 S ,继承了它的内容
  • 可能想要跟踪在HTTP 响应中发送回客户端的状态代码
    • 使用 Option<u8> ,它从 None 开始,然后在 status_line 中设置为 Some(code) ,但这并不理想,
      • 任何功能,在任何状态下,都能尝试访问代码,即使只有在发送状态行后才有意义这样做。
      • 对代码的任何访问都必须处理 None ,即使知道该字段是在状态行发送后设置的。
      • 将为所有状态的代码分配空间,这是一种浪费。在这种情况下,该字段很小(一个字节),但如果它很大呢?
    • 状态放在用作参数的状态类型中,这些问题就会消失
      // Similar to before:
      struct HttpResponse<S: ResponseState> {
          // This is the same field as in the previous example.
          state: Box<ActualResponseState>,
          // Instead of PhantomData<S>, we store an actual copy.
          extra: S,
      }
      
      // State type options. These now can add fields to HttpResponse.
      
      // Start adds no fields.
      struct Start;
      
      // Headers adds a field recording the response code we sent.
      struct Headers {
          response_code: u8,
      }
      
      trait ResponseState {}
      impl ResponseState for Start {}
      impl ResponseState for Headers {}
    • 这意味着在 Start 状态下,响应比 Headers 小一个字节;这在这里可能微不足道,但如果要存储更多状态,它就会变得有用。
      impl HttpResponse<Start> {
          fn status_line(self, response_code: u8, message: &str)
              -> HttpResponse<Headers>
          {
              // Capture the response code in the new state.
              // In an actual HTTP implementation you'd
              // probably also want to send some data. ;-)
              HttpResponse {
                  state: self.state,
                  extra: Headers {
                      response_code,
                  },
              }
          }
      }
      
      impl HttpResponse<Headers> {
          fn response_code(&self) -> u8 {
              // Hey look, it's the response code
              self.extra.response_code
          }
      }
    • 在 Headers 状态下,保证拥有 response_code ,并且能直接访问它。

#type/rust #public

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

No branches or pull requests

1 participant