Hacker News new | ask | show | jobs
by mpweiher 2873 days ago
Wow, Heisen-Swift, where the Heisenbugs are in the language specification!

What an object is, which is roughly equivalent to its observed behavior, should never depend on how it is declared.

   let greeter = LazyGreeter()
   let greeter1: Greeter = greeter
   print(greeter)
   print(greeter1)
   greeter.greet()
   greeter1.greet()
UPDATE: just in case it's not clear, this prints the following:

   greeter.LazyGreeter
   greeter.LazyGreeter
   sup
   Hello, World!
So the same object responds differently to the same message, depending on how it is declared. Yikes!
4 comments

It seems this is what you get when you decide dynamic dispatch is a demon that must be cast out. :/ It's a good example of Swift's at-times-infuriating insistence on chasing performance at the expense of developer ergonomics. That said, I'm sure it will be mediated eventually (although probably with more compiler annotations).
The problem here is that Swift is sort of this hybrid between two disparate worldviews, the old Objective-C world where inheritance is cool and dispatch is dynamic and the new world where inheritance is not even permitted and traits are used to provide dynamic dispatch. It's trying to be everything to everyone. This is one of those crossover points where the seams don't meet. If this was all dynamic, it would be fine because the messages would be dispatched properly. If this was all new world, you wouldn't even be allowed to subclass, would refer to your trait explicitly, and again, everything would work fine.

This is a fundamental structural issue with the language, philosophically, and will definitely cost programmer productivity. I'd argue this isn't a problem so much as behaves correctly due to insufficient planning.

It's not that they're taking a position on dynamic dispatch, it's that they're trying to take both positions, likely in support of Objective-C compatibility.

IMO dynamic dispatch has caused way more programmer productivity issues than anything else - you write code the compiler can't validate because you explicitly chose not to give it the information it needs to do it's job. It can't tell you your code is right. Nothing really can. The new world is a better place, Go and Rust have it right, inheritance is dead.

While inheritance is OK, and useful in programming-by-difference scenarios, Objective-C style polymorphism has always de-emphasized inheritance.

Dynamic dispatch is central to both the most productive and largest-scale software systems/environments in the world. If you think the compiler can tell you everything, you're in for a world of hurt, and if you think you can statically type-check the world: good luck.

Again, inheritance is useful, though overused in static languages that only allow polymorphism together with inheritance.

Objective-C deemphasizes formal inheritance. Class clusters and informal protocols generally substitute for some of the benefits that inheritance can give.
I guess fundamentally, this is the question I have about this situation: would you ever, in real code, want dispatch to go straight back to the default implementation when you have in hand a value with a custom implementation?

I may be having a failure of imagination; but I certainly haven't ever seen a case where I'd want that.

I’d imagine the issue lies in Swift classes utilizing only static dispatch. The protocol conformance creates a witness table populated by the methods of the class in question, but as Swift classes have no vtables except in ObjC compatability mode (right?) there’s no way to pass down the invocation from the protocol witness to a specific subclass as the method can not be resolved at runtime. This is the difference between the dynamic dispatch via witness table and the explicit static dispatch to the subclass when type information is available.

Thus it comes down to dynamic dispatch always (or at least for anything that has a protocol conformance at all) vs surprising behavior here.

I do agree with you, however.

Classes do get dynamic dispatch at least for methods/properties that can be overridden (i.e., `open`). I believe the value must also still have a reference to its class's method impls -- otherwise what happens when you cast it `if let noReallyLazyGreeter = lazyGreeter as? LazyGreeter {`? Not certain about these details, though.
This is far too far, and a lot of it is wrong
If you could elaborate on what is wrong with it, that would be helpful, compared to just dismissing it.
Yeah I’d love to have a conversation about your thoughts here!
> infuriating insistence on chasing performance at the expense of developer ergonomics

It would be slightly less infuriating had that chase actually delivered, but it dramatically has not.

Could you clarify... the "declaration" includes the type right?

So why should behavior not depend on the (static) type of the term?

That's a very standard use of static typing. It seems like you're coming from a dynamic background.

There really isn't any such principle or rule here, only it seems, unfamiliarity with type systems.

> What an object is, which is roughly equivalent to its observed behavior, should never depend on how it is declared.

This is actually a statement that strong typing should not exist. A "declaration" (really a type declaration) indicates the type, and if observed behavior cannot depend on the types there is not much point to types. Or stated positively: in a strongly typed language, the types are part of 'what an object is'.

Or, stated by example:

    let a: Int = 3
    let b: Float = 3
    print(a/2) //prints 1
    print(b/2) //prints 1.5
Both a and b are "the same" value, insofar as they are `==` and they model the same underlying mathematical concept. But behavior varies because they differ in type.

OP's snippet actually works quite a lot differently than the way you may expect; LazyGreeter.greet() and Greeter.greet() are two completely independent functions that share the same name. Name resolution is a complex topic in any language, but in Swift if you wanted to override another function you would say `override`, in which case the compiler will complain there is nothing you can override in `BaseGreeter` at which point you will understand the whole mistake.

There is actually no way to "pick" LazyGreeter's implementation of the unrelated function (as perhaps you expect). The only function we can call on a Greeter is Greeter.greet (or its overloads, and there are none). So if Greeter.greet did not exist, we would not get LazyGreeter.greet() but rather a type error.

I do think a case can be made that we need a similar `implements` keyword like `override` to check that we are implementing a protocol requirement.

> This is actually a statement that strong typing should not exist.

Nope. It is a statement that objects shouldn't be different depending on how you look at them.

The example is a class, so a reference type. That means that greeter and greeter1 are just two references to the exact same underlying object.

Your example are two distinct value type instances that you happened to initialize from the same literal.

So not even close to comparable situations. Speaking of comparable:

> Both a and b are "the same" value, insofar as they are `==`

Also nope.

   let a: Int = 3
   let b: Float = 3

   let r=a==b
   print(r)
Let's compile this:

   swiftc numbers.swift 
   numbers.swift:4:8: error: binary operator '==' cannot be applied to operands of type 'Int' and 'Float'
   let r=a==b
         ~^ ~
   numbers.swift:4:8: note: expected an argument list of type '(Int, Int)'
   let r=a==b
          ^
You may not be familiar with swift but a protocol isn’t necessarily a class. Structs ( so, value type) can also implements a protocol, and so in effect you can not tell a lot about what you’re manipulating, beyond what’s declared at the protocol level.
From TFA:

   class BaseGreeter: Greeter {}

   class LazyGreeter: BaseGreeter {
> That means that greeter and greeter1 are just two references to the exact same underlying object

They are two references that differ in type. It is a feature in Swift (and any strongly-typed language) that references are typed and when types differ, behaviors can differ. I understand you disagree with this design principle but it is an inherent property of strong type systems that have reference semantics.

> two distinct value type instances

“Nope.” There isn’t any such thing as a “value type instance” since instances are a semantic of reference types.

> objects shouldn’t be different depending on how you look at them

The “difference” here is only in the types, so this statement is equivalent to “references shouldn’t be different depending on their type.” This statement implies that types should be useless

> There isn’t any such thing as a “value type instance”

Hmm..

"An instance of a class is traditionally known as an object. However, Swift structures and classes are much closer in functionality than in other languages, and much of this chapter describes functionality that applies to instances of either a class or a structure type. Because of this, the more general term instance is used."

and

"Structure and Class Instances"

..

"Structures and Enumerations Are Value Types

A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function.

You’ve actually been using value types extensively throughout the previous chapters. In fact, all of the basic types in Swift—integers, floating-point numbers, Booleans, strings, arrays and dictionaries—are value types, and are implemented as structures behind the scenes."

https://docs.swift.org/swift-book/LanguageGuide/ClassesAndSt...

> “references shouldn’t be different depending on their type.”

You are confusing the type of the variable with the type of the object/value contained in the variable. A static type system is there to ensure that the type of the variable matches the type its contents.

> You are confusing the type of the variable with the type of the object/value contained in the variable.

I’m not “confusing” them; I’m prioritizing the former over the latter, whereas you are prioritizing the latter over the former. This is the classic strong/weak typing debate.

If you want a language in which the dynamic type overrides the static type, there are several (including Swift if you are explicit about the override, and sometimes even if you are not explicit).

> A static type system is there to ensure that the type of the variable matches the type in its contents

A strong type system is there to apply strict type rules. one of Swift’s type rules is that Greeter.greet() calls that function or an override, not an unrelated function that could only be inferred at runtime.

Your example does not have the same structure; `Int` and `Float` are concrete types, not interface types.

> strong typing

Strong typing is not the same as static typing. `let greeter: Greeter = LazyGreeter()` is a strongly-typed `LazyGreeter`: you cannot use it where a value of another incompatible type is required, nor can you change its runtime type. But its static type is `Greeter`. Swift uses the latter for method resolution, but there's nothing inevitable or inherent about that.

`NSArray * array = [NSArray new];` is statically typed as an `NSArray`, but its dynamic (and strong) type is `__NSArray0`, and that's where the value's implementations come from.

> they model the same underlying mathematical concept

They don't: one models an integral and the other models(/approximates) a real. Which is why, as another commentor already pointed out, they're not `==`.

a and b are not the same value, and don't model the same underlying concept.

a: Int would be 0x0000000000000003.

b: Float would be 0x40400000.

While both are specified literally as "3" the compiler de-sugars that into the values above, and further, the type system takes that knowledge into consideration to prevent you from doing things that don't make sense like trying to equate "a" and "b" without explicitly performing a lossy conversion (Binary operator '==' cannot be applied to operands of type 'Float' and 'Int').

I don't believe this to be a name resolution issue, but rather a protocol conformance creates a vtable for the protocol but beyond that dynamic dispatch doesn't exist so it's not possible to resolve the actual method overridden in the subclass.

This isn't a statement on typing (other than your example actually making a solid case for strong typing); rather the boundary between static and dynamic dispatch is not well-constructed in the case of protocol conformances.

Interestingly, in Swift, you Cannot compare `a == b` as the compiler tells you the two types are incompatible.
Haskell has no problem with this(though its default Num hierarchy is sorely lacking)

    Prelude> a = 1
    Prelude> a/2
    0.5
    Prelude> :t a/2
    a/2 :: Fractional a => a
    Prelude> b = 1 :: Int
    Prelude> b/2

    <interactive>:6:1: error:
        • No instance for (Fractional Int) arising from a use of ‘/’
        • In the expression: b / 2
          In an equation for ‘it’: it = b / 2
    Prelude> b `div` 2
    0
    Prelude> :t div
    div :: Integral a => a -> a -> a
I’m trying to wrap my C# brain around this...

Is this similar to the C# construct....

interface IGreeter { void Greet()}

class BaseGreeter :IGreeter {

public void Greet {Console.WriteLine(“Hello World”)}

public void IGreeter:Greet {Console.Writeline(“sup”)} }

}

Greeter greet1 = new Greeter();

IGreeter greet2 = greet1();

Console.Writeline(greet1 == greet2) greet1.Greet() greet2.Greet()

Would print I believe.

true

Hello World

sup