Live data from Hacker News

Arguing against using protobuffers

reasonablypolymorphic.com

291–300 of 307 posts

Re: Arguing against using protobuffers

#291

Earlier quoted context omitted.

Maybe there's an amazing type system idea out there that would be even better, but I don't know what it is. Required and optional is just an encoding of nullability in the type system. This is a common feature in most modern languages (Go excepted). Clearly Google got a very long way with proto1, whose designers felt strongly enough this was important to put it into what is otherwise a very feature-lite system. The c…

You have misunderstood the "required considered harmful" argument. It's not fundamentally about the abstract concept of required vs. optional but about the specific implementation in Protocol Buffers, which turns out to have had unintended consequences. Specifically: As implemented, required field checking occurred every time a message was serialized, not just when it was produced or consumed. Many systems involve mi…

Good to know about the 0x00 trick.

My point about metadata is that if protobufs had even a small amount of self description in them, the middlemen who weren't being updated could all have been found automatically and the version skew issue would have been much less of an issue. Like how Dapper can follow RPCs around, but for data and running binaries.

Google doesn't do that because for its specific domain it needs a very tight encoding, amongst other reasons (and the legacy issues). It could have fixed the validating-but-not-updated-middleman issue in other ways, but instead it made the schema type system less rigorous vs more rigorous. That seems the wrong direction.

Re: Arguing against using protobuffers

#293

Earlier quoted context omitted.

> Maybe there's an amazing type system idea out there that would be even better, but I don't know what it is. Certainly the usual proposals I see seem like steps backwards. I'd love to be proven wrong, but not on the basis of perceived elegance and simplicity, but rather in real-world use. Care to elaborate on what these usual proposals are and why they're backwards? Right now I'm thinking of 'row polymorphism', whic…

"Row polymorphism" sounds like exactly what protobuf does, which I think is the right answer. As I mentioned, I've heard people argue that the client and server should pre-negotiate exactly which version of the protocol they are using, and then that version should have a rigidly-defined schema. It may have been unfair to suggest that this is a common opinion among type theorists -- I don't have that many data points.

Row polymorphism is a more principled approach that makes the type theorists happy.

The point of row polymorphism is not just that you can say "I may or may not have other fields, but I definitely have a 'name' and 'email' field", but that the extra unknown fields have a name that can be used to constrain other types.

For example, you can have this function type:

{name: string, email: string, &rest} -> {name: string, email: string, &rest}

This is different than this function type:

{name: string, email: string, &rest} -> {name: string, email: string}

The first type can act as a forward compatible passthrough because it says that whatever extra fields are in the input are also in the output. The second type promises that its output only has name and email fields.

The same applies to variant types: you can say that a variant has option A, option B, and other options rest:

(A | B | &rest) -> (A | B | &rest)

vs

(A | B | &rest) -> (A | B)

Functions of these types are polymorphic in the schema. For instance, the type (A | B | &rest) -> (A | B) can be instantiated with rest = (C | D) to get (A | B | C | D) -> (A | B).

So row types are fundamentally different in that it's the functions that that explicitly deal with multiple possible schemas. At the end of the day all of the rest variables get instantiated with explicit types to get a concrete instantiation of a function in which all the rest variables have been replaced by concrete types.

A serialisation library could use this to do version negotiation automatically. After it has negotiated a version, the library instantiates the functions so that the rest type variables get instantiated with the actual concrete version of the schema.

Different languages implement row types in different ways. Some compile row types in a way akin to Java's type erasure. They compile a single version of each polymorphic function that can be used regardless of how the rest parameters get instantiated. Some compile row types in a way akin to C#'s generics or C++ templates: they compile a separate version for each instantiation.

The advantage of the latter is that the data representation can be optimised with full knowledge of the concrete schema. If we have a function of type {name: string, email: string, &rest} -> {name: string, email: string, &rest} instantiated with rest = {age: int} then that compiles to a version of type {name: string, email: string, age: int} -> {name: string, email: string, age: int}. This compiles to faster code because the compiler statically knows the size of the thing.

In a client-server situation you wouldn't know the schema of rest until run time, so you'd need either have a JIT compiler that can compile new versions at run time, or specify a fixed number of options for rest at compile time. To update a client-server application you'd need to recompile both the client and server with support for the new version. That's not nice but it does not have a chicken and egg problem because both the client and server still support the old version too.

TL;DR: with row types schemas are always rigidly defined, it's the functions that can handle multiple schemas.

Re: Arguing against using protobuffers

#294

Earlier quoted context omitted.

I've long thought about creating a standard(iana type) for PostgREST json schema + http querystring conventions. I've seen some APIs[1](only one I can remember now) that follow PostgREST conventions, so perhaps this could benefit all of us who don't want to jump on the GraphQL bandwagon but still want an expressive and interoperable/standardized way to query resources. We currently use OpenAPI but we found some short…

Thanks to you and the other maintainers for your tireless work on Postgrest (also as a member of the Haskell community)! Is there a paypal link for one-time-donations? I don't use patreon and am not really down to increase my online footprint but would love to be able to donate (I also couldn't find any mention of taking donations on the github or in the docs @ postgrest.org... you might get more donations if it were…

Thanks a lot for your feedback @hardwaresofton, I've just added a section in our README that includes a Paypal link for one-time donations.

https://github.com/PostgREST/postgrest#supporting-developmen...

Thank you for your support!

Re: Arguing against using protobuffers

#295
post #287

Earlier quoted context omitted.

After re-reading your comment above, I'm actually confused. You think you should never store a big-endian int? That is ridiculous. Some architectures are big-endian. You should not be using custom bitswapping as part of application code, because you cannot know the endianness of your architecture. The ntoh* functions are the right approach, and your claim is not only strong, it's wrong. The ntoh* functions exist to t…

Let me try saying it differently. The following code is poorly written: char *buf = ...; uint32_t word = *(int32_t *)buf; uint32_t host_word = ntohl(word); Because you just type-punned the read from buf. (In fact, this code is UB.) You could write it a little better like: char *buf = ...; uint32_t word; memcpy(&word, buf, 4); uint32_t host_word = ntohl(word); Although IIRC there is or at least was still some disagree…

My C code didn't include mention of ints though, so I'm wondering where you got that from.

Your first example is UB and again, is not something my example depended on.

Your final claims are overly cautious. It is perfectly fine to use uint32_t in this way. Uint32_t is defined as a 32-bit unsigned integer. There is a bijection between network order 32-bit unsigned integers and host order integers, and ntohs is the bijection. It is no different than storing any other value. It is certainly not wrong.

Re: Arguing against using protobuffers

#296

Earlier quoted context omitted.

Maybe there's an amazing type system idea out there that would be even better, but I don't know what it is. Required and optional is just an encoding of nullability in the type system. This is a common feature in most modern languages (Go excepted). Clearly Google got a very long way with proto1, whose designers felt strongly enough this was important to put it into what is otherwise a very feature-lite system. The c…

You have misunderstood the "required considered harmful" argument. It's not fundamentally about the abstract concept of required vs. optional but about the specific implementation in Protocol Buffers, which turns out to have had unintended consequences. Specifically: As implemented, required field checking occurred every time a message was serialized, not just when it was produced or consumed. Many systems involve mi…

> Validate your data in application code, at consumption time, where you can handle errors gracefully.

Honest question: how can I validate data in application code when optional fields decode to a necessarily-valid value by design?

Suppose I'm an application author and I have an integer field called "quantity" which decoded to a 0. How can I tell whether that 0 meant "the quantity was 0 in the database" or "the quantity field was missing" instead?

(One answer is that I should opt into a different default value, like -1, which the application can know indicates failure. If that's what I should always do, then why not help me gracefully recover by requiring that I always specify my fallback value explicitly, rather than silently defaulting to a potentially misinterpretable valid value like `0`?)

I understand that required fields break message buses that only need to decode the envelope, but if I am working on a client/server application where message buses are not involved (as almost all client/server programmers in the world are), I don't follow how "everything is optional, and optional means always succeed with a valid default value" facilitates graceful recovery in the application layer. In order to gracefully recover, the application has to be informed that something went wrong!

It seems to me that this design more directly facilitates bugs in the application layer that are difficult to detect because the information that something unexpected happened during decoding is intentionally discarded by default. It makes the resulting bugs "not the protocol layer's fault" by definition, but that is not a compelling pitch to me as an application author.

What am I missing?

Re: Arguing against using protobuffers

#297
post #161

Earlier quoted context omitted.

> every application has to be updated when a field is added, even if they do not use that field No, you maintain the older versions of the API. V1 of the API uses the V1 struct. V2 of the API uses the V2 struct, etc. Older applications maintain compatibility because it calls the older APIs, and you can convert between V1 to V2 and only keep one version of the API. Or, if you want, you can maintain both versions of th…

You are missing when you have a middle layer. Message comes in at v3 and hits a layer that only knows v1 then gets passed to a layer that is at v4. I'd wager most places don't have that many layers. But, if you are embracing microservices, you'll find yourself here fairly fast.

> You are missing when you have a middle layer.

And storage. People might have petabytes of historic data stored in protobufs.

Re: Arguing against using protobuffers

#298

Hello. I didn't invent Protocol Buffers, but I did write version 2 and was responsible for open sourcing it. I believe I am the author of the "manifesto" entitled "required considered harmful" mentioned in the footnote. Note that I mostly haven't touched Protobufs since I left Google in early 2013, but I have created Cap'n Proto since then, which I imagine this guy would criticize in similar ways. This article appear…

> OK, well, I've worked on lots of systems -- across three different companies -- where this feature is essential.

Here's an actual real life example: Chrome uses this in Chrome Sync feature that allows you to sync your browser configuration and state across various devices. The feature is implemented basically like this: Chrome sends its version of state to server in a proto, and the Sync server reconciles it with the one it has saved, updating both according to which one is more recent. The feature fundamentally depends on the fact that the client won't drop the fields it doesn't know, because it would then be data loss for some other more recent client who knows these fields and synced them to server: if the older client dropped these unknown fields, it would be an equivalent to syncing in an empty value of this field.

Original designers of proto3 (the most recent protocol buffers definition language and semantics) actually decided to drop the unknown field preservation, for simplicity reasons. This made Googlers so unhappy that an internal doc was created listing many internal use cases for this feature, and after discussion, this was added back to proto3.

Re: Arguing against using protobuffers

#299

Earlier quoted context omitted.

You have misunderstood the "required considered harmful" argument. It's not fundamentally about the abstract concept of required vs. optional but about the specific implementation in Protocol Buffers, which turns out to have had unintended consequences. Specifically: As implemented, required field checking occurred every time a message was serialized, not just when it was produced or consumed. Many systems involve mi…

> Validate your data in application code, at consumption time, where you can handle errors gracefully. Honest question: how can I validate data in application code when optional fields decode to a necessarily-valid value by design? Suppose I'm an application author and I have an integer field called "quantity" which decoded to a 0. How can I tell whether that 0 meant "the quantity was 0 in the database" or "the quant…

> Suppose I'm an application author and I have an integer field called "quantity" which decoded to a 0. How can I tell whether that 0 meant "the quantity was 0 in the database" or "the quantity field was missing" instead?

First, this is clear on the level of wire encoding: either the field has encoded 0 value, or it is simply missing from encoding.

Second, in proto2, you actually have has_quantity() method on a proto message, which will tell you whether quantity is missing or set to 0.

In proto3, the design decision was that the has_foo() methods are available only on embedded message field, and not available on primitive fields, so you'd have to wrap your int64 in a message wrapper, like e.g. the ones available in google/protobuf/wrappers.proto.

The point here (and a common pattern inside google3) is that in your handling code you simply manually check the presence of all required fields: if (!foo.has_quantity()) { return FailedPreconditionError("missing quantity"); }. It is a bit of a hassle, but the benefit is that you have control on where the bug originates and how it is handled in your application layer, as opposed to silently dropping the whole proto message on the floor.

Re: Arguing against using protobuffers

#300

Earlier quoted context omitted.

>Instead, you're going to get errors from the clients using version 2, because server version 2 was rolled back. You have to roll back the clients as well then. This depends on the update. If indeed the field was optional, you won't. A common example would be a field that is necessary for a new feature, but without which everything functions just fine, or functions with a minor degredation in experience. But more imp…

We agree to disagree. I don't think you can convince me that all optional is better than all required and vice versa, which is okay. My point is required fields makes software age better over the long run because everything is explicit. If you don't agree, that's your prerogative. Everyone thought NOSQL without schemas was a godsend, until their code/service iterated a dozen times, developers leave, documentation get…

Right, and my point is that all required fields prevents you from iterating. Your software doesn't age at all.

I've never found the problems you describe, and I work with some of the oldest protos around!

Post reply on HN