-
Notifications
You must be signed in to change notification settings - Fork 2
/
slides.html
821 lines (598 loc) · 17.9 KB
/
slides.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
<!DOCTYPE html>
<html>
<head>
<title>Dispatching Design Patterns</title>
<meta charset="utf-8">
<style>
@import url(https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz);
@import url(https://fonts.googleapis.com/css?family=Droid+Serif:400,700,400italic);
@import url(https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700,400italic);
body { font-family: 'Droid Serif'; }
h1, h2, h3 {
font-family: 'Yanone Kaffeesatz';
font-weight: normal;
}
.remark-code, .remark-inline-code { font-family: 'Ubuntu Mono'; }
</style>
</head>
<body>
<textarea id="source">
class: center, middle
# Dispatching Design Patterns
---
class: center, middle
github repo for this talk:
https://github.com/ninjaaron/dispatching-design-patterns
---
class: middle
**design pattern**:
- **1.** A formal way of documenting a general reusable solution to a
design problem in a particular field of expertise.
(wiktionary.org)
---
class: middle
**dispatch**:
- **7.** To destroy quickly and efficiently.
- **8.** (computing) To pass on for further processing,
especially via a dispatch table [...]
(also wiktionary.org)
---
class: middle
## Julia is different
- Julia looks like Python or Ruby but objects are different.
- Instead of classes, Julia interfaces are designed in terms of
structs and multiple dispatch, more similarly to Common Lisp.
- For object-oriented programers, Julia's objects may be disorienting.
---
class: middle
- Many "Gang of Four" design patterns are made irrelevant by Julia's
high-level abstractions.
- For example, the "strategy pattern" is replaced by passing functions
to functions.
```julia
function go_to_party(party::Party, small_talk_strategy::Function)
human = pop!(party.people)
try
small_talk_stragey(human)
catch error
weep_deeply()
return ENV["HOME"]
end
# etc.
```
- Peter Norvig: _Design Patterns in Dynamic Languages_
http://www.norvig.com/design-patterns/
---
class: center, middle
# Encapsulation in Julia
---
class: middle
- It's all about the structs.
```julia
# Meet the struct:
struct Point
x::Float64
y::Float64
end
```
- Struts get a default constructor.
```julia
julia> mypoint = Point(5, 7)
Point(5.0, 7.0)
```
- attribute access.
```julia
julia> mypoint.x
5.0
```
---
class: middle
- Structs are immutable by default. This is usually good.
```julia
julia> mypoint.x = 3.0
ERROR: setfield! immutable struct of type Point cannot be changed
```
- However, some objects should change over time.
```julia
julia> mutable struct Starship
name::String
location::Point
end
julia> ship = Starship("U.S.S. Enterprise", Point(5, 7))
Starship("U.S.S. Enterprise", Point(5.0, 7.0))
# move the ship, so to speak
julia> ship.location = Point(6, 8)
```
---
class: middle
- Alternate constructors.
```julia
julia> Starship(name, x, y) = Starship(name, Point(x, y))
Starship
julia> othership = Starship("U.S.S. Defiant", 10, 2)
Starship("U.S.S. Defiant", Point(10.0, 2.0))
```
- Internal constructors with `new` _override_ the default constructor.
```julia
julia> mutable struct FancyStarship
name::String
location::Point
FancyStarship(name, x, y) = new(name, Point(x, y))
end
julia> fancy = FancyStarship("U.S.S. Discovery", 14, 32)
fancy = FancyStarship("U.S.S. Discovery", 14, 32)
```
---
class: middle, center
# Methods
---
class: middle
- Abstraction in general is about simplifying internal complexity.
```julia
function move!(starship, heading, distance)
Δx = distance * cosd(heading)
Δy = distance * sind(heading)
old = starship.location
starship.location = Point(old.x + Δx, old.y + Δy)
end
```
- Users shouldn't need to care about trigonometry to move a ship.
```julia
julia> foo_ship = Starship("Foo", 3, 4)
Starship("Foo", Point(3.0, 4.0))
julia> move!(foo_ship, 45, 1.5)
Point(4.060660171779821, 5.060660171779821)
```
---
class: middle
- In OOP, methods specifically are about providing simple interfaces
composite data types.
- In Julia methods are attached functions, not objects--however, they
are still defined in terms of types, so you can still provide simple
interfaces to complex data in much the same way as with OOP.
- This is one way in which polymorphism is achieved in Julia.
```julia
struct Rectangle
width::Float64
height::Float64
end
width(r::Rectangle) = r.width
height(r::Rectangle) = r.height
struct Square
length::Float64
end
width(s::Square) = s.length
height(s::Square) = s.length
```
---
class: middle
- Once we have the interface for two different kinds of rectangles, we
can write higher level functions that can work with either type.
```julia
julia> area(shape) = width(shape) * height(shape)
area (generic function with 1 method)
julia> area(Rectangle(3, 4))
12.0
julia> area(Square(3))
9.0
```
- Julia methods can do everything methods can do in OOP languages, but
they can do a lot more, too!
---
class: middle, center
# Polymorphism
---
class: middle
- Julia provides abstract types to make hierarchies of types with
shared behavior.
- Abstract types don't have a data layout.
```julia
# Types which inherit from `Shape` should provide an
# `area` method.
abstract type Shape end
```
- However, you can define methods in terms of abstract types.
```julia
combined_area(a::Shape, b::Shape) = area(a) + area(b)
```
---
class: middle
- You can inherit from abstract types. `<:` is the subtype operator.
```julia
struct Circle <: Shape
diameter::Float64
end
radius(c::Circle) = c.diameter / 2
area(c::Circle) = π * radius(c) ^ 2
```
- Abstract types can also be subtypes of other abstract types.
```julia
# Types which inherit from `AbstractRectangle should
# provide `height` and `width` methods.
abstract type AbstractRectangle <: Shape end
area(r::AbstractRectangle) = width(r) * height(r)
```
---
class: middle
- And to fit the previous rectangles into the new type hierarchy:
```julia
struct Rectangle <: AbstractRectangle
width::Float64
height::Float64
end
width(r::Rectangle) = r.width
height(r::Rectangle) = r.height
struct Square <: AbstractRectangle
length::Float64
end
width(s::Square) = s.length
height(s::Square) = s.length
```
---
class: middle
- Hooray for polymorphism.
```julia
const c = Circle(3)
const s = Square(3)
const r = Rectangle(3, 2)
@assert combined_area(c, s) == 16.068583470577035
@assert combined_area(s, r) == 15.0
```
(and yes, Julia will optimize all of this with the JIT. This case
requires no runtime method lookup)
---
class: middle, center
# Modules for Code Organization
---
class: middle
- One possible downside of methods being bound to functions rather
than objects is that it doesn't provide an obvious path for code
organization.
- Julia has modules for keeping related chunks of code together.
- Now for something weird with modules... This is not normal in Julia,
I'm just putting it out there: encapsulating types and their
interfaces in modules...
---
class: middle
```julia
module Shape
abstract type T end
area(shape::T) = throw(MethodError(area, shape))
combined_area(a::T, b::T) = area(a) + area(b)
end
module Circle
import ..Shape
struct T <: Shape.T
diameter::Float64
end
radius(c::T) = c.diameter / 2
Shape.area(c::T) = π * radius(c) ^ 2
end
```
---
class: middle, center
# Parametric Types: statically typed dynamic typing
---
class: middle
- parametric types are called "generics" in some languages.
- It's not really dynamic typing, but it feels a bit like it.
- It involves _type constructors_ that define parameters with _type
variables_.
```julia
julia> struct Point{T} # `T` is a type variable
x::T
y::T
end
julia> Point(1, 3)
Point{Int64}(1, 3) # `T` was infered as `Int64`
```
- Note that all cases of `T` must be of the same concrete type.
```julia
julia> Point(1, 3.0)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
Point(::T, ::T) where T at REPL[2]:2
```
---
class: middle
- type constructors can also take multiple type variables.
```julia
julia> struct TwoTypePoint{X,Y}
x::X
y::Y
end
julia> TwoTypePoint(1, 3.0)
TwoTypePoint{Int64,Float64}(1, 3.0)
```
---
class: middle
- This is bad:
```julia
julia> Point("foo", "bar")
Point{String}("foo", "bar")
```
- It would be nice to make sure we could ensure that `x` and `y` are
numeric types.
- This works, but it is also bad (for performance):
```julia
julia> struct RealPoint
x::Real
y::Real
end
julia> RealPoint(0x5, 0xaa)
RealPoint(0x05, 0xaa)
```
---
class: middle
- This is the right way to constrain on an abstract type, but to still
get the performance of a concrete type:
```julia
julia> struct Point{T <: Real}
x::T
y::T
end
julia> Point(1, 3)
Point{Int64}(1, 3)
julia> Point(1.4, 2.5)
Point{Float64}(1.4, 2.5)
julia> Point("foo", "bar")
ERROR: MethodError: no method matching Point(::String, ::String)
```
- yay!
---
class: middle
- Parameterized types are great for defining your own container types.
```julia
# the list itself
struct Nil end
struct List{T}
head::T
tail::Union{List{T}, Nil}
end
```
---
class: middle
- For the demonstration, we create a function to construct a `List`
from an abstract array:
```julia
# built a list from an array
mklist(array::AbstractArray{T}) where T =
foldr(List{T}, array, init=Nil())
```
- `where T` is the syntax for declaring a type variable outside a
struct definition.
---
class: middle
- and add the iteration protocol... (this is how you make for loops
work in Julia)
```julia
# implement the iteration protocol
Base.iterate(l::List) = iterate(l, l)
Base.iterate(::List, l::List) = l.head, l.tail
Base.iterate(::List, ::Nil) = nothing
```
---
class: middle
- Thanks to parametric types, our linked list is as efficient as it can
be, while still being able to hold any type of element.
```julia
julia> list = mklist(1:3)
List{Int64}(1, List{Int64}(2, List{Int64}(3, Nil())))
julia> for val in list
println(val)
end
1
2
3
julia> foreach(println, mklist(["foo", "bar"]))
foo
bar
```
---
class: middle, center
# The Trait Pattern
---
class: middle
- Haskell and Rust allow implementing types that
provide the interface of numeric operations and also sorting
operations (combine `Num` and `Ord`)
- Julia's type system has no way to describe types that
implement multiple interfaces (the way method resolution works makes
this complicated)
This is arguably not a huge problem because Julia types can have
interfaces implemented without having to belong to a certain type
hierarchy.
- However, Tim Holy came up with a solution: the "Holy" trait.
---
class: middle, center
## an example from `Base`
---
class: middle
```julia
julia> map(uppercase, mklist(["foo", "bar", "baz"]))
ERROR: MethodError: no method matching length(::List{String})
```
- `List` implements the iteration protocol, but it does not work with
map because it doesn't implement length!?
```julia>
julia> write("myfile.txt", "foo\nbar\nbaz\n");
julia> lines = eachline("myfile.txt");
julia> length(lines)
ERROR: MethodError: no method matching length(::Base.EachLine{IOStream})
[...]
julia> map(uppercase, lines) |> println
["FOO", "BAR", "BAZ"]
```
- Some iterables are not treated this way! Double standard!
---
class: middle
- To fix this, we must add the `IteratorSize` trait to our type, like
this:
```julia
julia> Base.IteratorSize(::Type{List}) = Base.SizeUnknown()
julia> map(uppercase, mklist(["foo", "bar", "baz"]))
3-element Array{String,1}:
"FOO"
"BAR"
"BAZ
```
- Now, everything works as expected.
---
class: middle
- What's going on? Check out `generator.jl` in the source code for
`Base`:
```julia
abstract type IteratorSize end
struct SizeUnknown <: IteratorSize end
struct HasLength <: IteratorSize end
struct HasShape{N} <: IteratorSize end
struct IsInfinite <: IteratorSize end
```
- This is the beginning of how a trait is implemented. Just descriptions
of different iterator sizes with no data layout.
- Traits are empty types that give extra information to the compile it
can use for dispatches.
---
class: middle
- A little further down, we see this:
```julia
IteratorSize(x) = IteratorSize(typeof(x))
IteratorSize(::Type) = HasLength() # HasLength is the default
```
- This makes `HasLength` the default approach to sizing an iterable
for user-defined types.
- This seems like a bad idea, but it's probably too late to change it.
---
class: middle
- You can create your own traits. This is a simple one with no
subtypes.
```julia
struct FooBar end
# default case: error out
FooBar(::T) where T = FooBar(T)
FooBar(T::Type) =
error("Type $T doesn't implement the FooBar interface.")
add_foo_and_bar(x) = add_foo_and_bar(FooBar(x), x)
add_foo_and_bar(::FooBar, x) = foo(x) + bar(x)
```
- Using a trait like this is more of a contract that your type
implements a certain interface.
---
class: middle
- unfortunately, as defined, it doesn't actually ensure that the type
has the required interface.
```julia
julia> FooBar(Int) = FooBar()
FooBar
julia> add_foo_and_bar(3)
ERROR: MethodError: no method matching foo(::Int64)
```
- we can implement a function that checks first and then registers:
```julia
# call from the global scope
register_foobar(T::Type) =
if hasmethod(foo, Tuple{T}) && hasmethod(bar, Tuple{T})
@eval FooBar(::Type{$T}) = FooBar()
else
error("Type $T must implement `foo` and `bar` methods")
end
```
---
class: middle
```julia
julia> register_foobar(Int)
ERROR: Type Int64 must implement `foo` and `bar` methods
julia> foo(x::Int) = x + 1
foo (generic function with 2 methods)
julia> bar(x::Int) = x * 2
bar (generic function with 1 method)
julia> register_foobar(Int)
FooBar
julia> add_foo_and_bar(3)
10
```
---
class: center, middle
# Dispatches for basic Pattern Matching
---
class: middle
In some functional languages, you can define different function
definitions for different values. This is how one might define a
factorial function in Haskell:
```haskell
factorial 0 = 1
factorial x = x * factorial (x-1)
```
This means, when the input argument is 0, the output is 1. For all
other inputs, the second definition is used, which is defined
recursively and will continue reducing the input on recursive calls by
1 until it reaches 0.
---
class: middle
- You can't do _exactly_ this in Julia.
- On the other hand, this feature is often used with tags that allow
functions to deal with different types. Julia's multiple dispatch
works in a similar way.
- This is great for dealing with Julia's syntax trees in macros in a
recursive way.
```julia
macro replace_1_with_x(expr)
esc(replace_1(expr))
end
replace_1(atom) = atom == 1 ? Symbol("x") : atom
replace_1(e::Expr) = Expr(e.head, map(replace_1, e.args)...)
```
---
class: middle
- As you can see, this specific macro is idiotic, but I'm sure you
can do something more useful with recursive type matching.
```julia
julia> x = 10
10
julia> @replace_1_with_x 5 + 1
15
julia> @replace_1_with_x 5 + 1 * (3 + 1)
135
julia> @macroexpand @replace_1_with_x 5 + 1 * (3 + 1)
:(5 + x * (3 + x))
```
---
class: middle
- This approach can be useful for other recursively-defined types as
well, like our linked list.
```julia
Base.map(f, nil::Nil) = nil
Base.map(f, l::List) = List(f(l.head), map(f, l.tail))
julia> map(uppercase, mklist(["foo", "bar"]))
List{String}("FOO", List{String}("BAR", Nil()))
```
- linked lists are not really the best fit for Julia, but you might
find a worthwhile application for a binary tree or similar data
structure.
---
class: middle
# helpful links
- Christopher Rackauckas has a great blog post that deals with many
similar techniques (and others), _Type-Dispatch Design: Post
Object-Oriented Programming for Julia_
http://www.stochasticlifestyle.com/type-dispatch-design-post-object-oriented-programming-julia/
- Tom Kwong released a book in January of 2020 called _Hands-On Design
Patterns and Best Practices with Julia_
https://www.packtpub.com/eu/application-development/hands-design-patterns-julia-10
which contains some similar material and a lot more besides. I would
recommend it to anyone interested in developing large-scale
applications or libraries in Julia.
- This talk is based on a guide I wrote in Jupyter notebooks:
https://github.com/ninjaaron/oo-and-polymorphism-in-julia
---
class: middle
The slideshow, a transcript and other things are avaialbe on github:
https://github.com/ninjaaron/dispatching-design-patterns
</textarea>
<script src="https://remarkjs.com/downloads/remark-latest.min.js">
</script>
<script>
var slideshow = remark.create();
</script>
</body>
</html>