WWDC2015 Session 408

Transcript

X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
[Music]
[Silence]
[Applause]
>> Dave Abrahams: Hi, everybody.
My name is Dave Abrahams,
and I'm the technical lead
for the Swift standard library,
and it is truly my privilege
to be with you here today.
It is great to see all
of you in this room.
The next 40 minutes are about
putting aside your usual way
of thinking about programming.
What we're going to do together
here won't necessarily be easy,
but I promise you if
you stick with me,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
but I promise you if
you stick with me,
that it'll be worth your time.
I'm here to talk to you
about themes at the heart
of Swift's design, and introduce
you to a way of programming
that has the potential
to change everything.
But first, let me introduce
you to a friend of mine.
This is Crusty.
Now you've probably all worked
with some version of this guy.
Crusty is that old-school
programmer
who doesn't trust
debuggers, doesn't use IDEs.
No, he favors an 80
x 24 terminal window
and plain text, thank
you very much.
And he takes a dim view of
the latest programming fads.
Now I've learned
to expect Crusty
to be a little bit
cynical and grumpy,
but even so it sometimes
takes me by surprise.
Like last month we were
talking about app development,
and said flat out, 'I
don't do object-oriented.'
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and said flat out, 'I
don't do object-oriented.'
I could hardly believe my ears.
I mean, object-oriented
programming has been
around since the 1970s, so it's
not exactly some new-fangled
programming fad.
And, furthermore, lots
of the amazing things
that we've all built together,
you and I and the engineers
on whose shoulders we stand,
were built with objects.
'Come on,' I said
to him as I walked
over to his old-school
chalkboard.
'OOP is awesome.
Look what you can
do with classes.'
[Silence]
Yes. So first you can group
related data and operations.
And then we can build walls to
separate the inside of our code
from the outside, and
that's what lets us
maintain invariants.
Then we use classes to represent
relatable ideas, like window
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Then we use classes to represent
relatable ideas, like window
or communication channel.
They give us a namespace,
which helps prevent collisions
as our software grows.
They have amazing
expressive syntax.
So we can write method
calls and properties
and chain them together.
We can make subscripts.
We can even make properties
that do computation.
Last, classes are open
for extensibility.
So if a class author leaves
something out that I need, well,
I can come along
and add it later.
And, furthermore,
together, these things,
these things let us
manage complexity
and that's really the main
challenge in programming.
These properties, they directly
address the problems we're
trying to solve in
software development.
At that point, I had gotten
myself pretty inspired,
but Crusty just snorted
and [sighed].
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
but Crusty just snorted
and [sighed].
[hiss sound] He let all
the air out of my balloon.
And if that wasn't bad enough,
a moment later he
finished the sentence.
[Laughter]
Because it's true, in Swift,
any type you can name is a first
class citizen and it's able
to take advantage of
all these capabilities.
So I took a step back
and tried to figure
out what core capability enables
everything we've accomplished
with object-oriented
programming.
Obviously, it has to come from
something that you can only do
with classes, like inheritance.
And this got me thinking
specifically
about how these structures
enable both code sharing
and fine-grained customization.
So, for example, a superclass
can define a substantial method
with complex logic,
and subclasses get all
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
with complex logic,
and subclasses get all
of the work done by the
superclass for free.
They just inherit it.
But the real magic happens when
the superclass author breaks
out a tiny part of
that operation
into a separate customization
point
that the subclass can override,
and this customization
is overlaid
on the inherited implementation.
That allows the difficult logic
to be reused while enabling
open-ended flexibility
and specific variations.
And now, I was sure, I had him.
'Ha,' I said to Crusty.
'Obviously, now you have to bow
down before the power
of the class.'
'Hold on just a darn
tootin' minute,' he replied.
'First of all,
I do customization
whatchamacallit all the time
with structs, and second,
yes, classes are powerful
but let's talk about the costs.
I have got three major beefs
with classes,' said Crusty.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I have got three major beefs
with classes,' said Crusty.
And he started in on
his list of complaints.
'First, you got your
automatic sharing.'
Now you all know
what this looks like.
A hands B some piece of
perfectly sober looking data,
and B thinks, 'Great,
conversation over.'
But now we've got
a situation where A
and B each have their own very
reasonable view of the world
that just happens to be wrong.
Because this is the reality:
eventually A gets tired
of serious data and decides
he likes ponies instead,
and who doesn't love
a good pony?
This is totally fine until
B digs up this data later,
much later, that she got from A
and there's been a
surprise mutation.
B wants her data,
not A's ponies.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well, Crusty has a whole rant
about how this plays out.
'First,' he says, 'you start
copying everything like crazy
to squash the bugs in your code.
But now you're making
too many copies,
which slows the code down.
And then one day you handle
something on a dispatch queue
and suddenly you've
got a race condition
because threads are
sharing a mutable state,
so you start adding locks
to protect your invariants.
But the locks slow the
code down some more
and might even lead to deadlock.
And all of this is
added complexity,
whose effects can be summed
up in one word, bugs.'
But none of this is news
to Cocoa programmers.
[Laughter]
[Applause]
It's not news.
We've been applying a
combination of language features
like @property(copy)
and coding conventions
over the years to handle this.
And we still get bitten.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And we still get bitten.
For example, there's
this warning
in the Cocoa documentation
about modifying a mutable
collection while you're
iterating through it.
Right? And this is all
due to implicit sharing
of mutable state, which
is inherent to classes.
But this doesn't apply to Swift.
Why not? It's because Swift
collections are all value types,
so the one you're iterating
and the one you're
modifying are distinct.
Okay, number two
on Crusty's list,
class inheritance
is too intrusive.
First of all, it's monolithic.
You get one and only
one superclass.
So what if you need to
model multiple abstractions?
Can you be a collection
and be serialized?
Well, not if collection
and serialized are classes.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And because class inheritance
is single inheritance,
classes get bloated
as everything
that might be related
gets thrown together.
You also have to
choose your superclass
at the moment you
define your class,
not later in some extension.
Next, if your superclass
had stored properties, well,
you have to accept them.
You don't get a choice.
And then because it
has stored properties,
you have to initialize it.
And as Crusty says, 'designated
convenience required, oh, my.'
So you also have to make
sure that you understand how
to interact with your superclass
without breaking its invariants.
Right? And, finally, it's
natural for class authors
to write their code as though
they know what their methods are
going to do, without using
final and without accounting
for the chance that the
methods might get overridden.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
for the chance that the
methods might get overridden.
So, there's often a crucial
but unwritten contract
about which things
you're allowed
to actually override and,
like, do you have to chain
to the superclass method?
And if you're going to chain
to the superclass method,
is it at the beginning of
your method, or at the end,
or in the middle somewhere?
So, again, not news to
Cocoa programmers, right?
This is exactly why we use
the delegate pattern all
over the place in Cocoa.
Okay, last on Crusty's
list, classes just turn
out to be a really
bad fit for problems
where type relationships matter.
So if you've ever
tried to use classes
to represent a symmetric
operation,
like Comparison, you
know what I mean.
For example, if you want
to write a generalized sort
or binary search like
this, you need a way
to compare two elements.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to compare two elements.
And with classes, you end
up with something like this.
Of course, you can't just
write Ordered this way,
because Swift demands a
method body for precedes.
So, what can we put there?
Remember, we don't know anything
about an arbitrary
instance of Ordered yet.
So if the method isn't
implemented by a subclass, well,
there's really nothing we
can do other than trap.
Now, this is the first sign that
we're fighting the type system.
And if we fail to
recognize that,
it's also where we start
lying to ourselves,
because we brush the issue
aside, telling ourselves
that as long as each subclass
of Ordered implements
precedes, we'll be okay.
Right? Make it the
subclasser's problem.
So we press ahead and
implement an example of Ordered.
So, here's a subclass.
It's got a double value
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
It's got a double value
and we override precedes
to do the comparison.
Right? Except, of
course, it doesn't work.
See, "other" is just
some arbitrary Ordered
and not a number, so
we don't know that
"other" has a value property.
In fact, it might turn
out to be a label,
which has a text property.
So, now we need to down-cast
just to get to the right type.
But, wait a sec, suppose that
"other" turns out to be a label?
Now, we're going to trap.
Right? So, this is
starting to smell a lot
like the problem we had when
writing the body for precedes
in the superclass, and we
don't have a better answer now
than we did before.
This is a static
type safety hole.
Why did it happen?
Well, it's because classes don't
let us express this crucial type
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well, it's because classes don't
let us express this crucial type
relationship between the type
of self and the type of other.
In fact, you can use
this as a "code smell."
So, any time you see a forced
down-cast in your code,
it's a good sign that some
important type relationship has
been lost, and often that's due
to using classes
for abstraction.
Okay, clearly what we need is
a better abstraction mechanism,
one that doesn't force us
to accept implicit sharing,
or lost type relationships,
or force us to choose just
one abstraction and do it
at the time we define our
types; one that doesn't force us
to accept unwanted instance data
or the associated
initialization complexity.
And, finally, one that
doesn't leave ambiguity
about what I need to override.
Of course, I'm talking
about protocols.
Protocols have all these
advantages, and that's why,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Protocols have all these
advantages, and that's why,
when we made Swift, we made
the first protocol-oriented
programming language.
[Applause]
So, yes, Swift is great for
object-oriented programming,
but from the way for loops
and string literals work
to the emphasis in the
standard library on generics,
at its heart, Swift
is protocol-oriented.
And, hopefully, by the
time you leave here,
you'll be a little more
protocol-oriented yourself.
So, to get you started
off on the right foot,
we have a saying in Swift.
Don't start with a class.
Start with a protocol.
So let's do that with
our last example.
Okay, first, we need a protocol,
and right away Swift complains
that we can't put
a method body here,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that we can't put
a method body here,
which is actually pretty
good because it means
that we're going to trade
that dynamic runtime check
for a static check, right,
that precedes as implemented.
Okay, next, it complains that
we're not overriding anything.
Well, of course we're not.
We don't have a baseclass
anymore, right?
No superclass, no override.
And we probably didn't even
want number to be a class
in the first place, because we
want it to act like a number.
Right? So, let's just
do two thing at once
and make that a struct.
Okay, I want to stop for a
moment here and appreciate
where we are, because this
is all valid code again.
Okay, the protocol is
playing exactly the same role
that the class did in our
first version of this example.
It's definitely a bit better.
I mean, we don't have
that fatal error anymore,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I mean, we don't have
that fatal error anymore,
but we're not addressing the
underlying static type safety
hole, because we still need
that forced down-cast because
"other" is still some
arbitrary Ordered.
Okay. So, let's make it a number
instead, and drop the type cast.
Well, now Swift is
going to complain
that the signatures
don't match up.
To fix this, we need
to replace Ordered
in the protocol signature
with Self.
This is called a
Self-requirement.
So when you see Self in a
protocol, it's a placeholder
for the type that's
going to conform
to that protocol,
the model type.
So, now we have valid
code again.
Now, let's take a look at
how you use this protocol.
So, this is the binary
search that worked
when Ordered was a class.
And it also worked
perfectly before we added
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And it also worked
perfectly before we added
that Self-requirement
to Ordered.
And this array of
ordered here is a claim.
It's a claim that we're going
to handle a heterogeneous
array of Ordered.
So, this array could
contain numbers
and labels mixed
together, right?
Now that we've made
this change to Ordered
and added the Self-requirement,
the compiler is going
to force us to make this
homogeneous, like this.
This one says, 'I work
on a homogeneous array
of any single Ordered type T.'
Now, you might think
that forcing the array
to be homogeneous is too
restrictive or, like,
a loss of functionality or
flexibility or something.
But if you think about it,
the original signature
was really a lie.
I mean, we never really handled
the heterogeneous case other
than by trapping.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
than by trapping.
Right? A homogeneous
array is what we want.
So, once you add a
Self-requirement to a protocol,
it moves the protocol into
a very different world,
where the capabilities have a
lot less overlap with classes.
It stops being usable as a type.
Collections become homogeneous
instead of heterogeneous.
An interaction between instances
no longer implies an interaction
between all model types.
We trade dynamic polymorphism
for static polymorphism, but,
in return for that extra type
information we're giving the
compiler, it's more optimizable.
So, two worlds.
Later in the talk, I'll show
you how to build a bridge
between them, at least one way.
Okay. So, I understood
how the static aspect
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Okay. So, I understood
how the static aspect
of protocols worked, but
I wasn't sure whether
to believe Crusty that protocols
could really replace classes
and so I set him a
challenge, to build something
for which we'd normally use
OOP, but using protocols.
I had in mind a little
diagramming app
where you could drag and drop
shapes on a drawing surface
and then interact with them.
And so I asked Crusty to build
the document and display model.
And here's what he came up with.
First, he built some
drawing primitives.
Now, as you might imagine,
Crusty really doesn't
really do GUI's.
He's more of a text man.
So his primitives just print
out the drawing commands
you issue, right?
I grudgingly admitted that
this was probably enough
to prove his point, and then
he created a Drawable protocol
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
to prove his point, and then
he created a Drawable protocol
to provide a common interface
for all of our drawing elements.
Okay, this is pretty
straightforward.
And then he started
building shapes like Polygon.
Now, the first thing
to notice here
about Polygon is
it's a value type,
built out of other value types.
It's just a struct that
contains an array of points.
And to draw a polygon, we
move to the last corner
and then we cycle through all
the corners, drawing lines.
Okay, and here's a Circle.
Again, Circle is a value type,
built out of other value types.
It's just a struct that contains
a center point and a radius.
Now to draw a Circle, we make
an arc that sweeps all the way
from zero to two pi radians.
So, now we can build a diagram
out of circles and polygons.
'Okay,' said Crusty,
'let's take her for a spin.'
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
'Okay,' said Crusty,
'let's take her for a spin.'
So, he did.
This is a diagram.
A diagram is just a Drawable.
It's another value type.
Why is it a value type?
Because all Drawables are
value types, and so an array
of Drawables is also
a value type.
Let's go back to that.
Wow. Okay, there.
An array of Drawables is also
a value type and, therefore,
since that's the only
thing in my Diagram,
the Diagram is also
a value type.
So, to draw it, we
just loop through all
of the elements and
draw each one.
Okay, now let's take
her for a spin.
So, we're going to test it.
So, Crusty created a Circle
with curiously specific
center and radius.
And then, with uncanny
Spock-like precision,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And then, with uncanny
Spock-like precision,
he added a Triangle.
And finally, he built a Diagram
around them, and
told it to draw.
'Voila,' said Crusty,
triumphantly.
'As you can plainly see, this
is an equilateral triangle
with a circle, inscribed
inside a circle.'
Well, maybe I'm just not as
good at doing trigonometry
in my head, as Crusty is,
but, 'No, Crusty,' I said,
'I can't plainly see that,
and I'd find this demo a
whole lot more compelling
if I was doing something
actually useful
for our app like, you know,
drawing to the screen.'
After I got over my annoyance,
I decided to rewrite his
Renderer to use CoreGraphics.
And I told him I was
going to this and he said,
'Hang on just a minute
there, monkey boy.
If you do that, how am I
going to test my code?'
And then he laid out a pretty
compelling case for the use
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And then he laid out a pretty
compelling case for the use
of plaintext in testing.
If something changes
in what we're doing,
we'll immediately
see it in the output.
Instead, he suggested
we do a little
protocol-oriented programming.
So he copied his Renderer and
made the copy into a protocol.
Yeah, and then you have to
delete the bodies, okay.
There it is.
And then he renamed the original
Renderer and made it conform.
Now, all of this refactoring
was making me impatient, like,
I really want to see
this stuff on the screen.
I wanted to rush on and
implement a Renderer
for CoreGraphics,
but I had to wait
until Crusty tested
his code again.
And when he was finally
satisfied, he said to me, 'Okay,
what are you going to
put in your Renderer?'
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
what are you going to
put in your Renderer?'
And I said, 'Well, a CGContext.
CGContext has basically
everything a Renderer needs."
In fact, within the limits
of its plain C interface,
it basically is a Renderer.
'Great,' said Crusty.
'Gimme that keyboard.'
And he snatched something away
from me and he did something
so quickly I barely saw it.
'Wait a second,' I said.
'Did you just make every
CGContext into a Renderer?'
He had. I mean, it
didn't do anything yet,
but this was kind of amazing.
I didn't even have
to add a new type.
'What are you waiting for?'
said Crusty.
'Fill in those braces.'
So, I poured in the
necessary CoreGraphics goop,
and threw it all into a
playground, and there it is.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and threw it all into a
playground, and there it is.
Now, you can download
this playground,
which demonstrates everything
I'm talking about here
in the talk, after we're done.
But back to our example.
Just to mess with me,
Crusty then did this.
Now, it took me a second to
realize why Drawing wasn't going
into an infinite recursion at
this point, and if you want
to know more about
that, you should go
to this session, on Friday.
But it also didn't
change the display at all.
Eventually, Crusty decided
to show me what was happening
in his plaintext output.
So it turns out that it was
just repeating the same drawing
commands, twice.
So, being more of a
graphics-oriented guy,
I really wanted to
see the results.
So, I built a little scaling
adapter and wrapped it
around the Diagram and
this is the result.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
around the Diagram and
this is the result.
And you can see this in the
playground, so I'm not going
to go into the scaling
adapter here.
But that's kind of
a demonstration
that with protocols, we can do
all the same kinds of things
that we're used to
doing with classes.
Adapters, usual design patterns.
Okay, now I'd like to
just reflect a second
on what Crusty did with
TestRenderer though,
because it's actually
kind of brilliant.
See, by decoupling the document
model from a specific Renderer,
he's able to plug in an
instrumented component
that reveals everything
that we do,
that our code does, in detail.
And we've since applied this
approach throughout our code.
We find that, the more we
decouple things with protocols,
the more testable
everything gets.
This kind of testing is
really similar to what you get
with mocks, but it's
so much better.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
with mocks, but it's
so much better.
See, mocks are inherently
fragile, right?
You have to couple
your testing code
to the implementation details
of the code under test.
And because of that
fragility, they don't play well
with Swift's strong
static type system.
See, protocols give us
a principled interface
that we can use, that's
enforced by the language,
but still gives us the
hooks to plug in all
of the instrumentation we need.
Okay, back to our example,
because now we seriously need
to talk about bubbles.
Okay. We wanted this diagramming
app to be popular with the kids,
and the kids love
bubbles, of course.
So, in a Diagram, a bubble is
just an inner circle offset
around the center of the
outer circle that you use
to represent a highlight.
So, you have two circles.
Just like that.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And when I put this
code in context though,
Crusty started getting
really agitated.
All the code repetition
was making him ornery,
and if Crusty ain't
happy, ain't nobody happy.
[Laughter]
'Look, they're all complete
circles,' he shouted.
'I just want to write this.'
I said, 'Calm down, Crusty.
Calm down.
We can do that.
All we need to do is add another
requirement to the protocol.
All right?
Then of course we update
our models to supply it.
There's test Renderer.
And then the CGContext.'
Now, at this point Crusty's got
his boot off and he's beating it
on the desk, because here we
were again, repeating code.
He snatched the keyboard back
from me, muttering something
about having to do everything
his own self, and he proceeded
to school me using a
new feature in Swift.
This is a protocol extension.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
This is a protocol extension.
This says 'all models
of Renderer have this
implementation of circleAt.'
Now we have an implementation
that is shared among all
of the models of Renderer.
So, notice that we still have
this circleAt requirement
up there.
You might ask, 'what
does it means
to have a requirement that's
also fulfilled immediately
in an extension?'
Good question.
The answer is that a
protocol requirement creates a
customization point.
To see how this plays out,
let's collapse this method body
and add another method
to the extension.
One that isn't backed
by a requirement.
And now we can extend
Crusty's TestRenderer
to implement both
of these methods.
And then we'll just call them.
Okay. Now, what happens here
is totally unsurprising.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Okay. Now, what happens here
is totally unsurprising.
We're directly calling the
implementations in TestRenderer
and the protocol isn't
even involved, right?
We'd get the same result if
we removed that conformance.
But now, let's change
the context
so Swift only knows it has a
Renderer, not a TestRenderer.
And here's what happens.
So because circleAt
is a requirement,
our model gets the
privilege of customizing it,
and the customization
gets called.
That one. But rectangleAt
isn't a requirement,
so the implementation
in TestRenderer,
it only shadows the one in the
protocol and in this context,
where you only know you have a
Renderer and not a TestRenderer,
the protocol implementation
is called.
Which is kind of weird, right?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Which is kind of weird, right?
So, does this mean
that rectangleAt should
have been a requirement?
Maybe, in this case, it should,
because some Renderers
are highly likely
to have a more efficient
way to draw rectangles, say,
aligned with a coordinate
system.
But, should everything in your
protocol extension also be
backed by a requirement?
Not necessarily.
I mean, some APIs
are just not intended
as customization points.
So, sometimes the right fix is
to just not shadow the
requirement in the model,
not shadow the method
in the model.
Okay. So, this new
feature, incidentally,
it's revolutionized our work
on the Swift Standard Library.
Sometimes what we can do
with protocol extensions,
it just feels like magic.
I really hope that you'll enjoy
working with the latest library
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
I really hope that you'll enjoy
working with the latest library
as much as we've
enjoyed applying this
to it and updating it.
And I want to put our
story aside for a second,
so I can show you some things
that we did in Standard Library
with protocol extensions,
and few other tricks besides.
So, first, there's a
new indexOf method.
So, this just walks through
the indices of the collection
until it finds an element that's
equal to what we're looking for
and it returns that index.
And if it doesn't find
one, it returns nil.
Simple enough, right?
But if we write it this
way, we have a problem.
See the elements of an arbitrary
collection can't be compared
with equal-equal.
So, to fix that, we can
constrain the extension.
This is another aspect
of this new feature.
So, by saying this extension
applies when the element type
of the collection is Equatable,
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
of the collection is Equatable,
we've given Swift the
information it needs to allow
that equality comparison.
And now that we've
seen a simple example
of a constrained extension,
let's revisit our binary search.
And let's use it
on an array of Int.
Hmm. Okay, Int doesn't
conform to Ordered.
Well that's a simple fix, right?
We'll just add a conformance.
Okay, now what about Strings?
Well, of course,
this doesn't work
for Strings, so we do it again.
Now before Crusty starts banging
on his desk, we really want
to factor this stuff out, right?
The less-than operator
is present
in the Comparable
protocol, so we could do this
with an extension to comparable.
Like this.
Now we're providing the
precedes for those conformances.
So, on the one hand, this
is really nice, right?
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, on the one hand, this
is really nice, right?
When I want a binary
search for Doubles, well,
all I have to is add this
conformance and I can do it.
On the other hand, it's
kind of icky, because even
if I take away the conformance,
I still have this precedes
function that's been glommed
onto Doubles, which already have
enough of an interface, right?
We maybe would like to be
a little bit more selective
about adding stuff to Double.
So, and even though
I can do that,
I can't binarySearch with it.
So it's really, that precedes
function buys me nothing.
Fortunately, I can
be more selective
about what gets a precedes API,
by using a constrained
extension on Ordered.
So, this says that a type that
is Comparable and is declared
to be Ordered will
automatically be able
to satisfy the precedes
requirement,
which is exactly what we want.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
which is exactly what we want.
I'm sorry, but I think
that's just really cool.
[Applause] We've got
the same abstraction.
The same logical
abstraction coming
from two different places,
and we've just made them
interoperate seamlessly.
Thank you for the applause,
but I just, I think it's cool.
Okay, ready for a
palate cleanser?
That's just showing it work.
Okay. This is the signature of
a fully generalized binarySearch
that works on any Collection
with the appropriate
Index and Element types.
Now, I can already hear you guys
getting uncomfortable out there.
I'm not going to write
the body out here,
because this is already pretty
awful to look at, right?
Swift 1 had lots of generic
free functions like this.
In Swift 2, we used protocol
extensions to make them
into methods like this,
which is awesome, right?
Now, everybody focuses on
the improvement this makes
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Now, everybody focuses on
the improvement this makes
at the call site, which
is now clearly chock full
of method-y goodness, right?
But as the guy writing
binarySearch,
I love what it did
for the signature.
By separating the conditions
under which this method applies
from the rest of
the declaration,
which now just reads
like a regular method.
No more angle bracket blindness.
[Applause]
Thank you very much.
Okay, last trick before
we go back to our story.
This is a playground
containing a minimal model
of Swift's new OptionSetType
protocol.
It's just a struct
with a read-only Int
property, called rawValue.
Now take a look at the broad
Set-like interface you actually
get for free once
you've done that.
All of this comes from
protocol extensions.
And when you get a chance,
I invite you to take a look
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
And when you get a chance,
I invite you to take a look
at how those extensions
are declared
in the Standard Library, because
several layers are working
together to provide
this rich API.
Okay, so those are some of the
cool things that you can do
with protocol extensions.
Now, for the piece de
resistance, I'd like to return
to our diagramming example.
Always make value
types equatable.
Why? Because I said so.
Also, eat your vegetables.
No, actually, if you want to
know why, go to this session
on Friday, which I
told you about already.
It's a really cool
talk and they're going
to discuss this issue in detail.
Anyway, Equatable is easy
for most types, right?
You just compare corresponding
parts for equality, like this.
But, now, let's see what
happens with Diagram.
Uh-oh. We can't compare two
arrays of Drawable for equality.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Uh-oh. We can't compare two
arrays of Drawable for equality.
All right, maybe we can do it
by comparing the
individual elements,
which looks something like this.
Okay, I'll go through
it for you.
First, you make sure they have
the same number of elements,
then you zip the
two arrays together.
If they do have the same number
of elements, then you look
for one where you have
a pair that's not equal.
All right, you can
take my word for it.
This isn't the interesting
part of the problem.
Oops, right?
This is, the whole reason we
couldn't compare the arrays is
because Drawables
aren't equatable, right?
So, we didn't have an equality
operator for the arrays.
We don't have an
equality operator
for the underlying Drawables.
So, can we just make
all Drawables Equatable?
We change our design like this.
Well, the problem with this is
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Well, the problem with this is
that Equatable has
Self-requirements,
which means that Drawable
now has Self-requirements.
And a Self-requirement
puts Drawable squarely
in the homogeneous, statically
dispatched world, right?
But Diagram really needs
a heterogeneous array
of Drawables, right?
So we can put polygons and
circles in the same Diagram.
So Drawable has to stay
in the heterogeneous,
dynamically dispatched world.
And we've got a contradiction.
Making Drawable equatable
is not going to work.
We'll need to do
something like this,
which means adding a new
isEqualTo requirement
to Drawable.
But, oh, no, we can't
use Self, right?
Because we need to
stay heterogeneous.
And without Self, this is just
like implementing
Ordered with classes was.
We're now going to
force all Drawables
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
We're now going to
force all Drawables
to handle the heterogeneous
comparison case.
Fortunately, there's
a way out this time.
Unlike most symmetric
operations, equality is special
because there's an
obvious, default answer
when the types don't
match up, right?
We can say if you have
two different types,
they're not equal.
With that insight, we
can implement isEqualTo
for all Drawables when
they're Equatable.
Like this.
So, let me walk you through it.
The extension is
just what we said.
It's for all Drawables
that are Equatable.
Okay, first we conditionally
down-cast other
to the Self type.
Right? And if that
succeeds, then we can go ahead
and use equality comparison,
because we have an
Equatable conformance.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
because we have an
Equatable conformance.
Otherwise, the instances
are deemed unequal.
Okay, so, big picture,
what just happened here?
We made a deal with the
implementers of Drawable.
We said, 'If you
really want to go
and handle the heterogeneous
case, be my guest.
Go and implement isEqualTo.
But if you just want to just
use the regular way we express
homogeneous comparison,
we'll handle all the burdens
of the heterogeneous
comparison for you.'
So, building bridges
between the static
and dynamic worlds is a
fascinating design space,
and I encourage you
to look into more.
This particular problem we
solved using a special property
of equality, but the
problems aren't all like that,
and there's lots of really
cool stuff you can do.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
and there's lots of really
cool stuff you can do.
So, that property of equality
doesn't necessarily apply,
but what does apply
almost universally?
Protocol-based design.
Okay, so, I want to say a few
words before we wrap up about
when to use classes, because
they do have their place.
Okay? There are times when you
really do want implicit sharing,
for example, when the
fundamental operations
of a value type don't make any
sense, like copying this thing.
What would a copy mean?
If you can't figure
out what that means,
then maybe you really do want
it to be a reference type.
Or a comparison.
The same thing.
That's another fundamental
part of being a value.
So, for example, a Window.
What would it mean
to copy a Window?
Would you actually
want to see, you know,
a new graphical Window?
What, right on top
of the other one?
I don't know.
It wouldn't be part of
your view hierarchy.
Doesn't make sense.
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
Doesn't make sense.
So, another case
where the lifetime
of your instance is tied to
some external side effect,
like files appearing
on your disk.
Part of this is because values
get created very liberally
by the compiler, and
created and destroyed,
and we try to optimize
that as well as possible.
It's the reference types that
have this stable identity,
so if you're going to make
something that corresponds
to an external entity,
you might want
to make it a reference type.
A class. Another case
is where the instances
of the abstraction
are just "sinks."
Like, our Renderers,
for example.
So, we're just pumping, we're
just pumping information
into that thing, into
that Renderer, right?
We tell it to draw a line.
So, for example, if you
wanted to make a TestRenderer
that accumulated the text
to output of these commands
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
that accumulated the text
to output of these commands
into a String instead of just
dumping them to the console,
you might do it like this.
But notice a couple
of things about this.
First, it's final, right?
Second, it doesn't
have a base class.
That's still a protocol.
I'm using the protocol
for the abstraction.
Okay, a couple of more cases.
So, we live in an
object-oriented world, right?
Cocoa and Cocoa Touch
deal in objects.
They're going to
give you baseclasses
and expect you to subclass them.
They're going to expect
objects in their APIs.
Don't fight the system, okay?
That would just be futile.
But, at the same time,
be circumspect about it.
You know, nothing in your
program should ever get too big,
and that goes for classes
just as well as anything else.
So, when you're refactoring
and factoring something
X-TIMESTAMP-MAP=MPEGTS:181083,LOCAL:00:00:00.000
So, when you're refactoring
and factoring something
out of class, consider
using a value type instead.
Okay, to sum up.
Protocols, much greater than
superclasses for abstraction.
Second, protocol
extensions, this new feature
to let you do almost
magic things.
Third, did I mention you should
go see this talk on Friday?
Go see this talk on Friday.
Eat your vegetables.
Be like Crusty.
Thank you very much.
[Applause]
[Silence]