[swift-evolution] Should we rename "class" when referring to protocol conformance?

Matthew Johnson matthew at anandabits.com
Sat May 7 23:11:35 CDT 2016

> On May 7, 2016, at 3:03 PM, Dave Abrahams <dabrahams at apple.com> wrote:
> on Sat May 07 2016, Matthew Johnson <matthew-AT-anandabits.com> wrote:
>> This depends on the type. For types representing resources, etc it works just
>> fine. But for models it does not work unless the model subgraph is entirely
>> immutable and instances are unique. 
>> I agree that it isn't a good idea to provide a default that will
>> certainly be wrong in many cases.
> Please show an example of a mutable model where such an equality would
> be wrong.  

Maybe wrong is a little bit too strong a word, but it certainly isn’t the behavior people are accustomed to.  I think most people consider model instances to be logically equal if their properties are equal regardless of the address of the instances in memory.  Reference identity only works as expected if the model instances are uniqued in memory.  

let a: NSMutableArray = [1, 2, 3]
let b: NSMutableArray = [1, 2, 3]

let referenceEquality = a === b  // false
let elementEquality = a == b      // true

>>    I assume what is meant by "PureValue", is any object A, whose own references
>>    form a subgraph, within which a change to any of the values would constitute
>>    a change in the value of A (thus impermissible if A is immutable). Thus
>>    structs would quality as “PureValues”.
>> As you noted in a followup, not all structs qualify. Structs that whose members
>> all qualify will qualify. References to a subgraph that doesn't allow for any
>> observable mutation (i.e. deeply immutable reference types) also qualify.
>> This means the following qualify:
>> * primitive structs and enums
>> * observable immutable object subgraphs
>> * any type composed from the previous
>> It follows that generic types often conditionally qualify depending on their
>> type arguments.
>>    I also assume that enforcing immutability on an object graph, via CoW or
>>    otherwise, would be unfeasible. You could enforce it on all values
>>    accessible by traversing a single reference for reference types, however.
>>    This is why I don’t really buy the argument that there is no such this as
>>    deep vs shallow copy. Deep copy means copying the whole “PureValue” or
>>    subgraph, shallow copy means traversing a single reference and copying all
>>    accessible values.
>>                I don’t mean to imply that it is the *only* valuable
>>            property. However, it I (and many others) do believe it is an
>>            extremely
>>            valuable
>>            property in many cases. Do you disagree?
>>            I think I do. What is valuable about such a protocol? What generic
>>            algorithms could you write that work on models of PureValue but
>>            don't
>>            work just as well on Array<Int>?
>>            Array<Int> provides the semantics I have in mind just fine so there
>>            wouldn’t be
>>            any. Array<AnyObject> is a completely different story. With
>>            Array<AnyObject> you cannot rely on a guarantee the objects
>>            contained
>>            in the array will not be mutated by code elsewhere that also happens
>>            to have a reference to the same objects.
>>        Okay then, what algorithms can you write that operate on PureValue that
>>        don't work equally well on Array<AnyObject>?
> You haven't answered this question.  How would you use this protocol?

I think the best example was given by Andy when discussing pure functions.  Maybe I want to write a generic function and ensure it is pure.  I can only do this if I know that any arguments received that compare equal will always present the same observable state.  For example, maybe I wish to memoize the result.  

I cannot write such a function for all T, and I also cannot write such a function for all T that have value semantics if we adopt the “references are values” view of the world.  I need an additional constraint that rejects things like Array<UIView>.  (T would obviously also be constrained by a protocol that exposes the properties or methods my function requires to compute its result)

In general, it would be used where you need to ensure that the result of any operation observing the state of any part of the aggregate value will always return the same value at any point in the future.  If I observe a[0].foo now I know with certainty the result of observing a[0].foo at any point in the future.  This aspect of preservation of observed values across time is essential to the distinction between Array<LayoutValue> (see below) and Array<UIView>.  It doesn’t matter when I observe the frames of the elements of Array<LayoutValue>, I will always get the same rects back.  With Array<UIView> that is obviously not the case as the frame of the view could be mutated by anyone with a reference to the views at any time in between my observations of the frame values.

struct LayoutValue {
	frame: CGRect

>>            let t = MyClass()
>>            foo.acceptWrapped(Wrap(t))
>>            t.mutate()
>>            In this example, foo had better not depend on the wrapped instance
>>            not
>>            getting
>>            mutated.
>>            foo has no way to get at the wrapped instance, so it can't depend on
>>            anything about it.
>>            Ok, but this is a toy example. What is the purpose of Wrap? Maybe
>>            foo
>>            passes the
>>            wrapped instance back to code that *does* have visibility to the
>>            instance. My
>>            point was that shared mutable state is still possible here. 
>>            And my point is that Wrap<T> encapsulates a T (almost—I should have
>>            let
>>            it construct the T in its init rather than accepting a T parameter)
>>            and
>>            the fact that it's *possible* to code something with the structure
>>            of
>>            Wrap so that it has shared mutable state is irrelevant.
>>            The point I am trying to make is that the semantic properties of
>>            Wrap<T> depend
>>            on the semantic properties of T (whether or not non-local mutation
>>            may be
>>            observed in this case). 
>>        No they do not; Wrap<T> was specifically designed *not* to depend on the
>>        semantic properties of T. This was in answer to what you said:
>>                A struct wrapping a mutable reference type certainly doesn’t
>>            “feel” value semantic to me and certainly doesn’t have the
>>            guarantees usually associated with value semantics (won’t
>>            mutate behind your back, thread safe, etc).
>>        I have been trying to get you to nail down what you mean by PureValue,
>>        and I was trying to illustrate that merely being “a struct wrapping a
>>        mutable reference type” is not enough to disqualify anything from being
>>        in the category you're trying to describe. What are the properties of
>>        types in that category, and what generic code would depend on those
>>        properties?
> Again, the key questions are above, asked a different way.
> -- 
> -Dave

More information about the swift-evolution mailing list