Hacker News new | ask | show | jobs
by INTPnerd 2295 days ago
The main benefit of classes is as a way to define custom types. Otherwise you are stuck with the types built into the language, which are not designed to help ensure correct program state, logic, and behavior. When you use classes in this way, I call it type oriented programming (TOP). Why would I compare classes to types? Just pass in the initial value at construction time, define the "operations" on that type by creating methods for your class, and define which other types those operations work with by setting the types of parameters those methods work with. Make the class immutable, always returning a new instance with the new updated value passed into the constructor. Why did I mention these types helping the program be more correct? This should be used as the primary form of contracts. But these contracts are very simple to use, you just specify the relevant type in the parameters and return values, and you are done defining the contract for any given method. For example, let's say a method should only work with integers larger than zero, instead of either accepting an Int or an UnsignedInt, which would allow 0, you could define your own class called PositiveInt. It would be designed to throw an exception if you pass anything smaller than 0 into the constructor. Then instead of writing code inside the method that makes sure the user of the method is following the contract, you just specify PositiveInt as the parameter type. If the contract is violated, the exception will be thrown as early as possible, before the method is even called, helping programmers catch the original source of the problem. This also makes your code more readable, because you can see exactly what each method accepts and returns just by looking at the method signature. When you start thinking this way, you will notice many core types are missing from the language, that should have been there from the beginning. Fortunately you now know how to build them yourself.
8 comments

> The main benefit of classes is as a way to define custom types. Otherwise you are stuck with the types built into the language

I'm going to stop you right there, because plenty of languages (especially functional ones) have ways of declaring custom complex types without using classes.

A simple TypeScript example:

    interface DuckTypedObject {
      quack: true
      bark?: false
      eyes: 'beady'
    }
This will require that any object used as a DuckTypedObject must have the `quack` and `eyes` properties and may optionally have a `bark` of `false`, but doesn't otherwise prescribe what the object actually has to be.
This is the best part of TS IMO (structural typing).
agreed 100%. structural, statically-checkable typing of objects is the best thing to happen for reusable and testable code in my programming lifetime.
Sorry to be pedantic but that isn't quite right. I do use the same technique myself quite a lot with the options pattern.

However it is worth keeping the following in mind. The compiler will check that in your code that these properties are present and assigned correctly. However at runtime nothing is guaranteed especially when dealing with the DOM.

One of the things that I don't like about TypeScript (I have written a fair bit of it) is that you believe you have type safety when it is really type hinting.

Validation functions are pretty easy to write, as long as your data is being handled in a predictable way.
Yes. However you can for example do the following in typescript:

    function someFunction(obj?:DuckType) {
        //Snip other logic
        someOtherFunction(obj);
    }

    function someOtherFunction(obj:DuckType) {

    }
The type checker catch that. So you still have to do:

    function someFunction(obj?:DuckType) {
        obj = obj || { /* set some object properties */
        //Snip other logic
        someOtherFunction(obj);
    }

    function someOtherFunction(obj:DuckType) {

    }
I have found plenty of examples where people haven't set a default value because the compiler hasn't flagged anything wrong with the code and you get an uncaught reference error.
Huh, shouldn't the things on the right hand side of the colons be types not values?
Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero? Do they also allow you to define your own operations on those types? That is the part where classes seem to be a good fit, methods are basically operations supported by a type.
> Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero?

I'm not an Ada expert, but it has excellent support for range-restricted integer types.[0]

Ada's 'discriminated types' are also fun. They let you create members which only exist when they're applicable. [1]

> Do they also allow you to define your own operations on those types

Looks like Ada supports operator overloading, yes. [2]

[0] https://www.adaic.org/resources/add_content/standards/05rm/h...

[1] https://www.adaic.org/resources/add_content/standards/05rat/...

[2] https://www.adaic.org/resources/add_content/standards/05aarm...

Ada is also great at separating the various aspects of OOP into separate language concepts, instead of going "everything is done with classes!!" as many popular languages do, leading to a lot of confusion in this thread.
A typical way to handle this in a functional language would be to create a datatype with a name like "PositiveInt", which just contains an Int inside it. However, in a language like OCaml, you can make it so that users of this type cannot directly create it, and instead must use a function like "makePosInt", which would check that its argument is positive, then give you back a value of type PositiveInt containing your data.

I'm not too experienced with this though, so this is pretty much the extent of my knowledge on this topic.

Rust is excellent for this. You can define:

    struct PositiveInteger(u32);
Then give it a constructor (which in rust is just a regular static function) that checks the non-zero variant. You can define method on this type, and make it implement traits (which are kind of like interfaces).

The best bit: there is zero runtime cost to this, the memory-representation of this type is identical to that of the underlying u32.

Rust-style enums which can contain data, and also have method implemented on them are even better. Doesn't mean classes (structs in Rust) aren't useful, but once you use a language that allows you to define other kinds of custom types, they seem very restrictive when they're the only available tool.

That sounds like a class.
Yep. This whole discussion just seems like a C vs C++ styleguide slapfight.

Having the functions that operate on the struct attached directly to the struct declaration, vs having some functions that the first parameter is the struct on which the function operates, doesn't seem like a particularly meaningful distinction to me. OK, you like C-style programming in favor of C++-style programming, congrats. It's still a class either way.

The distinction you describe is not meaningful, but the key feature that separates classes from other forms of code organization/polymorphism, like typeclasses as in Haskell/Rust, is not that. It is inheritance.
I guess it's kind close to a C++ class. It's pretty different to a class in a language like Java, because all classes in Java are heap allocated and behind references.

Enums are are the better example of non-class types. For example, you can have:

    enum StringOrInt {
        String(String),
        Int(u32),
    }
And you can go ahead and implement methods on that type. Classes have "AND-state", not "OR-state". But a Type in general can have either kind of state.
That's equivalent to wrapping an enum in a class. Emulations of type hierarchies without OO often fail like this, having a A-or-B be literally the same type so losing out on type safety/forcing constant rechecking of the discriminant.
It's a product type[0], containing a single inner type of `u32`. Sometimes classes look like product types.

[0] https://en.wikipedia.org/wiki/Product_type

Data types are not classes.
Sum types and typeclasses in Haskell (or enums and traits in Rust) are a much better fit than classes and the mess that is operator overloading.
Thanks! These definitely sound like what I should explore next in my journey.
I believe you're looking for dependent types: https://en.wikipedia.org/wiki/Dependent_type
You actually don't need types or classes to do this. You could use design by contract, which is what I've done in Python and Perl, neither of which have very fancy type systems compared to something like Haskell.

With design by contract you can put in whatever fancy constraints you want on function parameters and return values, and those will be enforced.

As far as objects go, they're much more useful for me as just a means of passing state. Rather than using a bunch of global variables or having to pass in a ton of function arguments, I can just use an object which contains all the state I need.

Of course, having lots of state can usher in its own set of problems, and there's something to be said for trying to make your code as stateless as possible. But sometimes you need state, and maybe even a lot of it.

I use C structs in the same way. I think it's fine to use classes for this purpose. The problem with classes is that certain people go crazy with inheritance and polymorphism, things which sound smart on paper but almost always lead to horrific unmaintainable code in practice.
Yeah, basically the lesson of the last 20 years of Java and C# and so on is that inheritance sucks and what you mostly are after is composition.
This is a good point. Right now Kotlin is my favorite language. By default classes and methods are closed for inheritance, and therefore for polymorphism. I like the way this is the default, but you can override it when/if necessary. It also has data classes, which in my opinion is what most classes should be. I think when a class is not be a good fit for data classes, this is a code smell.
An excellent article on type-driven design and development was posted on HN [1][2], as darkkindness put it "Encode invariants in your data, don't enforce invariants on your data"

[1] https://news.ycombinator.com/item?id=21476261 [2] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

> For example, let's say a method should only work with integers larger than zero

I dislike this example. The numeric systems of every programming language I've ever used has been (more or less) terrible, precisely because there are extremely common and simple arithmetic types, just like this, which it's terrible at representing. Half of the "modern" languages I've seen just provide the thinnest possible wrapper around the hardware registers ("Int32"!).

(What if I need to accept an integer in the range 5 < x < 10 instead? Am I supposed to define a new class for every range?)

Instead of saying we need a system of user-definable classes so every user can fix the numerics on their own, for each program they write, I'd say we should fix our numeric systems to support these common cases, and then re-evaluate whether we really need classes.

Are there non-numeric analogues to this type of value restriction? Maybe. It doesn't seem like a very common one, but it is an interesting one. Perhaps what we really want is "define type B = type A + restriction { invariant X }". I can't think of any examples offhand in my own work, but that could be because I haven't had that language feature so I wasn't looking for places to apply it.

I guess it comes down to an unresolved duality.

One being the idea of types as describing shape of data. In the best of cases perhaps some semantics tied to the data (how the bits are to be interpreted)

Than there is the the other view, the Curry-Howard one. Where types describes proofs and invariants of the program it self, and how interesting properties of program compositions can be ensured.

It seems much time is wasted when people holding one perspective debates with people holding the other.

Perhaps we should have separate words for these concepts.

Not only were custom types invented decades before classes, but the languages with the strongest emphasis on type safety tend to either lack them or consider them to be unidiomatic.
Which languages would you say are really good for defining your own types? Would they be a good fit for the example I provided where a function needs to accept integers larger than zero? Do they also allow you to define your own operations on those types? That is the part where classes seem to be a good fit, methods are basically operations supported by a type.
For the integers larger than zero example, I'd say the earliest archetype for a good design (that I know of) is Ada, which lets you declare a type with a limited numeric range like so:

  type MyType is range min .. max
There's already a built-in for positive integers, which is defined as

  subtype Positive is Integer range 1 .. Integer'Last;

Note the subtype there. Ada recognizes that a positive integer is a type of integer, but not the other way around. And it enforces that in the type checking: You can pass any Positive into a function that accepts Integer, but you can't just pass an Integer into a function that accepts Positive. This happens even though they're not classes and this isn't OOP. Ada does have object-oriented constructs, but they are a later addition to the language. I have never used Ada professionally, but my understanding (based on book learning) is that it tends to be used conservatively.

It's similar in OCaml. Despite the O standing for "object-oriented", creating classes isn't necessarily considered idiomatic. The other tools in the chest tend to be conceptually simpler, and therefore to be preferred when they will get the job done.

"define your own operations" is a requirement I'm having a hard time making sense of. To me, that is just another way of saying, "define functions", which is a feature of every language I've used except for one really ancient dialect of BASIC.

If I recall correctly, Pascal would also let you define a ranged integer, long before Ada.
Thanks, looks like you recall correctly.

Every so often, I wonder if I should spend some time with Pascal.

You can go with more of a Haskell/Rust style approach, where you declare a type, and then define operations using either plain functions, or using something kinda similar an interface (typeclasses in Haskell, traits in Rust).
> When you use classes in this way, I call it type oriented programming (TOP).

Watch out, you might make topmind mad! https://wiki.c2.com/?TopMind

In my experience, custom types often turn out either being so restrictive as to be less than useful or being leaky abstractions. (This might be an example of just where a healthy engineering compromise is unavoidable.)
have you heard of structs?