10
$\begingroup$

Taking this question to its logical extreme. Some languages like C++ and Python use class for everything. Python classes are classes, interfaces (both nominal and structural), mixins, and namespaces, all rolled into one keyword. Other languages tend to split these things out, using different keywords for different concepts. You see interface crop up a lot, for defining behaviors abstractly without allowing the interface to be directly instantiated. Some languages have others, for instance Raku roles are mixins, and Scala and Kotlin have singleton objects baked in.

What are the different class-like constructs that are seen in programming languages today? What do they do differently, and what motivates each one?

$\endgroup$
1
  • 1
    $\begingroup$ ML-style modules. Originally, modules were second-class citizens in ML-family languages. This was done in order to enforce predicativity of module types. Over time, ML-style modules evolved, see e.g. Ocaml modules (now first-class citizens) and Scala's singleton objects. $\endgroup$ Sep 17, 2023 at 22:28

1 Answer 1

9
$\begingroup$

Here's my attempt at categorising them. I've tried to cover the most significant differences between these constructs in different languages, and I've tried to choose language-agnostic terms; but those are somewhat subjective considerations, so take it with a grain of salt. This will also necessarily be biased towards constructs in languages I'm familiar with.

Structs

Probably the simplest composite data type; it consists of a set of fields, each of which has a name and a type. The only things you can do with them are construct them (by providing values for each field), and read or write fields by name. Generally, all field accesses are static, meaning it's known at compile-time which field an access is bound to; you can't do things like my_struct[some_field] where some_field is itself a variable, at least not unless the language supports some kind of reflection.

Generally, structs are value types, but languages which support them usually allow taking references to them too; and they are nominal types, so two struct types with the same field names and types aren't compatible.

Structs may have functions associated with them, which might look like methods if the language supports Uniform Function Call Syntax (UFCS). However, I do not classify them as methods since they are not dynamically dispatched based on the subject's runtime type.

Tuples (product types)

These are similar to structs: the most obvious difference is that the fields are numbered (or ordered) rather than named. However, there are a few more subtle differences.

Tuple types are almost always value types, and structural types, meaning that two tuple types are compatible if their respective component types are. They may also be anonymous types. A tuple type may be representable as e.g. S * T or (S, T) where S and T are themselves types; this means that generic functions can operate on tuples in ways that they would not be able to on structs.

I don't know of any languages besides Rust which allow associated functions and UFCS with tuple types.

Records

In many contexts a record is a synonym for a struct. However, at least to me the term "record" has a few different connotations to term "struct". For one, records might be comparable for equality (by comparing their fields) by default, and they are normally used for things where such comparison would make sense. If two record instances happened to have equal fields despite not logically being equal (in the domain), then normally the programmer would add an explicit "identity" or "ID" field in order to differentiate them. So records are value types but they are often used to model things which have identities.

Record types might be either nominal or structural. If they are structural, then there is probably a subtyping relationship between records which have strictly more fields, and possibly when the type of a field in one record is a subtype of that field's type in another record.

Classes

There are several major distinctions between classes and structs, mostly relating to the fundamental principles of Object-Oriented Programming. Like structs, they have named fields, and field accesses are (usually) either statically bound, or at least refer to statically-known field names.

The most significant feature of classes is that they can have methods. The difference between a function and a method is that a method can be overridden, so there can be multiple implementations of a method, and when the method is invoked, it is dynamically dispatched (i.e. an implementation is selected at runtime) based on the subject's runtime type.

Classes also have constructors or initialisers ─ special methods (or method-like members) which are invoked when the class is instantiated. Whereas structs can be constructed as data by just providing their field values, class constructors might provide their own values for some or all of the class's fields, or perform other behaviour.

Generally, classes are reference types ─ instances with equal fields still have different identities ─ and they are nominal types.* They can be extended by subclasses, which inherit members (fields and methods) from their superclasses. (This could be single or multiple inheritance.) Class inheritance also implies subtyping: subclasses are subtypes of their superclasses.

Particularly in statically typed languages, there is likely to be some kind of visibility feature for class members, allowing them to be declared as public or private (or possibly other visibilities).

* I don't know of any languages besides Typescript in which classes are structurally typed. Even in Typescript, structural compatibility of two classes unrelated by inheritance is almost always unintended, and a source of potential bugs.

Dynamic classes

These are almost always just called "classes", but I think it's important to distinguish the classes in languages like Javascript or Python from the classes in languages like Java or C#. Dynamic classes actually exist at runtime and are values in their own right. This is in contrast to e.g. Class<?> instances in Java, which represent the class but are a different language construct distinct from the class itself.

Dynamic classes are usually mutable: they can be monkey-patched, changing the behaviour of their instances, often even instances which had already been constructed before the monkey-patch was made. New dynamic classes can be declared at runtime, and they might extend other dynamic classes without necessarily knowing at "compile-time" which other class they extend.

Mixins

These are similar to classes, in that they can declare fields and methods, and can inherit members or be inherited from. The difference is that a mixin cannot be constructed directly, and it might not even actually be a type (rather, it is only a blueprint for a set of members).

Mixins may be a separate language construct from classes in order to provide a mechanism for multiple inheritance, if the language only allows single inheritance for classes. In this case, a class can inherit from any number of mixins but may only (directly) extend one other class.

Dynamic objects

These are objects, generally in dynamic languages like Javascript or Python, which allow fields to be added or removed, or their types changed, at runtime. All accesses (fields and methods) are dynamically dispatched, since it isn't necessarily possible to know statically whether a particular object will even have a particular member.

They are always reference types. They might support inheritance from a dynamic class or a prototype, or not at all. In the latter case, they are constructed by directly listing their members. It is often possible to access fields dynamically, e.g. my_object[some_field] where some_field is itself a variable, or to iterate over an object's members, without needing a special API for reflection.

Dynamic objects only exist in dynamically-typed languages, so duck typing with them is common. Such languages sometimes later introduce an optional static or gradual type system, but these are generally not powerful enough to describe every way that dynamic objects are used in practice.

Interfaces

An interface is a data type that is not a data structure. Typically it is declared similarly to a class, but all of its members are dynamically dispatched. For example, in a language where class field accesses are statically bound, interfaces probably would not be allowed to declare fields.

Unlike concrete data types which can be instantiated directly, interfaces are implemented by other types (particularly classes), so to get a value of an interface type you must construct a value of some concrete type which implements the interface. Interfaces may be nominally or structurally typed; i.e. the language may or may not require you to declare explicitly which interfaces are implemented by a class.

An interface which declares methods generally does not provide implementations of them; those are instead provided by implementing classes.

Traits

These are quite similar to interfaces, in that they are data types but not data structures, they are implemented by other types, and the trait declaration generally doesn't provide implementations for its own members. However, at least to me a "trait" does not necessarily mean that its members must be dynamically dispatched, and this means traits can be implemented by structs or other types which don't have any dynamically dispatched members.

Still, since a trait may be implemented by multiple types, this means that when a trait "method" is invoked, an implementation needs to be selected. This could be either by monomorphisation of generic functions at compile-time, or by dynamic dispatch at runtime (Rust, for example, supports both).

$\endgroup$
8
  • $\begingroup$ This is a comprehensive answer, and I'd like to point out that languages which have classes sometimes also define metaclasses, which are useful to distinguish between "what" is implemented and "how" it is implemented. I however don't know if that makes an answer on its own. $\endgroup$
    – coredump
    Sep 18, 2023 at 15:07
  • $\begingroup$ @coredump Fair point; I didn't count metaclasses since I only know of them in Python. Python's metaclasses are really just classes, and it's the "A is the metaclass of B" relationship that distinguishes them. In that sense the language construct is really a relationship between classes rather than an alternative to them. Similarly, Java has inner classes, which are a kind if class and it's the relationship to the outer class that distinguishes them. $\endgroup$
    – kaya3
    Sep 19, 2023 at 10:02
  • $\begingroup$ Interfaces are called "abstract classes" in some languages. $\endgroup$
    – Barmar
    Sep 20, 2023 at 21:29
  • 1
    $\begingroup$ @Barmar In all the languages I'm familiar with, abstract classes are classes which don't necessarily provide implementations for all of their members ─ as opposed to interfaces, which don't (generally) provide implementations for any of their members. Which languages are you referring to? $\endgroup$
    – kaya3
    Sep 20, 2023 at 21:32
  • $\begingroup$ True. Abstract classes are generally used for all the different class-like things that can't be instantiated by themselves. So interfaces, traits, mixins. Interfaces are abstract classes that don't provide any implementation, just declarations. $\endgroup$
    – Barmar
    Sep 20, 2023 at 21:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .