7

According to Wikipedia the Liskov substitution principle states that

objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program

Normally the Liskov substitution principle doesn't apply to classes because they are not objects.

Now in Ruby, classes are objects too. The most basic class from which all other classes inherit is BasicObject. BasicObject allows the .new class method (the "constructor"; it allocates an object and executes the instance method #initialize on it) to be called without arguments.

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments, because the subclass is an object and if it wouldn't support it, the subclass wouldn't be a substitute for it's parent class. If applied literally, one consequence would be that every constructor has to support being called without any arguments.

This seems odd. But I wonder if there is any point in going into that direction? Or is this just an incompatibility of the Liskov principle (which assumes that the set of all objects and the set of all classes are disjunct) and Ruby (where the set of all classes is a subset of the set of all objects).

This strangely reminds me of Russell's antinomy, a paradox, which, as far as I understand, was resolved by separating classes and instances (make them disjunct).

4 Answers 4

3

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments

Your suspicion is wrong.

The problem is that you're inferring from the base class's implementation what the base class's contract is. But they are two distinct things, even if not literally written out separately. In this case, the base class's contract is that you can construct things with new, and if the arguments are incorrect, you may error (e.g. throw).

The fact that in the base class, zero arguments is acceptable, does not immediately require that in the contract, zero arguments is acceptable and must be allowable by all derived classes.

It's merely a default implementation of a contract. It may permit things outside that contract. It may offer things that are optionally part of that contract. There's no strict 1:1 relationship between the base class's implementation and the contract. You can't assume that just because the base class's implementation permits something that it is immediately a mandatory part of the contract.

However, in answer to the most general part of your question, then yes. Class metatypes are instances like any other and obey the same rules as any other inheritance hierarchy, including LSP.

3

Languages vary greatly in how classes are manifest (i.e. at runtime). In some language systems,

  • there is no runtime manifestation of a class, so Liskov is simply not applicable to classes.

  • the runtime manifestation of all class is a direct instance (of some class like TYPE or CLASS, for example), and all they are is a token that represents a specific real class, but otherwise don't participate in subclassing themselves. So Liskov still doesn't really apply (as these are just tokens, i.e. instances of just one class rather than from a class hierarchy).

  • Yet in other systems, the runtime manifestation of the class is an instance of a formal metaclass mechanism. In such a language system, we would expect to allow user specification of metaclasses, and further then allow normal user-defined subclassing among metaclasses.

In such systems as the latter, we would expect normal subclassing rules and issues to apply. In those systems we could create metaclass subclasses that ought to follow Liskov, just like we can create regular subclasses that ought to follow Liskov. (See note below on definition Liskov.)

Further, in some such systems, the instance methods of a metaclass provide the static methods for its classes; which makes for a pretty regular form in that: adding an instance method to a base metaclass would make that instance method available to that metaclass' subclasses, the therefore make those method available as static methods on any instances of classes of those meta-sub-classes. As always when subclassing, Liskov can properly be followed or not.

I don't know which of the above categories (if any) Ruby falls under.

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments, because the subclass is an object and if it wouldn't support it, the subclass wouldn't be a substitute for it's parent class.

I agree. (but that doesn't necessarily mean Ruby does!)

If applied literally, one consequence would be that every constructor has to support being called without any arguments.

It's not that every constructor would have to be callable without arguments, but instead that every class has to include a parameterless constructor. (Many languages allow a given class to have multiple constructors with different signatures. I don't know about Ruby.)


NOTE: Remember that Liskov is about more than method availability in subclasses but also about the expected behaviors of those methods among the base and subclasses. I don't think you could have Liskov in a subclass that didn't provide the base class' methods, so I would think that doing so is a Liskov prerequisite, yet while necessary, providing the same methods and signatures is not sufficient for Liskov (runtime behavior must also match). (A number of languages will enforce at compile time via their type system that subclasses provide base class methods; but I don't know of any languages the enforce the expected behavior is proper as that is substantially more subjective.)

For example, a subclass that provides the base class method, yet always throws an exception (e.g. method not allowed, or unexpected) violates Liskov (assuming that more normal behavior is expected in the base or other subclasses).

1
  • 1
    "Many languages allow a given class to have multiple constructors with different signatures. I don't know about Ruby" – Ruby doesn't have constructors. Class#new is a method like any other. Apr 12, 2016 at 0:18
1

The right answer involves a lot of, "Not quite," with a sprinkling of, "You have a point."

First let's take a literal interpretation. Ruby has class methods and instance methods. Class methods are the methods of the class object. Instance methods are the methods of instances of that class. Inheritance is a relationship between class objects - calling .superclass on the one class gives you another. However that relationship between classes is NOT strictly speaking a subtype relationship. And therefore the Liskov Substitution Principle applies only to instance methods, not class methods. So having different class methods for classes in an inheritance tree is not a violation of the principle.

You can demonstrate this to yourself by creating independent subclasses of Class, create an object foo of the one class. Create an object of the other class, using MyClass2.new(foo) when you create it to make it inherit from the first. And now you see very explicitly that one class can inherit from another class while those class objects are different types of things.

That's the "not quite" answer. Now for the sprinkling.

In the original usage, a "type" is a contract for what operations are supported on an object. In statically typed languages, the contracts are explicit - the compiler will not let you try to pass an object of the wrong type to function that knows what it wants.

However Ruby uses duck typing. You can pass anything anywhere, and as long as it responds to the right methods or does the right thing, things will work out. This makes the contract of a "type" implicit, not explicit. In particular "type" does not always mean class. (It often does, but not always.) And the real relationship of a type is based on what operations are in the code.

So if your code specifies a contract for what a plugin does, any class that implements that contract is of the right type. If your code does meta-programming and calls .class to get the class, then starts doing things with the class, class methods on the object ARE part of what makes a type a type. If your code uses introspection to discover all classes whose name follows a given convention and does something with them (people do stuff like this with unit test frameworks) then the naming pattern of the class IS part of your type.

Think of this as, "A type is a contract, and the contract is implicitly defined by what operations are in the code."

If you think about it that way, Ruby's dynamic features allows pretty much anything to become part of the type. And what the Liskov Substitution Principle applies to depends on what dynamic features you choose to use. Furthermore in any complex piece of Ruby software - including its core classes, it is easy to write new code which finds that you're violating the Liskov Substitution Principle.

This sounds scary. What does it mean in practice?

It means that you need to be careful when adding complex metaprogramming to an existing system. It also means that you should be clear for yourself what kinds of software contracts you maintain. And, assuming you're not doinig complex metaprogramming, you need to pay more attention to the Liskov Substitution Principle on instance methods than class ones.

0

In this case I suspect that the Liskov principle would imply that now each subclass must also support .new without any arguments

That's not correct.

You require here that the metaobject of some class derived from BaseObject need to support niladic new, because the metaobject of BaseObject does.

The LSP states that objects of a subclass must support the same interface as the the base class.

The disconnect here is that the metaobjects of Derived and BaseObject are both instances of Class, not of Derived and BaseObject respectively, thus the LSP does not apply. You could argue that as instances of the same class, they still should have the same interface, but a) that's not part of the LSP, and b) that's not how Ruby works.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.