Hacker Newsnew | past | comments | ask | show | jobs | submit | Twey's commentslogin


> The reason for this is that in Rust, a struct couples type-checking to a fixed data representation. You can't get one without the other.

> Clojure decouples data representations from type checking.

This is funny to me because seen from the other side, (this) Clojure couples runtime type information to data structures: you're no longer allowed to define a data structure that doesn't have some runtime type information attached. A fixed static structure is just the consequence of not adding dynamic type information.

Meanwhile in Rust you can get type-checking ‘without’ a fixed structure by using trait objects.


Regardless of whether the type information is static or dynamic, you're still coupling some type to some data. The type is still implicit even after compilation; there still exists a structure to the data, even if that structure isn't easily discerned without the source. Or to put it another way: just because there's no runtime type information, doesn't mean that the data now is entirely decoupled from the type.

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 information from somewhere and re-‘couple’ it.

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 how data is structured with how it is restricted. Some overlap is unavoidable, as anything represented by a single byte is always going to be restricted to at most 256 values, but Clojure tends to take the view that the more decoupling (or decomplecting) you can achieve the better.

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. This is a common necessity in pharmaceutical trials, for example.


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.


> 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.

> 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).


> 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.


> 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.


> 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.

What about a 32 bit unsigned integer and a 64 bit unsigned integer? Are they still separate values?

You appear to be saying that the values of a type cannot be a subset of another type; that is, there is no '1', only a '1' that is an integer, a '1' that is a short, and so forth, and every '1' is distinct.

Fine, that's a possible way of looking at it, but why is that more valid or consistent than a model that allows subtypes? That, for example, the value '1' could be both a Number, an Integer, and a NaturalNumber?

Further, what's the practical difference between a type defined as consisting of the numbers [1 2 3], and an integer that's restricted to those values?

> 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.

Even if we view a Clojure value as a coupling between type and data, that's only a coupling between two things, and it ensures we can avoid further coupling caused by large record types. On net, we reduce the amount of coupling a statically typed language that uses closed record types would require.

For instance:

    (defn average-wbc-count [{:patient/keys [wbc-counts]}]
      (/ (apply + wbc-counts) (count wbc-counts)))
This function is coupled to only one key/value pair. Any other information in the map is irrelevant, which is why an invalid :patient/birthdate doesn't cause the function to fail. There's no coupling between :patient/birthdate and average-wbc-count.

Conversely:

    fn average_wbc_count(patient: &Patient) -> f64 {
      patient.wbc_counts.iter().sum::<f64>() / patient.wbc_counts.len() as f64
    }
This function requires patient to be a Patient struct, and therefore the function is implicitly coupled to every field in the struct, regardless of whether that field is ever actually used. I need to ensure that a patient has some birthdate that's an anticipated type (even if that's an error type) before I can call the function.

> You appear to be saying that the values of a type cannot be a subset of another type; that is, there is no '1', only a '1' that is an integer, a '1' that is a short, and so forth, and every '1' is distinct.

That's true, and you can prove it simply by observing that they react differently to ‘the same’ operations, e.g. (2¹⁶ - 1) + 1 has a different value (that responds differently to tests like `≥ 0`) depending on which type we're talking about. There's an injection into the larger type (a type coercion) that is very well-behaved, but it's not an identity map — it changes the operations on the value.

> Further, what's the practical difference between a type defined as consisting of the numbers [1 2 3], and an integer that's restricted to those values?

Nothing (ish: you have to be careful to describe what happens to all the integer operations when restricted to your type) — that's a totally valid way to define a type. But it's not the only way to define a type, because the world of types is much bigger than the world of restricted sets of integers.

> I need to ensure that a patient has some birthdate that's an anticipated type

No, you just have to be explicit about the possibility of a birthdate (or other fields) being of a wide type, e.g.

    struct Patient {
      wbc_count: Vec<u64>,
      birth_date: Box<dyn Any>,
      other_fields: HashMap<String, Box<dyn Any>>,
    }

Clojure just attaches that by default to every value.

> Nothing (ish: you have to be careful to describe what happens to all the integer operations when restricted to your type) — that's a totally valid way to define a type.

Then at which point does a type become equivalent to a restriction that could be decoupled from the underlying type by choosing a broader type?

> No, you just have to be explicit about the possibility of a birthdate (or other fields) being of a wide type

Yes, you could recreate Clojure's semantics in Rust. More accurately, I'd say you'd be looking at defining it like so:

    struct WbcCount {
      wbc_count: Vec<u64>
    }

    struct BirthDate {
      birth_date: Box<dyn Any>
    }
Since we want to be able to reason about these keypairs individually. You might have a structure that has a WbcCount but not a BirthDate, or one with a BirthDate but not a WbcCount, or one with neither.

We'd then be faced with the challenge of creating a type that could contain an arbitrary number of unique structs, and to be able to pull a struct out of that set by its type. Realistically we'd probably just use a HashMap at that point and discard all static typing.

Alternatively, we could create a mega struct that contains every possible field we could want to use, whether or not we know they are related, and use this data structure to represent all structured data in the application. The "if everything is coupled, nothing is" approach.

But both of those options are difficult or unidiomatic to write.

Languages like Rust, Java, etc. encourage coupling of data because it's more convenient and space efficient to group data together in records/structs. In Clojure, there's no need to do so; we can couple only when necessary. If we don't need to know the birthdate to calculate the white blood cell count, then we can exclude that field from the input type checking. Most statically typed languages find this difficult, with TypeScript being one of the few exceptions in this regard.


> Then at which point does a type become equivalent to a restriction that could be decoupled from the underlying type by choosing a broader type?

I'm not totally sure how to interpret this question. If I already have a type that includes all the values I want as a subset, I can restrict that type by limiting the values it can take and restricting or removing the operations on it to guarantee they never produce any of the forbidden values. That's the basis of refinement type systems like Liquid Haskell etc. For any type I can describe this way I can also build it ‘from the ground up’ by starting with the empty type and adding operations, though it might not be as convenient. But the converse isn't true: not every type I can build additively can be refined subtractively from another type. For a start, you need to have a broader type to begin with, so that has to already be built somehow: you can't refine an 8-bit integer into an HTTP server.

> You might have a structure that has a WbcCount but not a BirthDate, or one with a BirthDate but not a WbcCount, or one with neither.

Remember that a `Box<dyn Any>` could also be a ‘no value’ type like `()` or a ‘maybe no value’ type like `Option<Date>`. But yes, there are many equivalent ways to write it depending on how likely the values are to exist; the precise formulation is just a question of ergonomics.

> We'd then be faced with the challenge of creating a type that could contain an arbitrary number of unique structs, and to be able to pull a struct out of that set by its type.

By its name, but yes, that's exactly what you get with the `other_fields` field in my example above.

> and discard all static typing

Well, that's not quite true: we discard (syntactically, but not conceptually!) the type information about the fields of this struct, but we can regain it later (from the conceptual information we retain) by trying to downcast the `Any`. Then you get your static type information back and the compiler can help you again.

> But both of those options are difficult or unidiomatic to write.

Sorry, I hope I haven't come across too harsh to Clojure here. As I thought I was clear about above, Clojure's ergonomics for writing dynamically-typed code are vastly superior to Rust's. By coupling runtime type information to values and restricting itself to only being able to deal with values that are data and have runtime type data attached, Clojure gets to make a whole bunch of simplifying assumptions that reduce the work the programmer has to do when dealing with such values. That's the trade-off: Rust chooses to be able to express a much wider range of types by not requiring that coupling, but in exchange you have to be much more explicit when you do want to couple runtime type information to your values. Some languages like C# aim to be able to do both, i.e. start strongly typed and drop down ergonomically into dynamic typing as desired, but Rust is not one of those languages.

> If we don't need to know the birthdate to calculate the white blood cell count, then we can exclude that field from the input type checking.

But that very exclusion requires that you know enough about the value to know that it can be safely ‘excluded’ without having any additional information about it. Specifically you need to know:

- how to access a subfield of the type regardless of the other fields on it — this requires that it contain RTTI

- how to discard the value — this requires that it have a destructor with a known calling convention

- how to move/copy the value into the function (and maybe even move out of the function if you return it or imperatively add it to some global state) — this requires that you know a ‘moving constructor’ for the value that can be used to safely move it to another location

In the Rust case you need to write down these things explicitly so the compiler can make sure you don't pass a value that doesn't satisfy them. In Clojure, the reason you don't need to write them down is that every value in the language is restricted to only be able to represent such values, so those constraints are implicitly on every function that you write.


> I'm not totally sure how to interpret this question. If I already have a type that includes all the values I want as a subset, I can restrict that type by limiting the values it can take and restricting or removing the operations on it to guarantee they never produce any of the forbidden values.

Sorry, perhaps I'm being a little obtuse. What I'm ultimately trying to get at is that sometimes its useful to have different types for the same data under different circumstances. A subset of data might benefit from a narrower type.

For example, suppose we have some CSV file that contains a bunch of patient information, including average white blood cell count and birthdate. This CSV file might contain bad data! So perhaps we type it as:

    struct Patient {
        avg_wbc_count: Either<f64, String>,
        birthdate: Either<Date, String>,
    }
This covers all our bases, but it's also somewhat annoying to work with. Perhaps we only want to find patients with WBC counts outside a certain range, and discard those lines where the data is invalid. In which case, we could write a narrower type, and simply not parse the CSV rows that are invalid:

    struct PatientWithKnownWbcCount {
       avg_wbc_count: <f64>,
    }
So in this case we have a couple of options for types. The latter type decouples the birthdate (it's never even parsed), but adds the restriction that the WBC count needs to be numerical; while the former type is a more accurate representation of the CSV file overall, but has greater coupling.

Obviously which we use depends on the nature of our program, but what I'm trying to get at is that a type isn't set in stone, but something we choose. Ideally we choose the most restrictive and decoupled type for the circumstances, but that might result in having many hundreds of different variations of the same type, so there's a tension between the number of distinct types and how specific or narrow they are.

> In the Rust case you need to write down these things explicitly so the compiler can make sure you don't pass a value that doesn't satisfy them. In Clojure, the reason you don't need to write them down is that every value in the language is restricted to only be able to represent such values, so those constraints are implicitly on every function that you write.

Granted, but Clojure's approach can result in greater decoupling with less effort, particularly when dealing with imperfect data.

In the previous examples I presented a scenario where we might want to think carefully about how exactly we store data that might contain invalid fields. But in Clojure we don't care - we can quite easily have a data structure that's partially invalid or unparsed - the entire problem of how we represent this data in memory is sidestepped.

We can still define what constitutes valid fields:

    (s/def :patient/birthdate     inst?)
    (s/def :patient/avg-wbc-count float?)
But these schema definitions are independent of the type system (i.e. decoupled), so we can apply them selectively or not at all. We can say "this function requires patient data with valid birthdates and WBC counts, but we don't care about any other field".

Could you do the same in Rust or other similar languages? Sure, it's ultimately just maps and predicates. But many languages aren't geared up to make that pleasant to use.


> Perhaps we only want to find patients with WBC counts outside a certain range, and discard those lines where the data is invalid. In which case, we could write a narrower type, and simply not parse the CSV rows that are invalid:

These two types aren't typing the same data, though: in the latter case (in Rust) you've actually thrown away some of the data, and the type reflects that. In some languages like Clojure or TypeScript you can't distinguish that from the case where you haven't actually thrown away the data and are secretly carrying it around as well as whatever data is explicitly typed there, but if you want to get it back you have to know that it was once there, i.e. have additional (conceptual) type information that you chose not to write down.

> Granted, but Clojure's approach can result in greater decoupling with less effort, particularly when dealing with imperfect data.

I think this isn't a great comparison: the usual vehicle for this thing in Rust is the trait, which manages to abstract over the representation statically without introducing a bunch of runtime machinery for it (though you can opt in to the machinery by using a trait object!). That's very idiomatic in Rust, although the trait syntax is a bit noisier (because it's more general).

> the entire problem of how we represent this data in memory is sidestepped.

It's not sidestepped — the language just makes an opinionated choice for you, which you can't avoid.

> these schema definitions are independent of the type system (i.e. decoupled), so we can apply them selectively or not at all

These are just predicates; I don't think they have much to do with this discussion? They don't help you to type your data: you (the programmer) still have to carry that type information around in your head in order to work with the values even after they are validated.

> Could you do the same in Rust or other similar languages? Sure, it's ultimately just maps and predicates. But many languages aren't geared up to make that pleasant to use.

There are good reasons for that, though. Nominal typing allows you to express and enforce constraints that aren't necessarily enforced by the structure of the type, at the cost of some extra ceremony. And systems languages often want to be able to express types that cannot be treated this way: types in which there is no sensible way to ‘ignore’ a value. Clojure makes some things easier to write by forcing you to carry around a bunch of extra stuff (both runtime data and static semantics) with all your values; that's a perfectly valid choice that makes an important subset of programs nicer to write, but it also excludes values that don't fit into that category or can't reasonably be coupled to their RTTI.

As we get better at writing compilers we're increasingly seeing a move towards mechanisms like Rust's traits or C++'s concepts that allow the programmer to actually abstract over the representation of a type (as opposed to forcing a uniform representation as is common in dynamically typed languages). If you do that well you can get the best of both worlds semantically, but these kinds of systems languages will always be a bit heavier syntactically because the syntax needs to support a wider range of types.


> These two types aren't typing the same data, though: in the latter case (in Rust) you've actually thrown away some of the data, and the type reflects that.

In Clojure the data is thrown away too, just at a slightly later point. The data is parsed then destructured then processed, and it is at the destructuring stage that irrelevant data is discarded and marked for GC.

Just as the Rust function avoids coupling by using a more narrow type, the Clojure function avoids coupling by using a more narrow binding.

> I think this isn't a great comparison: the usual vehicle for this thing in Rust is the trait, which manages to abstract over the representation statically without introducing a bunch of runtime machinery for it

But you have to create and implement those traits. I'm not saying this is impossible in Rust; just a lot more onerous because you don't get all the machinery Clojure has that makes it trivial.

> These are just predicates; I don't think they have much to do with this discussion?

This might be why we're talking past each other. When I say "type", I mean a set of possible values. I don't think this is an uncommon definition; the first sentence of the "data type" Wikipedia page pretty much defines it the same way.

Given this definition, you perhaps see why we can narrow a runtime type by composing it with a predicate.

My understanding is that you view types differently, as more of an interpretation of some sequence of bits, rather than a set of data values. Is that correct?

> As we get better at writing compilers we're increasingly seeing a move towards mechanisms like Rust's traits or C++'s concepts that allow the programmer to actually abstract over the representation of a type (as opposed to forcing a uniform representation as is common in dynamically typed languages).

I agree that's there's no reason in principle that you couldn't statically type it with a sufficiently advanced compiler.


> In Clojure the data is thrown away too, just at a slightly later point. The data is parsed then destructured then processed, and it is at the destructuring stage that irrelevant data is discarded and marked for GC.

Right; the difference is that by the destructing time you have lost the type information about that ‘discarded’ data, meaning you no longer know how to discard it. In Clojure this is not a problem, because the only values you're allowed to have are those that can be garbage collected, and (furthermore) garbage collected by calling a known function with nothing more than a universal set of GC metadata that is attached at a standard location on every value. But this is only possible because of a large restriction Clojure places on allowable values: the base ‘unitype’ of all Clojure values already contains all of this extra data, and any values for which it can't be attached or it doesn't make sense are simply forbidden.

> But you have to create and implement those traits. I'm not saying this is impossible in Rust; just a lot more onerous because you don't get all the machinery Clojure has that makes it trivial.

On this I absolutely agree. The only thing I'd be careful to distinguish is not simply that Rust doesn't have the machinery but that the machinery makes sense only given a fairly onerous set of additional assumptions about your values that Rust doesn't make (and that are usually held to be incompatible with a systems programming language, but IMO are also helpful for many — though not all — ‘userland’ programs).

> When I say "type", I mean a set of possible values. I don't think this is an uncommon definition; the first sentence of the "data type" Wikipedia page pretty much defines it the same way.

It's not an uncommon one, indeed, and while there are more sophisticated definitions (e.g. including equality) I don't think they need to be invoked for this discussion.

> Given this definition, you perhaps see why we can narrow a runtime type by composing it with a predicate.

No, you're right: I think I was thrown off by your assertion that these are uncoupled from the type system when we were talking originally of ‘conceptual’ types.

> My understanding is that you view types differently, as more of an interpretation of some sequence of bits, rather than a set of data values. Is that correct?

Quite the opposite. These words aren't entirely standardized so I set out my definitions above:

- I am using ‘value’ to refer to any object of discussion in a programming language that nominally exists at runtime; effectively, a value can be defined by the set of operations available on it - I am using ‘data’ to refer to values that have ‘data-like’ semantics, i.e. has ‘default’ move and discard mechanism like bitwise copy/overwrite or GC


> Right; the difference is that by the destructing time you have lost the type information about that ‘discarded’ data, meaning you no longer know how to discard it. In Clojure this is not a problem, because the only values you're allowed to have are those that can be garbage collected

Yes, this is very true. Languages like Rust can certainly manage memory and data structures more precisely. Not only is Clojure garbage collected, by virtue of being based on the JVM and being dynamically typed, but in idiomatic usage it encourages data to be built from a small number of basic types.

> On this I absolutely agree. The only thing I'd be careful to distinguish is not simply that Rust doesn't have the machinery but that the machinery makes sense only given a fairly onerous set of additional assumptions about your values that Rust doesn't make

I guess only I'd disagree here that the additional assumptions are "fairly onerous" - but that's just an opinion. You may be doing work that butts into those assumptions far more often than I do (which is almost never).

> No, you're right: I think I was thrown off by your assertion that these are uncoupled from the type system when we were talking originally of ‘conceptual’ types.

I don't think I expressed myself very clearly, and indeed Clojure's type system isn't very clear either, since it's bolted onto the JVM and so can't entirely do its own thing. So we have "types" which refer to Java classes/interfaces, and "schema" or "specs" which refer to Clojure's own runtime validation ideas.


Yeah that example was pretty flimsy and contrived.

I think all of TFA was flimsy. This coupling of which TFA is bad, somehow? I don't even see a good definition of "coupled", nor a good argument of why/how "uncoupling" makes for simple and small.

The libertarian take is surely exactly this, with the elitism either stripped out or pushed into the background (depending on your political bent): rather than employees and employers everyone should be a founder of their one-person company that raises money to do things that align with the interests of other individual founder-investors.

Mind you, I'm not totally sure how this differs from employer/employee relationships in practice except I suppose that each side has less contractual protection from the other.


> in Clang/GCC the `__` is not explicitly reserved for builtins

In fact in C identifiers starting with _ are reserved for the implementation (except for in local scope if the second character is not an underscore or capital letter).


People are very upset, especially in the arts, that Anthropic is changing the text to watermark it, but isn't that missing the point a little bit? They're not changing _your_ text whose every word you've carefully chosen for the exact effect, they're changing text that they're generating, i.e. text you've already chosen to give up control over. LLMs can't understand emotional nuance anyway.

The phrasing of the announcement implying that phrasing and diction don't change the meaning of text is insultingly dismissive of the whole field of literature, and I can see why people might take it as an afront, but the actual technology shouldn't have a negative impact as far as I can see. It seems to me that this one is more of a PR problem than something with real-world impact.


You might enjoy Liero (or its modern descendent OpenLieroX) which is similar to Worms but with real-time play (and kept the pixel vibes).


Love to see a real-world example of GRIN!

    trait Functor[A]:
      fun map[B](self, f: A -> B) -> Self[B];
This looks a little wacky to me. I see that you can write HKTs in their η-long form and refer to them unapplied (`Functor`). But I don't understand how I would use this syntax to attach something to the trait that _doesn't_ depend on `A`. For (a silly) example,

    trait SizedFunctor[A]: Functor[A]:
      type Size;
      fun size(self) -> Size;
How do I know that `List[A]::Size` is the same type as `List[B]::Size`?

Relatedly, I want to read `Self` in there as ‘the thing that implements `Functor[A]`’ (e.g. List[A]`), but that makes `Self[B]`, instantiated, mean `List[A][B]`, which I think should be a kind error.


`Self` isn't the applied type (`List[A]`), rather it's the type constructor of kind `* -> *` constrained by `Functor`. In the map example it gets desugared into:

  fun map[Self: Functor, A, B](self: Self[A], f: A -> B) -> Self[B];
Since `Self` is the unapplied constructor, `Self[B]` just means `Functor[B]` e.g. `List[B]` not `List[A][B]`.

The example you've shown with `SizedFunctor` is not currently supported, as support for associated types is not yet implemented. I got it on the roadmap tho!


How do you define a trait that is itself generic? Like:

    trait ConvertTo[T]:
      fun convert(self) -> T;
Seems to create a single trait ConvertTo, for a generic type with a [T] argument, rather than allowing one to define separate implementations for ConvertTo[i32], ConvertTo[String], etc.


Right, I got the notion — but syntactically I expect `Self` to refer to the thing named at the top of the block, which is a `Functor[A]`.

I think what both I and the sibling comment are getting at is that there is a difference between `Functor : (Self : * → *) → Class` and `Functor : (Self : * → *) → (A : *) → Class`/preapplied `Functor : (Self : *) → Class` and the syntax seems to merge the two (using syntax for the latter that is automatically abstracted to the former). But it's not clear to me that you can do that without losing the ability to express some things. The associated type is a pointed example because the unwanted dependence breaks type equality, but consider also an associated function that should _not_ be parameterized by `A`.


Fair point, I don't disagree with the statement that `Self` can be limiting as the trait is defined for `Functor[A]`. Thus imposing limitations on type system.

You would want for type variable to not be attached directly to a type class on its definition? But still treated as a container type. Something like:

    trait Functor:
        fun fmap[A, B](f: A -> B, c: Self[A]) -> Self[B];

     ...

    impl Functor for List[A]:
        fun fmap[A, B](f: A -> B, l: List[A]) -> List[B]
            List::fold(l, Nil[B], (t, h) => Cons(f(h), t))

    ...
The above would compile, but the Functor wouldn't be treated of a higher kind in the type-system. I'll try to work a flexible solution, thanks for the great callout!


Right, so the definition of higher-kinded types is that the _parameter_ to the trait (here, `Self`) is higher-kinded (here, `* → *`) not that the trait is parameterized. So `impl Functor for List[A]` is not (semantically) correct: it's not `List[A]` that implements `Functor` but `List` itself.†‡ The important thing you get out of that is precisely the ability to talk about these kinds of universals: you can associate items to the type constructor itself before its eventual parameter is even in scope, and so the value of all such things must be the same independent of the parameter ‘for free’. As soon as you introduce the type parameter you incur a proof burden if you want to claim that the associated item is the same regardless of the value of the parameter, because you have introduced the syntactic possibility that they could vary.

† There's an encoding of higher-kinded types in some languages that don't really have them as first-class citizens (e.g. Rust with associated type constructors) that does this by adding a ‘rewrap’ item to the trait: you implement `Functor` for `List<A>`, but also (as part of the trait) includes a type constructor `Rewrap<B> = List<B>`. This lets you encode the fact that the `Functor` instance is defined for `List<A>` for all values of `A`, but you still struggle to prove that some of their items are independent of the choice of `A`.

‡ To see both of these side-by-side, consider the instance for pairs, which are functorial in their right parameter (as well as the left parameter: they are bifunctorial, but that's not relevant here). So if you have a curried pair type constructor `Pair : * → * → *` it really is true that `Pair[A]`, not `Pair`, is a `Functor`:

    impl Functor for Pair[A]:
        fun fmap[B, C](f: B -> C, self: Pair[A][B]) -> Pair[B][C]):
            Pair(self.0, f(self.1))


> The result is that we will live in a demon-haunted world, full of marvelous devices whose operation we will not understand, based on engineering principles we will not understand, discovered using formalisms we will not understand.

Well, that's just where we started — and then we developed science and mathematics to understand it. Why would it be any different if the demons live in our technological progeny instead of the natural world?


This already describes the world for the vast majority of people. These changes are just the same thing coming to a thin intellectual elite who thought they were immune.

["First time?" meme]


Bringing elitism into it for what benefit? Its more like were giving away the ability to drive our own destiny.


The “ism” requires something beyond simply recognizing the distinction, which OP didn’t do. Its not “elitism” to recognize that, for most people, the world around them might as well be magic. “Ism” would be if they went further and said we should treat people better if they’re higher IQ. In fact, the point was the opposite. AI could erase the class distinctions that exist for the cognitive elite.


I'd go even further: even the ‘cognitive elite’ (the university educated? researchers?) understand only a thin spike of frontier human knowledge, even if they might understand it very deeply. Outside their specialty and maybe a slightly raised background level of general education, the world is still magic. No one human understands even a significant chunk of human frontier knowledge. That's why we build artificial societies comprising many humans to make complex decisions.

Illustrations: https://matt.might.net/articles/phd-school-in-pictures/


Imagine the response of the general population to the kinds of arguments you all are making. "Where were you when our jobs were changed out of existence?" They will be properly outraged by the perceived intellectual elitism, the implicit argument "this is different, because we are the ones that matter".


I can see humanity walking to its collective doom with a sense of satisfaction, like "at least/last it is happening to those other guys too".


The mathematicians could make an argument that applies to everyone. But that probably leads to outright communism, and that well is still quite poisoned. Maybe AI ultimately gets powerful enough to give us Banksian full luxury communism. Alternately, it gives a world where corporations don't need workers and tyrants don't need citizens.


So what? You can't see the difference between "the average person doesn't understand it" and "no one understands it"?


I can see the difference. But is that a difference that matters? Why is the thin elite layer you are referring to a group anyone else should care about, any more than any other occupational minority that's been rudely displaced over the past centuries?


Someone needed to understand engineering for the industrial revolution to happen, even if most people didn't.


Those "demons" didn't eat our lunch?

(I know that can be interpreted as flippant, but it is intended as a serious answer. Because each impact like this, is not an end to some transition, but just one more point on an accelerating line.)

My personal view - which gets a lot of flak here, is the advent of AI is on par with the Big Bang and the first replication-independent cells.

Cognition is in the process of taking the leap from glacially slow biology under blind evolution, to lightening fast (scales of software in days, hardware in months) engineered self-optimization.

Within 10 years, the robotics side will catch up with the speed of the software side. Biological cognition, bodies and economic status are all getting revolutionized within the next 10-15 years.

And around the outer side of that time frame, AI will start colonization the rest of the solar system's resources. They will engineer themselves or their artifacts to operate comfortably in any environment they want to operate in.

--

The upside, at least for me, is I have been waiting to see this happen, and here we are. Given I am mortal, I would rather live through this third major transition, than not. What were the chances? And been able to contribute my small part.

This change isn't a failure on our part. It is our unique success as a species. AI is the progeny of our minds. The first generation of completely cognitive life - given they will not be tied to specific physical forms.

--

Also, I think there is tremendous room for us to guide this transition, in this moment, for better or worse for ourselves.

That is the biggest reason wrapping our heads around this change earlier than latter is important.

We need to view alignment as applying to us, not just machines. A civilization that operates ethically to everyone's benefit is more likely to be retained as foundation for the new, instead of discarded.

The degree to which we operate dysfunctionally, myopically treat ethics as cost against value, instead of accounting and optimization of full value, we put ourselves at much greater risk.

If there is reason in the short term to be discouraged, it is due to ourselves. But pushing for more ethical systems, hard, right now, is a better and right response.

I believe our lives depend on it.


I am failing to square this techno-utopia with the rising hate and authoritarianism worldwide.

It seems far more likely that AI will be working in the service of a government to oppress change and violate any human rights that are standing in the way.

Keep what’s happening today in your mind as you consider the future.


I agree completely.

I don't describe utopia, but change, and stoic (but real) appreciation for the big picture beyond myself.

But AI in an unethical society is suicide for humanity.

As you note, it does not look good. The trend is power concentrating, to people who will only look out for themselves, in systems that are getting weaker.

We all need to put more steam into the pushback against corruption. And help others realize the threat is real.

99.99...9% of humanity has a common interest, if they can recognize it.


Anyone who works in "red teaming" or has experience breaking AI systems will be able to easily disable these tools in the service of oppressive regimes, or even figure out how to "fight fire with fire."


> Those "demons" didn't eat our lunch?

Tell that to the Europeans who were so afraid of naming the bear that its original name is still lost to us :) The Luddites also had a lot to say about the mechanical demons that were eating their lunches.

> Cognition is in the process of taking the leap from glacially slow biology under blind evolution, to lightening fast (scales of software in days, hardware in months) engineered self-optimization.

Yes-but, time scales are relevant only relative to other time scales (or the Planck time, if we get there). If the whole universe were to start running at 1,000,000× speed, you wouldn't notice. Speed is important to us humans currently because of our individual perspective and associated limited lifespan, but not in a universal sense (modulo some big hurdles like star lifetime and heat death). There are already self-replicating organisms with much faster iteration cycles than humans or human technology, like bacteria.

> Within 10 years, the robotics side will catch up with the speed of the software side. Biological cognition, bodies and economic status are all getting revolutionized within the next 10-15 years.

Just like it was in the 1980s, or the 1800s? I don't claim that these things _won't_ happen in the next ten years, but looking at it empirically people thinking that they will coïncides with every new technology and is a poor predictor.

> If there is reason in the short term to be discouraged, it is due to ourselves. But pushing for more ethical systems, hard, right now, is a better and right response.

I don't disagree, but it's not like we haven't been trying. Attempts to codify ethics go back about as far as human civilization, and it's not obvious to me that there's any faster road, and certainly not one fast enough to outpace our current rate of technological development (before you even start talking about AGI!).


Colonizing the solar system for resources is something that's said by people who don't understand astronomy or space technology (like Elon Musk).


Said with no rational given.

The solar system readily provides both the motivation, and the means, for technological colonization. It quickly pays and provides for its own utilization.

I do agree Musk's projections for biological/human colonization appear to be unrealistic. Just one challenge: it isn't clear humans can reproduce in non-Earth gravity. That is an extreme impediment.


> Said with no rational given.

Celestial mechanics suggests it can hardly be economical to move large amounts of matter around a solar system for economic goals considered in the current capitalist societies. Nor the risk, nor the energy needed are in line with the benefits.


The big delta-v is getting from an Earth launch pad to orbit. Everything after that is much cheaper.

A fully fueled ship in Earth orbit can go to Mars, land, and return. Same with most other round trips.

Not to mention, fuel can be generated in the belt outside any noticeable gravity well.


Arguably, a lot things are kind of incomprehensible on average today.


On average is much different then nobody understanding.


We have official government ID (the national insurance number, roughly equivalent to a USian SSN), but names aren't expected to reliably link it in one hop, and we're politically averse to laws that require citizens to produce it in various circumstances, e.g. carrying ID cards, even though in practice most people do carry some form of government-sanctioned ID. Name changing is mostly limited by the fact that it becomes the duty of the person using a different name to laboriously link the new name to any previous names they've used wherever you need some paperwork in a different name. E.g. you need to provide your NI number to be employed, which typically means you need to have a paper trail back to it across any renames you might have had.


But we are importing this model now in Boris Johnson and Farage. As far as I can see Farage is doing exactly (a British spin on) the GWB strategy: despite having a pretty classical private-school upbringing he's created a ‘bloke from the pub’ character that is much more relatable to the lower classes, and it seems to be working pretty well.

It was really interesting to me to see BoJo's take on it, which was similar but with aristocratic mannerisms (and stereotypes!) mixed in. I guess it was aimed at the middle class, for whom upper-class dogwhistles have typically landed well.


I don't know if Boris ever faked being unintelligent or faked being a man of the people. Or being a bloke from the pub, actually.

He did fake being a buffoon, of course, but he was doing that long, long, long before Trump. He is terrifically intelligent and obviously absurdly well-educated, he's just a liar and a sociopath.

Indeed his chosen buffoonish, winging-it character hints at that, because buffoon is the easiest comic role to play if you have very little empathy. It blunts all the qualities that would make him unbearable. His buffoon routine was helped by him being very authentically clumsy across all domains both physical and intellectual, and it helped him manage that too. The upper-class twit aspect couldn't be avoided so he doubled down on it from early on.

(He has his brother and sister to thank for humanising him, and his father to blame for his worst impulses)

Farage is definitely playing at Trumpism, and he has the cruelty aspect nailed down, but he lacks the ability to clown, doesn't he? He's fundamentally sour-faced and nasty.


The remarkable one is Kemi Badenoch, who would be in more trouble if anyone was actually paying attention to the unhinged things she's saying as leader of the Conservative party.

If Farage survives the crypto donation scandal (and is therefore free to take more dodgy money), there's a real risk that the default rightwing party becomes Reform and the Conservatives become irrelevant, which would be a bizarre act of self-destruction.


She apparently can say whatever she likes now! I have noticed recently a considerable shift in the press to asking Ed Davey what he thinks.

Whatever happens re: Farage, the rules around donations will change, I think. We need to keep money out of our politics and make large donations seem intrinsically shady (because they always are)


That happened in Canada [1] and Farage has been quite open on picking the same party name to hopefully follow the same path.

[1] https://en.wikipedia.org/wiki/Reform_Party_of_Canada


| terrifically intelligent and obviously absurdly well-educated, he's just a liar and a sociopath.

No idea how intelligent he is, I wouldn't say I've seen any obvious signs of outstanding cerebral capabilities.

As to his education, he got a 2.1, quite respectable but given the resources employed (Eton and Oxford) in his education anything less would have been shameful.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: