Hacker News new | ask | show | jobs
by stickyricky 1612 days ago
> Why does adding types result in any more coupling than what exists already?

Because the client and server now share a code base. The client requires the server to run.

> If your client and server both treat field "temperature" as a string for protocol version 1, and then the client upgrades to protocol version 2 and temperature is now a float, the server has to be modified anyway, type system or no type system, because otherwise it'll break.

If the data interchange format changes then yes both the client and server need to change. But if either the client or the server wants to coerce temperature to a different type they are free to do so. As long as the conform to the interchange format the internal representation of the data is irrelevant to third-parties.

In a world where types are shared, updates to the server necessitate updates to the client even if the client doesn't want to change (and vice-versa).

> If anything, the type system helps to expose the fact that the client and server now disagree about the type of a field, which is helpful.

The coupling should receive the credit not the type system. Regardless this undoubtedly true. Two isolated pieces of code don't know what the other is doing. You need to test and you need to write design docs.

2 comments

Much of the point of TS is to make make this kind of refactoring easier, and it doesn't prevent coercing the data format later at all.
> Because the client and server now share a code base.

This isn't true. Types aren't "a code base", they're an interface - without which it is impossible for a client and server to talk with each other anyway.

> The client requires the server to run.

Not relevant. Nothing about a typed interface prevents the client from running without having a running server - it won't be very useful without something to exchange data with, but you can still run it, unless you've coded otherwise.

> But if either the client or the server wants to coerce temperature to a different type they are free to do so.

No, type coercions are bad engineering. They're brittle and only work in a very tiny number of situations - nothing approaching the general case.

> As long as the conform to the interchange format the internal representation of the data is irrelevant to third-parties. Type coercions at the interface level are a hack that is not acceptable for anything beyond hobbyist work.

An interchange format is a set of types. Types are not internal representation - they're a mathematical concept used in type-checking. Types happen to be used in the process of generating an internal representation of data, but that's not what they are. You're falsely equating the two, and your point is invalid without that equivalence.

> In a world where types are shared, updates to the server necessitate updates to the client even if the client doesn't want to change (and vice-versa).

This is false. If the server is changed, the client only needs to change if the interface between the two also changes. If the server makes a change to its internal data storage format, then that has nothing to do with the client, and so the interface stays the same, and so the shared type declarations/schema stay the same.

If the interface changes, then you already have to update both client and server, so the tiny overhead of maintaining an explicit type schema dwarfed by the amount of effort you spent changing the rest of the code anyway, and more than made up for by the compile-time detection of warnings when you change the interface code for the server, compile, and then get an error that the client no longer compiles.

> The coupling should receive the credit not the type system.

A type system will detect type errors at compile-time. A tightly-coupled system with type coercion and no types will detect errors at run-time, if at all. It's obviously better to detect errors at compile-time. Therefore, even if coupling is correlated with the usefulness of a type system, type systems are still obviously beneficial to include.

Regardless, it doesn't make any sense to talk about "credit" in this case. I think that you misunderstand the purpose (and utility) of type systems, but I'm not sure what to suggest to you so that you can understand.

> I think that you misunderstand the purpose (and utility) of type systems, but I'm not sure what to suggest to you so that you can understand.

I enjoy writing code in typed languages. I am in full agreement that they have purpose and utility.

> No, type coercions are bad engineering.

The opposite is true. Your job as a software engineer is to coerce types. I don't mean calling `float64(unsafeString)`. That's certainly type coercion but, as you said, its brittle and probably not the best design.

Consider a database. It stores its data on disk as something (ignore implementation). It loads that data into memory where it is coerced to a type (probably in the C-language). Its filtered, grouped, whatever else. Serialized to something and sent over the wire. Your Python server receives that something and coerces it to a Python type. That Python type is serialized to something and sent over the wire. Your Javascript front-end loads the data and represents it as a Javascript type.

Nothing about this process is bad or ugly or brittle. It would be infinitely worse to maintain the something type across all these different boundaries. Imagine pickling types whenever we want to save something to disk or share it over the network. Imagine being forced to write application code in the same language as your database!

> Type coercions at the interface level are a hack that is not acceptable for anything beyond hobbyist work.

Representing a value in the database as an enum and as its string name in the interchange format is not hobbyist work. I'm certain if you have worked professionally you've used enums and not marshaled their underlying value.

> If the server is changed, the client only needs to change if the interface between the two also changes.

You are advocating for type coercion. The server and client store internal representations and must marshal an interface type to communicate. That's a form of type coercion (I should say "casting" since its explicit but I've mistakenly used coercion so far so we'll roll with it).

---

I think you're too tunnel visioned on the types argument. Types are great. I love them. But coupling your database to your server to your client just to share types between the three is obviously not a good idea. There are greater concerns at play. Extend the argument all the way. Separate your client from your server.

Thank you for your reponse.

> I enjoy writing code in typed languages. I am in full agreement that they have purpose and utility.

My apologies, that was a bit uncharitable of me.

And, you're right, I didn't quite understand the meaning of "coercion" - I misinterpreted it to mean "implicit coercion". So, my intention was to say that "implicit type coercion is bad engineering" - you're correct in that in general, it's not bad, and necessary.

However, you still don't understand my point, which is that adding types don't introduce any more coupling than what actually had to exist in order to make the program run in the first place.

Let's ignore implicit type coercions, because they are bad engineering, and not relevant to the following point. In a language without implicit type coercions, programs must be well-typed to run correctly. In dynamic languages, even though the code might not contain type annotations, the above principle must still hold. If you pass a string to a function expecting an integer, then your code will break, and all that dynamic typing does is push the error from compile-time to whenever in run-time that you hit that code path.

This is equally true for a client-server interface. If your client and server do not agree on their interface, which includes types (alongside packet header, and field length and order), they will break. If you do not explicitly state what those types are in some formal spec, the types still have to line up in the program! The types are still there, even if you don't explicitly state or check them.

> It would be infinitely worse to maintain the something type across all these different boundaries.

This isn't true; you already maintain types between the boundaries formed between functions in a single program, there are far more of those than inter-program boundaries, and after you set up the tooling for checking network communication schemas, it's only a little more work than maintaining "normal" types between functions.

> Imagine pickling types whenever we want to save something to disk or share it over the network.

I don't understand - "pickling types" is redundant because the pickle format already includes types automatically. Unless you mean building a schema for the pickle and machine-checking that the loading code and the saving code match? In which case, if you're in an engineering context, I absolutely advocate for this, because a mismatch means a runtime error at best, and data loss or corruption at worst. The argument for doing so in a client-server setup is even stronger, however, because data saving and loading code are colocated with each other (making errors easier to manually spot) far more often than sending and receiving code in a client-server codebase.

> Imagine being forced to write application code in the same language as your database!

There's absolutely nothing about type schemas that forces you to use a single language - most schemas are language-independent, like JSON Schema[1] (which is a schema (including types) for JSON data that is in JSON, so you already have a parser for it, and you write it once and then check it everywhere) and ASN-1.

> coupling your database to your server to your client just to share types between the three is obviously not a good idea

The point is that there's no more coupling than what exists in the first place. Programs must be well-typed to run correctly (if there's implicit type coercion, which is bad practice, the program must still be well-typed - you just now automatically insert type-level logic for the coercions) - that means the types are there, even if you don't write them, so the coupling is also there, even if you don't want it.

If your database sends a string to your server when it was expecting an integer, then either your program will crash, or there's implicit coercion - bad practice, but then either the program is well-typed, in which case it's actually technically expecting either a string or an integer (with runtime detection - also bad practice), or the program is not well-typed, in which case you'll get a runtime error.

[1] https://json-schema.org/

If we reset for a moment. My point is that merging an SPA with an API server with the explicit and sole purpose of defining a type once and using it in two different domains is not a meaningful use of programming time. In my perfect world, an identical definition of the type would be defined in each domain.

You're welcome to disagree but I'm having a hard time following the conversation.

When did I ever say that you would need to "merge" your SPA with an API server? I didn't, because you don't, because, like I said above...

My point is "there's no more coupling [from using an explicit shared type schema] than what exists in the first place." for which I provided a logical argument in the comment you replied to.

This was in response to your earlier statement "At the expense of coupling which is not a trivial concern."

Do you have any additional arguments for why adding a type schema introduces additional coupling that were not covered above?

I use a JSON specification to manage interchange. I repeatedly reference the necessity of data interchange formats (e.g. a well-typed JSON schema). Interchange formats mean applications couple to a specification. Not to a code base. Which I'm sure you're well aware since apparently we've been in complete agreement the entire time.