Live data from Hacker News

Simple Is Not Small

jyn.dev

91–95 of 95 posts

Re: Simple Is Not Small

#91
post #87

Earlier quoted context omitted.

I think I struggle to assemble a coherent notion of what it means to (conceptually) decouple a value from its type. You can completely forget the type of a value and treat it as opaque bytes, but then there are no valid operations left on the value. Even moving it around or discarding it may be invalid if it's pointed to elsewhence. The only thing you can meaningfully do is try to recover its (static or dynamic) type…

Types are used to both define how data is represented in memory and to restrict which values are allowed. For instance, an enum might be internally represented as a byte, but the compiler further restricts the permitted values to those the enum represents, and limits what operations are available (e.g. we can multiple a u8 by a u8, but not an enum by an enum). In this sense, most statically-typed languages conflate h…

In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values. It's an unfortunate historical accident, which we're slowly getting over, that we conflate values with data (in the sense of ‘plain old data’, i.e. values that support a special hardware-supported kind of copying and moving, et cetera) and data with numbers, and then think we have to use types to restrict it from being treated as numbers and get back to smaller sets of values.

> In this sense, most statically-typed languages conflate how data is structured with how it is restricted.

Most statically-typed languages are actually very loose about how values are structured at runtime, leaving it mostly up to the implementation (e.g. see C++ padding and field reordering, or Haskell's autoboxing, which makes approximately no guarantees about what's behind the pointer — usually some graph-rewriting metadata). Where the conflation does exist is that a lot of systems languages allow you to write and typecheck code that assumes something about the language's representation of the type's values (e.g. that you can take the address of a field of a struct and later dereference it), though you can usually opt out of that with PIMPL or a trait object or something. But guaranteeing (and allowing the programmer to rely on the guarantee) that every value's representation also carries a bunch of additional runtime information is a much stronger version of that coupling.

> This can be useful when dealing with data that is in some sense invalid. You might receive data that's outside expected bounds or even of a different type, and it might make sense to handle it in some fashion.

The very fact that you can handle that data at all means that the value carries additional type information that allows you to do so. It's only ‘decoupled’ from the type in the sense that you didn't have to write it there, because it's automatically coupled to every value representable in the language regardless of what type you give it.

> This is a common necessity in pharmaceutical trials, for example.

I'll have to do some guesswork here, but I imagine when people make arguments like this they are significantly imagining a situation in which, say, all the values are expected to be in the range [5, 100] and some befuddled experimenter or piece of machinery gives you the value 2. A-ha, you say: I know sometimes the equipment undermeasures near the bottom of its range, so I'll clamp this value to 5!

This isn't really a type error. The fact you know you can safely do that means that the data is really typed in a different range than you said — but it's still typed. The conceptual type (even if you never write it down) is inherent in the very fact that you can somehow handle it: you know what to do with values down to 2 so the real type of supported inputs is at least [2, 100] (with some special semantics for the low values beyond that of being numbers).

A real type error looks like: you're expecting values in [5, 100] and then one of the values actually turns out to be the concept of intellectual honesty. That type doesn't support ~any of the same operations as the numbers you were expecting, even with the extended domain that more accurately reflects the set of values you can really accept. Even discarding it might have disastrous results for your experiment! In fact, the concept of intellectual honesty doesn't even have a good discriminator: while I know that's what you got because I'm the rascal who snuck it in there, you have no idea what it is, and no way of finding out. Most likely you're going to try to compare it to 5 to see if it needs to be clamped, with unpleasant consequences for us all.

Re: Simple Is Not Small

#92
post #91

Earlier quoted context omitted.

Types are used to both define how data is represented in memory and to restrict which values are allowed. For instance, an enum might be internally represented as a byte, but the compiler further restricts the permitted values to those the enum represents, and limits what operations are available (e.g. we can multiple a u8 by a u8, but not an enum by an enum). In this sense, most statically-typed languages conflate h…

In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values. It's an unfortunate historical accident, which we're slowly getting over, that we conflate values with data (in the sense of ‘plain old data’, i.e. values that support a special hardware-supported kind of copying and moving, et cetera) and data with numbers, and then think we have…

> In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values.

I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001.

A type limits both which values it is possible to represent (i.e. a u8 limits us to representing the integers 0 to 255), and also determines how it is encoded in memory (in this case 8 bits).

> Most statically-typed languages are actually very loose about how values are structured at runtime

Yes, but ultimately the compiler needs to be able to map a sequence of bits to the value it represents, even if there's not a strict one-to-one mapping.

> I'll have to do some guesswork here, but I imagine when people make arguments like this they are significantly imagining a situation in which, say, all the values are expected to be in the range [5, 100] and some befuddled experimenter or piece of machinery gives you the value 2

That's a common assumption, but the reality can be much more messy. It might be that we expect an integer between 5 and 100, but receive a string of UTF-8 characters instead.

For example, suppose you want to record a patient's date of birth. In Clojure, we might represent that as a map that connects a :patient/birthdate key with an encoded date object:

    {:patient/birthdate #date "1972-04-08"}
But what if the patient doesn't know their exact birthdate? Perhaps they immigrated when they were a young child from a less developed country and don't know the exact year they were born in. However, they tell the doctor that they do know they were born before 1980, because that's when they first arrived in the country.

In this case, the doctor might record the data as:

    {:patient/birthdate "before 1980"}
Even though the data doesn't match the type we expect (a date), it's important that it still be recorded as it could affect the medicine that the patient is given. These sorts of messy entries are not uncommon in areas where it's more important to accurately record the data than precisely type it.

Re: Simple Is Not Small

#93
post #91

Earlier quoted context omitted.

In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values. It's an unfortunate historical accident, which we're slowly getting over, that we conflate values with data (in the sense of ‘plain old data’, i.e. values that support a special hardware-supported kind of copying and moving, et cetera) and data with numbers, and then think we have…

> In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values. I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001. A type l…

> I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001.

I'm using it in its most general sense: as an object of discourse in a programming language, independent of representation or semantics. 1 is a value in most languages, to be sure, but it's also specific type of value, viz. a number: you can do number things to it, like add it or divide it, or check it for equality with 2.

> A type limits both which values it is possible to represent (i.e. a u8 limits us to representing the integers 0 to 255), and also determines how it is encoded in memory (in this case 8 bits).

To reiterate: a type doesn't limit the values but specifies the values (or rather, more generally, the meaningful operations on a value of that type). Values are not numbers by default; only by being typed as a number does a value take on number semantics. Without the knowledge that a value is a number it is meaningless to treat it as a number.

> Yes, but ultimately the compiler needs to be able to map a sequence of bits to the value it represents

Sure; in any language implementation you have to represent the values somehow, nobody could disagree. My point is that the type doesn't (necessarily) specify that representation in any language I can think of.

> That's a common assumption, but the reality can be much more messy. It might be that we expect an integer between 5 and 100, but receive a string of UTF-8 characters instead.

Sure: that's not fundamentally different from the first example I gave. The real type of `:patient/birthdate` there is just the (discriminated) union of the date type and the string type. It still has a type, and if it didn't you wouldn't be able to process it (definitionally, because a type tells you what kind of processing makes sense for the value). And the string values aren't ‘outside’ the type: even if you choose to write the wrong type down in your Clojure, the fact that you also process strings means that you know the real type (and you embed that knowledge into the code).

Re: Simple Is Not Small

#94
post #93

Earlier quoted context omitted.

> In fact the opposite is true: raw values have no valid operations and types allow you to add operations that make sense for those values. I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001. A type l…

> I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001. I'm using it in its most general sense: as an object of discourse in a programming language, independent of representation or semantics. 1 is a va…

> To reiterate: a type doesn't limit the values but specifies the values (or rather, more generally, the meaningful operations on a value of that type).

But a value can have more than one possible type. The number 1 could come from an unsigned byte, or a signed long, for example. These are different types that support the same numerical operations, but differ in cardinality. So we can't say that a type's only purpose is to specify the meaningful operations on a value, as we might have two types that are identical in that regard.

> The real type of `:patient/birthdate` there is just the (discriminated) union of the date type and the string type.

Yes, in this particular instance that would be the case, but that's not necessarily something you know ahead of time. The point is that you may not have anticipated that not everyone would know their date of birth, and the data you receive is invalid according to your earlier assumptions.

In Clojure this results in a more graceful failure condition. Functions that don't require the date of birth will continue to work with no change required. If I want the average white blood cell count of a patient, I don't care what the date of birth is, and therefore the output for that particular operation isn't affected.

Re: Simple Is Not Small

#95
post #93

Earlier quoted context omitted.

> I think you're using 'value' to mean something slightly different to the way I meant it. So to get us onto the same page, by 'value', I mean something independent of how its stored; that is, the number 1 is a value, whether it's stored as as 00000001 or 0000000000000001. I'm using it in its most general sense: as an object of discourse in a programming language, independent of representation or semantics. 1 is a va…

> To reiterate: a type doesn't limit the values but specifies the values (or rather, more generally, the meaningful operations on a value of that type). But a value can have more than one possible type. The number 1 could come from an unsigned byte, or a signed long, for example. These are different types that support the same numerical operations, but differ in cardinality. So we can't say that a type's only purpose…

> But a value can have more than one possible type. The number 1 could come from an unsigned byte, or a signed long, for example.

Here we disagree. Unsigned 8-bit integers† and signed 64-bit integers, while both conveniently notated with Arabic numerals, are actually different values that support different operations, for example negation. They share quite a few similarities in how their operations interact with one another, for example each (assuming wrapping) is a monoid with 0 and +, but the semantics of the actual operations differs if looked at more closely. Mathematics agrees: the element ‘1’ of N/2⁸ and the element ‘1’ of Z/2⁶⁴ are not the same thing (their encodings coincide sometimes, but it's poor form to make assumptions about it).

† Bytes are data but not numbers, and so support only data operations like duplication and discarding, not number operations like adding and multiplication: as an artefact of representation you can usually ask the hardware or programming language to manipulate them as if they were numbers, but the result remains meaningless.

> The point is that you may not have anticipated that not everyone would know their date of birth, and the data you receive is invalid according to your earlier assumptions.

But that's exactly what I'm saying: you must have made some assumptions about that missing data, otherwise there is no safe thing you can do to it (including discarding it, which is a popular choice). This works in Clojure only because Clojure couples some semantics (data semantics plus operations on runtime type information) into every value, i.e. it restricts what values are even representable in the language in order to ensure that this function will always be safe to write.

In more strictly typed languages you can still talk about values that support these behaviours, but you are required to be explicit about it, because there are some values that can be represented that don't support these operations.

> Functions that don't require the date of birth will continue to work with no change required.

Any function that takes the date of birth ’requires’ the date of birth (or more generally a possibly-empty set of ‘leftover’ values). The only thing that differs is what's required from it: some functions might require that it be a date while other functions only require that it be data, for example. The universally imposed limitation that all values must be coupled to data semantics and runtime type information is convenient for ergonomics if you write a lot of these functions (since you don't have to remember to write that assumption down), but it's important to remember that it is a coupling — the resulting values are more complex than values without those things bundled on, and the trade-off is that you can no longer talk about values for which they don't hold.

Post reply on HN