├── README.md ├── ast_pattern_matching.jl ├── foobar_trait.jl ├── linkedlist.jl ├── shapes.jl ├── slides.html ├── strategy.jl └── strategy.md /README.md: -------------------------------------------------------------------------------- 1 | 2 | Great links related to this talk. 3 | 4 | Most of the material in this talk, along with a bit more, is available 5 | as a notebook here: 6 | https://github.com/ninjaaron/oo-and-polymorphism-in-julia 7 | 8 | I should also say that many of the things I will talk about now were 9 | covered some years ago in a great blog post by Chris Rackaucus, 10 | [Type-Dispatch Design: Post Object-Oriented Programming for Julia]( 11 | http://www.stochasticlifestyle.com/type-dispatch-design-post-object-oriented-programming-julia/), 12 | which is linked in the github repo for this talk. 13 | 14 | Tom Kwong also has a (relatively) new book about Design patterns in 15 | Julia which is helpful, [Hands-On Design Patterns and Best Practices 16 | with Julia]( 17 | https://www.packtpub.com/eu/application-development/hands-design-patterns-julia-10). 18 | 19 | Old Peter Norvig talk about design patterns in Dynamic languages: 20 | http://www.norvig.com/design-patterns/ 21 | 22 | It's mostly about Dylan, which is kind of a legacy language, but in 23 | many ways it is a close cousin of Julia with a very similar feature 24 | set. (It's semantically a Lisp, but it has "normal" syntax, very much 25 | like Julia) For this reason, many of the things he addresses in the 26 | talk are applicable to Julia as well. 27 | 28 | # Dispatching Design Patterns 29 | 30 | > design pattern: 31 | > 32 | > 1. A formal way of documenting a general reusable solution to a 33 | > design problem in a particular field of expertise. 34 | 35 | (wiktionary.org) 36 | 37 | > dispatch: 38 | > 39 | > [...] 40 | > 41 | > 7. To destroy quickly and efficiently. 42 | > 8. (computing) To pass on for further processing, 43 | > especially via a dispatch table [...] 44 | 45 | (also wiktionary.org) 46 | 47 | Julia is a different kind of programming language. Most Julia users 48 | come to the language because it combines the intuitive, high-level 49 | abstractions of dynamic languages like Python or Matlab with 50 | performance that rivals statically compiled languages like 51 | Fortran or C++ in some cases. 52 | 53 | However, while this is Julia's "claim to fame", there are many 54 | additional qualities which distinguish Julia from more mainstream 55 | programming languages. Specifically, Julia's type system looks more 56 | like the type systems found in functional programming 57 | languages---taking influence both from Common Lisp, as well as, 58 | perhaps to a lesser extent, from ML-style languages like Haskell 59 | and OCaml 60 | 61 | Perhaps the most striking thing for users coming from a language like 62 | Python, Java, or C++ is that Julia has no classes. I feel that what 63 | Julia has is much better, but for programmers oriented towards 64 | objects, Julia's approach can be disorienting. 65 | 66 | ## Dispatching the Gang of Four 67 | 68 | I'm pretty active on Quora.com, and one of the most frequent 69 | complaints I see there (and elsewhere) about Julia is that the lack of 70 | classes makes it difficult to use for large software projects. I 71 | believe this is because many of the most widely taught software design 72 | patterns presuppose the availability of classes in a language---though 73 | of course many are just complaining that you can't inherit data layout 74 | from a super type. I would remind those people that inheriting data 75 | layout breaks the principle of dependency inversion (i.e. one should 76 | depend on abstractions, not concretions) 77 | 78 | The good news for the former group (those who miss their 79 | object-oriented design patterns) is that Julia's high-level 80 | abstractions provide a built-in way to eliminate many of the problems 81 | these design patterns were originally intended to address. A classic 82 | example is the strategy pattern, where you may want to use one 83 | approach to a problem in a one context and approach in another 84 | context. In a language like Julia with first-class functions, 85 | different "strategies" (i.e. functions) can simply be passed around as 86 | function arguments, so classes are unnecessary. 87 | 88 | This is just one example, but if you want more, there is an old Peter 89 | Norvig talk called _Design Patterns in Dynamic Programming_ where he 90 | discusses these things, as well as patterns that emerge in dynamic 91 | programming languages---almost all of which are applicable to 92 | Julia. (The example language in the talk, Dylan, is one of Julia's 93 | nearest neighbors in terms of design) 94 | 95 | slides here: http://www.norvig.com/design-patterns/ 96 | 97 | In this sense, Julia "dispatches" many traditional design patterns by 98 | providing flexible high-level language features. 99 | 100 | However, the main thrust of this talk about design patterns that 101 | emerge specifically for Julia and how create fast, flexible data 102 | structures and interfaces in the Julian way using structs, methods, 103 | abstract types, and parameterized types---design patterns based on 104 | Julia's unique approach to method dispatch. 105 | 106 | 107 | For me, the best features of object orientation are the ability to 108 | encapsulate complexity in a simple interface and the ability to write 109 | flexible, polymorphic. Julia is one of the best languages out there 110 | when it comes to flexibility and polymorphism. It has good parts and 111 | bad parts when it comes to encapsulation, but I want to show you how 112 | to make the most of the good parts. 113 | 114 | ## Encapsulation 115 | 116 | ### Structs 117 | 118 | In Julia, the data layout of objects is defined as a `struct`. This is 119 | like C and many other languages. A `struct` definition looks like this: 120 | 121 | ```julia 122 | struct Point 123 | x::Float64 124 | y::Float64 125 | end 126 | ``` 127 | 128 | It's a block that has the name of the `struct` followed by a list of 129 | field names and their types. The types of fields are technically optional, 130 | but this is one of the few places in Julia where type declarations 131 | make a big difference for performance. Unless it's a case where 132 | performance absolutely doesn't matter, you should be putting types on 133 | your `struct` fields. Later we'll come back and look at how to make 134 | these fields polymorphic without sacrificing the performance. 135 | 136 | Defining a struct also creates a constructor: 137 | 138 | ```julia 139 | julia> mypoint = Point(5, 7) 140 | Point(5.0, 7.0) 141 | ``` 142 | 143 | Attribute access should also look familiar: 144 | 145 | ```julia 146 | julia> mypoint.x 147 | 5.0 148 | ``` 149 | 150 | One thing you may notice if you're familiar with more strict object 151 | oriented languages is that there is no privacy for struct fields in 152 | Julia. For better or for worse, Julia, like Python, relies on the 153 | discipline of the user not to rely on implementation details of a 154 | struct. I personally would like to see some form of enforced privacy 155 | in a future version of Julia, perhaps at a module level, but this is 156 | what we have for now. 157 | 158 | To package authors, I would recommend using the Python convention of 159 | prefixing field names of private attributes with an underscore, and to 160 | library users, I would recommend never accessing struct fields 161 | directly unless invited to do so in the package documentation. Relying 162 | on code that is considered an implementation detail by the package's 163 | author is a recipe for pain. 164 | 165 | On the bright side, Julia's structs are immutable by default, which I 166 | suppose is a limited form of access control---not to mention, it's a 167 | guarantee that the compiler can use to preform some interesting 168 | optimizations. 169 | 170 | If we try to change an immutable struct in place, we get an error: 171 | 172 | ```julia 173 | julia> mypoint.x = 3.0 174 | ERROR: setfield! immutable struct of type Point cannot be changed 175 | ``` 176 | 177 | This is often a good thing. In the case of a point, it's a way to 178 | designate a fixed location. However, perhaps we want to define an 179 | entity that can change locations: 180 | 181 | ```julia 182 | julia> mutable struct Starship 183 | name::String 184 | location::Point 185 | end 186 | 187 | julia> ship = Starship("U.S.S. Enterprise", Point(5, 7))y 188 | Starship("U.S.S. Enterprise", Point(5.0, 7.0)) 189 | ``` 190 | 191 | We can now "move" the ship by changing its location: 192 | 193 | ```julia 194 | ship.location = Point(6, 8) 195 | ``` 196 | 197 | Let's say we don't want to to use the `Point` constructor explicitly 198 | every time we create new ship. Adding an alternative constructor for 199 | an object is as easy as adding a new dispatch to a function: 200 | 201 | ```julia 202 | julia> Starship(name, x, y) = Starship(name, Point(x, y)) 203 | Starship 204 | 205 | julia> othership = Starship("U.S.S. Defiant", 10, 2) 206 | Starship("U.S.S. Defiant", Point(10.0, 2.0)) 207 | ``` 208 | 209 | You _override_ the default constructor for a struct by defining a 210 | constructor inside of the struct's definition, using the `new` keyword 211 | to access the default constructor internally: 212 | 213 | ```julia 214 | julia> mutable struct FancyStarship 215 | name::String 216 | location::Point 217 | FancyStarship(name, x, y) = new(name, Point(x, y)) 218 | end 219 | 220 | julia> fancy = FancyStarship("U.S.S. Discovery", 14, 32) 221 | fancy = FancyStarship("U.S.S. Discovery", 14, 32) 222 | ``` 223 | 224 | This could be used, for example, to insure that initialization values 225 | fall with a certain valid range. 226 | 227 | ### Methods 228 | 229 | In general, you don't want your user to have to care how your starship 230 | is implemented. You simply give an interface for how it acts. If 231 | someone wants to move their ship, a function should be supplied to 232 | allow them to do so without the need for extra math. 233 | 234 | ```julia 235 | function move!(starship, heading, distance) 236 | Δx = distance * cosd(heading) 237 | Δy = distance * sind(heading) 238 | old = starship.location 239 | starship.location = Point(old.x + Δx, old.y + Δy) 240 | end 241 | ``` 242 | 243 | Then we can move our ships like this: 244 | 245 | ```julia 246 | julia> foo_ship = Starship("Foo", 3, 4) 247 | Starship("Foo", Point(3.0, 4.0)) 248 | 249 | julia> move!(foo_ship, 45, 1.5) 250 | Point(4.060660171779821, 5.060660171779821) 251 | ``` 252 | 253 | This may be so obvious it goes without saying. Where it gets 254 | interesting is when we start adding multiple methods to functions for 255 | different types. Yes---in Julia, methods belong to functions, not to 256 | data types. However, methods can still be defined in terms of types 257 | (as well as number of arguments). 258 | 259 | As a very basic example, let's compare a struct for a square and a 260 | rectangle: 261 | 262 | ```julia 263 | struct Rectangle 264 | width::Float64 265 | height::Float64 266 | end 267 | width(r::Rectangle) = r.width 268 | height(r::Rectangle) = r.height 269 | 270 | struct Square 271 | length::Float64 272 | end 273 | width(s::Square) = s.length 274 | height(s::Square) = s.length 275 | ``` 276 | 277 | We need to know the width and height to define a rectangle, but for a 278 | square, we only need to store length of one side. However, since a 279 | Square is also a kind of rectangle, we want to give it the same 280 | interface, defining width and height functions for it as well. 281 | 282 | We use the type declarations here to show that these are different 283 | function methods for different types. The compiler keeps track of this 284 | information and will select the right method based on the input 285 | types. Note that type declarations on function parameters are _not_ 286 | used to improve performance. 287 | 288 | Once we have that basic rectangle interface, we can define an area 289 | function that uses this interface and does the right thing for both 290 | squares and rectangles: 291 | 292 | ```julia 293 | julia> area(shape) = width(shape) * height(shape) 294 | area (generic function with 1 method) 295 | 296 | julia> area(Rectangle(3, 4)) 297 | 12.0 298 | 299 | julia> area(Square(3)) 300 | 9.0 301 | ``` 302 | 303 | Because methods in Julia can be defined in terms of types, they can do 304 | everything methods can do in a language with classes---they can simply 305 | do other things as well! 306 | 307 | ## Polymorphism 308 | 309 | ### Abstract Types 310 | 311 | _Polymorphism_ in programming just means that you can reuse the same 312 | code for different types. Julia is really good at this. 313 | 314 | In the previous example, we defined an `area` function that would work 315 | with any type that provides `height` and `width` methods. This sort of 316 | "free" polymorphism is very common in Julia, but sometimes it can be 317 | useful to organize interfaces in a more constrained, hierarchical 318 | way. To do this, we would use abstract types. In Julia, abstract types 319 | have no data layout. They can only used for sub-typing and for 320 | type declarations on function methods. 321 | 322 | Continuing with shapes, let's define our first abstract type: 323 | 324 | ```julia 325 | """Types which inherit from `Shape` should provide an 326 | `area` method. 327 | """ 328 | abstract type Shape end 329 | ``` 330 | 331 | Julia doesn't currently provide a way to define interface constrains 332 | for subtypes, so I've added a doc string that explains the required 333 | interface for anyone who wishes to subtype from this abstract 334 | type. Now, let's give it a method: 335 | 336 | ```julia 337 | combined_area(a::Shape, b::Shape) = area(a) + area(b) 338 | ``` 339 | 340 | Now let's define struct that is a subtype of `Shape` and provides the 341 | necessary `area` interface: 342 | 343 | ```julia 344 | struct Circle <: Shape 345 | diameter::Float64 346 | end 347 | radius(c::Circle) = c.diameter / 2 348 | area(c::Circle) = π * radius(c) ^ 2 349 | ``` 350 | 351 | Form `TypeName <: AbstractType` is used to declare that `TypeName` is 352 | a subtype of `AbstractType` in the context of a struct definition. In 353 | an expression, the same syntax is used to test if one type is a 354 | subtype of another. 355 | 356 | An abstract type can also be a subtype of another abstract type: 357 | 358 | ```julia 359 | """Types which inherit from `AbstractRectangle should 360 | provide `height` and `width` methods. 361 | """ 362 | abstract type AbstractRectangle <: Shape end 363 | area(r::AbstractRectangle) = width(r) * height(r) 364 | ``` 365 | 366 | Here, we make an `AbstractRectangle` type which is a subtype of 367 | `Shape`, and provides a function for computing the area of a 368 | rectangle. From there, we can once again define our concrete rectangle 369 | types from earlier, but this time inheriting from `AbstractRectangle`: 370 | 371 | ```julia 372 | struct Rectangle <: AbstractRectangle 373 | width::Float64 374 | height::Float64 375 | end 376 | width(r::Rectangle) = r.width 377 | height(r::Rectangle) = r.height 378 | 379 | struct Square <: AbstractRectangle 380 | length::Float64 381 | end 382 | width(s::Square) = s.length 383 | height(s::Square) = s.length 384 | ``` 385 | 386 | Using this approach, we can combine the areas of different shapes in 387 | arbitrary ways: 388 | 389 | ```julia 390 | c = Circle(3) 391 | s = Square(3) 392 | r = Rectangle(3, 2) 393 | 394 | @assert combined_area(c, s) == 16.068583470577035 395 | @assert combined_area(s, r) == 15.0 396 | ``` 397 | 398 | Julia's method resolution algorithm can find the right execution path 399 | for each shape, even though the exact code is different in every 400 | case. What's more, in cases where the code is _type stable_, this 401 | polymorphism has no runtime cost. Cases that require runtime 402 | polymorphism do, of course, have a cost. 403 | 404 | ### Code organization with modules 405 | 406 | One possible downside of the flexibility of Julia's approach to 407 | defining structs and methods is that it doesn't provide an obvious 408 | method for code organization. Methods for different types can be 409 | defined anywhere. Sometimes this is useful, but it isn't necessarily 410 | the best way to organize your code. I've been working with OCaml a lot 411 | recently, and the way the language uses modules to encapsulate types 412 | got me thinking that a similar approach might be helpful in 413 | Julia. This should pattern should be seen as somewhat provisional, 414 | since I haven't observed it in Julia code in the wild. Nonetheless, 415 | here it is: 416 | ```julia 417 | module Shape 418 | abstract type T end 419 | area(shape::T) = throw(MethodError(area, shape)) 420 | combined_area(a::T, b::T) = area(a) + area(b) 421 | end 422 | 423 | 424 | module Circle 425 | import ..Shape 426 | 427 | struct T <: Shape.T 428 | diameter::Float64 429 | end 430 | radius(c::T) = c.diameter / 2 431 | Shape.area(c::T) = π * radius(c) ^ 2 432 | end 433 | ``` 434 | 435 | This approach is obviously quite boiler-plate-y for a short program, 436 | but I think it may be useful in larger projects and libraries, because 437 | it makes it explicit where the struct is implementing the interface of the 438 | abstract type, and it is also more friendly to tab completion 439 | (something people sometimes complain about in Julia), since the 440 | methods specific to a certain type are in the type's module. 441 | 442 | This is just one suggestion for how one might approach code 443 | organization in the absence of classes similar to what some other 444 | languages use. I'm putting it out into the universe to see what 445 | happens. 446 | 447 | ### Parametric Types: statically typed dynamic typing 448 | 449 | Parametric types (known as "generics" in some languages) don't really 450 | give you dynamic typing, but languages like Haskell and OCaml that use 451 | them everywhere can almost feel dynamically typed because of the 452 | flexibility they provide. Julia is already dynamically typed, but type 453 | parameters give extra information to the compiler to help it create 454 | efficient code without having to pin down specific types at dev time. 455 | 456 | Coming back to the `Point` example from earlier, one potential 457 | weakness is that it only works with `Float64` types. However, we might 458 | want it to work with other types as well. What we can do is declare a 459 | struct as a _type constructor_. This isn't the same as an object 460 | constructor. This is an incomplete type that takes another type as a 461 | parameter to complete it. Here's an example: 462 | 463 | ```julia 464 | julia> struct Point{T} 465 | x::T 466 | y::T 467 | end 468 | 469 | julia> Point(1, 3) 470 | Point{Int64}(1, 3) 471 | ``` 472 | 473 | Here, `struct Point{T}` shows that we are declaring a type constructor 474 | for concrete types where the type variable `T` is filled in with 475 | another type. `T` could be anything. It's just a convention. 476 | 477 | `x::T` and `y::T` shows that both `x` and `y` will be of type T when 478 | the value of T is known. The types for type variables can be specified 479 | manually with the constructor, but it is normally inferred from the 480 | input arguments. Here, we use `Int64`s as the input arguments, so 481 | `Int64` becomes the type parameter. Now, because both `x` and `y` are 482 | specified in terms of the same type variable, they must be the same 483 | type. 484 | 485 | ```julia 486 | julia> Point(1, 3.0) 487 | ERROR: MethodError: no method matching Point(::Int64, ::Float64) 488 | Closest candidates are: 489 | Point(::T, ::T) where T at REPL[2]:2 490 | ``` 491 | 492 | If we want different types (which would sort of be unusual in the 493 | context of a point, though perhaps there could be a good reason), we 494 | would use two type variables in the struct definition: 495 | 496 | ```julia 497 | julia> struct TwoTypePoint{X,Y} 498 | x::X 499 | y::Y 500 | end 501 | 502 | julia> TwoTypePoint(1, 3.0) 503 | TwoTypePoint{Int64,Float64}(1, 3.0) 504 | ``` 505 | 506 | One thing to keep in mind about the type variables we've used so far 507 | is that they are unconstrained, so they could literally be anything: 508 | 509 | ```julia 510 | julia> Point("foo", "bar") 511 | Point{String}("foo", "bar") 512 | ``` 513 | 514 | Obviously a point with strings for `x` and `y` is a pretty bad idea 515 | and breaks a lot of assumptions about what a point is by functions 516 | that might deal with this type. We probably want to limit the 517 | constructor to only working with numeric types. We _could_ do this with 518 | an absract type: 519 | 520 | ```julia 521 | julia> struct RealPoint 522 | x::Real 523 | y::Real 524 | end 525 | 526 | julia> RealPoint(0x5, 0xaa) 527 | RealPoint(0x05, 0xaa) 528 | ``` 529 | 530 | This _works_, but it doesn't insure that both `x` and `y` are the same 531 | concrete type, and much more importantly, it makes it impossible for 532 | the compiler to infer the concrete types of `x` and `y`, meaning it 533 | cannot optimize very well. 534 | 535 | What we can do instead is constrain the type variable: 536 | 537 | ```julia 538 | julia> struct Point{T <: Real} 539 | x::T 540 | y::T 541 | end 542 | 543 | julia> Point(1, 3) 544 | Point{Int64}(1, 3) 545 | 546 | julia> Point(1.4, 2.5) 547 | Point{Float64}(1.4, 2.5) 548 | 549 | julia> Point("foo", "bar") 550 | ERROR: MethodError: no method matching Point(::String, ::String) 551 | ``` 552 | 553 | Using this approach, we can make reasonable type constraints, keep 554 | good performance, and still keep our point from being limited to one 555 | concrete numeric type. 556 | 557 | Parameterized types are especially useful for defining container types 558 | that are meant to store all kinds of objects. As an example, we're 559 | going to define a linked list. This is not really a very practical 560 | data structure in Julia, but it's easy to define, and it shows the 561 | kind of situation where type variables are really useful. 562 | 563 | ```julia 564 | # the list itself 565 | struct Nil end 566 | 567 | struct List{T} 568 | head::T 569 | tail::Union{List{T}, Nil} 570 | end 571 | ``` 572 | 573 | So the empty struct, `Nil` is to signal the end of a list. The list 574 | itself has one field which is the value the node contains, and a 575 | second field which contains the rest of the list (which is either 576 | another instance of `List{T}` or `Nil`). 577 | 578 | ```julia 579 | # built a list from an array 580 | mklist(array::AbstractArray{T}) where T = 581 | foldr(List{T}, array, init=Nil()) 582 | ``` 583 | 584 | Next, we implement a function that create a list from an array (for 585 | demonstration purposes). 586 | 587 | ```julia 588 | # implement the iteration protocol 589 | Base.iterate(l::List) = iterate(l, l) 590 | Base.iterate(::List, l::List) = l.head, l.tail 591 | Base.iterate(::List, ::Nil) = nothing 592 | ``` 593 | 594 | Finally, we implement the iteration protocol on the list, which what 595 | `for` loops use internally. I don't want to cover this specific code 596 | in too much detail, but this is a common theme in Julia code: If you 597 | want to override part of Julia's syntax for your specific type, there 598 | is usually a function somewhere in Base that you can add methods to 599 | for your type. You have to look through the documentation to find 600 | them, but I have a link specifically for the iteration protocol in the 601 | notes. 602 | 603 | https://docs.julialang.org/en/v1/base/collections/#lib-collections-iteration-1 604 | 605 | The important thing here is that we have a basic implementation of a 606 | linked list here that is as efficient as it can be and works with any 607 | kind of value thanks to parametric types. 608 | 609 | ```julia 610 | julia> list = mklist(1:3) 611 | List{Int64}(1, List{Int64}(2, List{Int64}(3, Nil()))) 612 | 613 | julia> for val in list 614 | println(val) 615 | end 616 | 1 617 | 2 618 | 3 619 | 620 | julia> foreach(println, mklist(["foo", "bar"])) 621 | foo 622 | bar 623 | ``` 624 | 625 | ### The Trait Pattern 626 | 627 | One thing I don't love about Julia's design is that types can only 628 | inherit from one super type. Julia's types are strictly 629 | hierarchical. This doesn't always map well to real world problems. 630 | 631 | For example, `Int64` is a type of number, while `String` is a type of 632 | text. However, both things can be sorted. In some languages, like 633 | Haskell or Rust, you could explicitly add an `Ord` trait to these with 634 | the appropriate methods to implement an interface that allows 635 | ordering. They can implement methods from multiple traits for a more 636 | flexible interface. 637 | 638 | Julia doesn't have a language-level feature like this, and you could 639 | argue that it doesn't need it. You can simply add methods to support 640 | any interface you like without needing to say anything about it in 641 | terms of types. However, it still can be useful in terms of mentally 642 | mapping how different types in your code are related. In practical 643 | terms, it can be useful for dispatching to different strategies for 644 | different types. 645 | 646 | The trait pattern, sometimes called "the Holy trait" after Tim Holy, 647 | who suggested it on the Julia mailing list, emerged to address this 648 | usecase. It is now used a fair amount in `Base` and the Julia standard 649 | library. 650 | 651 | As an example, let's use our newly created linked-list: 652 | 653 | ```julia 654 | julia> map(uppercase, mklist(["foo", "bar", "baz"])) 655 | ERROR: MethodError: no method matching length(::List{String}) 656 | Closest candidates are: 657 | length(::Core.SimpleVector) at essentials.jl:596 658 | length(::Base.MethodList) at reflection.jl:852 659 | length(::Core.MethodTable) at reflection.jl:938 660 | ``` 661 | 662 | The error message reports that this doesn't work because `List` 663 | doesn't have a `length` method. This is true, but it's not the whole 664 | story. In order to be efficient, `map` tries to determine the length 665 | of the output in advance so it can allocate all the space needed for 666 | the new array in advance. *However*, this is not actually 667 | necessary. Julia arrays can be dynamically resized as they are built 668 | up, so there `map` could still theoretically work without a `length` 669 | method, and indeed, you can make it do this. Simply add 670 | `Base.IteratorSize` trait---in this case of the type 671 | `Base.SizeUnknown`. The funny thing in Julia is that the default 672 | 673 | ```julia 674 | julia> Base.IteratorSize(::Type{List}) = Base.SizeUnknown() 675 | 676 | julia> map(uppercase, mklist(["foo", "bar", "baz"])) 677 | 3-element Array{String,1}: 678 | "FOO" 679 | "BAR" 680 | "BAZ 681 | ``` 682 | 683 | Now, everything works as expected. 684 | 685 | What's going on? If we look at `generator.jl` in the source code for 686 | `Base`, we will find these lines: 687 | 688 | ```julia 689 | abstract type IteratorSize end 690 | struct SizeUnknown <: IteratorSize end 691 | struct HasLength <: IteratorSize end 692 | struct HasShape{N} <: IteratorSize end 693 | struct IsInfinite <: IteratorSize end 694 | ``` 695 | 696 | This is the beginning of how a trait is implemented. Just descriptions 697 | of different iterator sizes with no data layout. These traits exist 698 | purely to give the compiler extra information. If we look down a 699 | little further, we find code like this: 700 | 701 | ```julia 702 | IteratorSize(x) = IteratorSize(typeof(x)) 703 | IteratorSize(::Type) = HasLength() # HasLength is the default 704 | ``` 705 | 706 | For some reason, the default `IteratorSize` is `HasLength`. This is 707 | great for efficiency if your type actually has a length method, but 708 | leads to a rather unfortunate scenario if your data structure has no 709 | length, like if it is a generator, since the error you get gives no 710 | indication that there is any fix aside from implementing a `length` 711 | method. 712 | 713 | Anyway, you can use traits to efficiently implement similar patterns. 714 | This is a simple case where there are no sub-types of the trait. 715 | 716 | ```julia 717 | struct FooBar end 718 | 719 | # default case: error out 720 | FooBar(::T) where T = FooBar(T) 721 | FooBar(T::Type) = 722 | error("Type $T doesn't implement the FooBar interface.") 723 | 724 | add_foo_and_bar(x) = add_foo_and_bar(FooBar(x), x) 725 | add_foo_and_bar(::FooBar, x) = foo(x) + bar(x) 726 | ``` 727 | 728 | The downside here is that there is no way to tell if the type actually 729 | implements the required interface: 730 | 731 | ```julia 732 | julia> FooBar(Int) = FooBar() 733 | FooBar 734 | 735 | julia> add_foo_and_bar(3) 736 | ERROR: MethodError: no method matching foo(::Int64) 737 | ``` 738 | 739 | We could add a registration function to ensure a registered type has 740 | the correct interface beforehand: 741 | 742 | ```julia 743 | register_foobar(T::Type) = 744 | if hasmethod(foo, Tuple{T}) && hasmethod(bar, Tuple{T}) 745 | @eval FooBar(::Type{$T}) = FooBar() 746 | else 747 | error("Type $T must implement `foo` and `bar` methods") 748 | end 749 | ``` 750 | 751 | then: 752 | 753 | ```julia 754 | julia> register_foobar(Int) 755 | ERROR: Type Int64 must implement `foo` and `bar` methods 756 | 757 | julia> foo(x::Int) = x + 1 758 | foo (generic function with 2 methods) 759 | 760 | julia> bar(x::Int) = x * 2 761 | bar (generic function with 1 method) 762 | 763 | julia> register_foobar(Int) 764 | FooBar 765 | 766 | julia> add_foo_and_bar(3) 767 | 10 768 | ``` 769 | 770 | ## Dispatches for basic pattern matching 771 | 772 | In some functional languages, you can define different function 773 | definitions for different values. This is how one might define a 774 | factorial function in Haskell: 775 | 776 | ```haskell 777 | factorial 0 = 1 778 | factorial x = x * factorial (x-1) 779 | ``` 780 | 781 | That means, when the input argument is 0, the output is 1. For all 782 | other inputs, the second definition is used, which is defined 783 | recursively and will continue reducing the input on recursive calls by 784 | 1 until it reaches 0. 785 | 786 | You can't do exactly this in Julia (actually, you can if you encode 787 | numbers into types, but that makes the compiler sad). However, in 788 | practice, this feature is often used with tags that allow functions to 789 | deal with different input types. Because Julia functions dispatch 790 | based on types, that usecase actually is possible. 791 | 792 | One of the places this is most useful in Julia is when dealing with 793 | abstract syntax trees of the sort you interact with when defining 794 | macros, since you will often want to walk the syntax trees in a 795 | recursive way: 796 | 797 | ```julia 798 | macro replace_1_with_x(expr) 799 | esc(replace_1(expr)) 800 | end 801 | 802 | replace_1(atom) = atom == 1 ? :x : atom 803 | replace_1(e::Expr) = 804 | Expr(e.head, map(replace_1, e.args)...) 805 | ``` 806 | 807 | Here, we define an idiotic macro that replaces all instances of `1` 808 | with `x` in the code. Because abstract syntax trees are a recursively 809 | data structure composed of expressions containing lists of expressions 810 | and atoms, we can define actions on the nodes we're looking for while 811 | passing all the sub-nodes of an expression recursively to the same 812 | replace_1 function. 813 | 814 | ```julia 815 | julia> x = 10 816 | 10 817 | 818 | julia> @replace_1_with_x 5 + 1 819 | 15 820 | 821 | julia> @replace_1_with_x 5 + 1 * (3 + 1) 822 | 135 823 | 824 | julia> @macroexpand @replace_1_with_x 5 + 1 * (3 + 1) 825 | :(5 + x * (3 + x)) 826 | ``` 827 | 828 | This approach is also useful for intercepting other types of nodes in 829 | syntax trees, if you want them, and can be helpful when traversing any 830 | kind of recursively-defined data structure. The linked list from above 831 | is another good example. 832 | 833 | ```julia 834 | julia> Base.map(f, nil::Nil) = nil 835 | 836 | julia> Base.map(f, l::List) = List(f(l.head), map(f, l.tail)) 837 | 838 | julia> map(uppercase, mklist(["foo", "bar"])) 839 | List{String}("FOO", List{String}("BAR", Nil())) 840 | ``` 841 | -------------------------------------------------------------------------------- /ast_pattern_matching.jl: -------------------------------------------------------------------------------- 1 | macro replace_1_with_x(expr) 2 | esc(replace_1(expr)) 3 | end 4 | 5 | replace_1(atom) = atom == 1 ? :x : atom 6 | replace_1(e::Expr) = 7 | Expr(e.head, map(replace_1, e.args)...) 8 | 9 | -------------------------------------------------------------------------------- /foobar_trait.jl: -------------------------------------------------------------------------------- 1 | struct FooBar end 2 | 3 | # default case: error out 4 | FooBar(::T) where T = FooBar(T) 5 | FooBar(T::Type) = 6 | error("Type $T doesn't implement the FooBar interface.") 7 | 8 | add_foo_and_bar(x) = add_foo_and_bar(FooBar(x), x) 9 | add_foo_and_bar(::FooBar, x) = foo(x) + bar(x) 10 | 11 | register_foobar(T::Type) = 12 | if hasmethod(foo, Tuple{T}) && hasmethod(bar, Tuple{T}) 13 | @eval FooBar(::Type{$T}) = FooBar() 14 | else 15 | error("Type $T must implement `foo` and `bar` methods") 16 | end 17 | -------------------------------------------------------------------------------- /linkedlist.jl: -------------------------------------------------------------------------------- 1 | struct Nil end 2 | 3 | struct List{T} 4 | head::T 5 | tail::Union{List{T}, Nil} 6 | end 7 | 8 | # built a list from an array 9 | mklist(array::AbstractArray{T}) where T = 10 | foldr(List{T}, array, init=Nil()) 11 | 12 | # implement the iteration protocol 13 | Base.iterate(l::List) = iterate(l, l) 14 | Base.iterate(::List, l::List) = l.head, l.tail 15 | Base.iterate(::List, ::Nil) = nothing 16 | -------------------------------------------------------------------------------- /shapes.jl: -------------------------------------------------------------------------------- 1 | module Shape 2 | abstract type T end 3 | area(shape::T) = throw(MethodError(area, shape)) 4 | combined_area(a::T, b::T) = area(a) + area(b) 5 | end 6 | 7 | 8 | module Circle 9 | import ..Shape 10 | 11 | struct T <: Shape.T 12 | diameter::Float64 13 | end 14 | radius(c::T) = c.diameter / 2 15 | Shape.area(c::T) = π * radius(c) ^ 2 16 | end 17 | 18 | 19 | module AbstractRectangle 20 | import ..Shape 21 | 22 | abstract type T <: Shape.T end 23 | width(rectangle::T) = throw(MethodError(width, rectangle)) 24 | height(rectangle::T) = throw(MethodError(width, rectangle)) 25 | Shape.area(r::T) = width(r) * height(r) 26 | end 27 | 28 | 29 | module Rectangle 30 | import ..AbstractRectangle 31 | 32 | struct T <: AbstractRectangle.T 33 | width::Float64 34 | height::Float64 35 | end 36 | AbstractRectangle.width(r::T) = r.width 37 | AbstractRectangle.height(r::T) = r.height 38 | end 39 | 40 | 41 | module Square 42 | import ..AbstractRectangle 43 | 44 | struct T <: AbstractRectangle.T 45 | length::Float64 46 | end 47 | AbstractRectangle.width(s::T) = s.length 48 | AbstractRectangle.height(s::T) = s.length 49 | end 50 | 51 | c = Circle.T(3) 52 | s = Square.T(3) 53 | r = Rectangle.T(3, 2) 54 | 55 | @show Shape.combined_area(c, s) 56 | @show Shape.combined_area(s, r) 57 | -------------------------------------------------------------------------------- /slides.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dispatching Design Patterns 5 | 6 | 18 | 19 | 20 | 815 | 817 | 820 | 821 | 822 | -------------------------------------------------------------------------------- /strategy.jl: -------------------------------------------------------------------------------- 1 | happy_hour_price(x) = x÷2 2 | normal_price(x) = x 3 | 4 | struct Customer 5 | drinks::Vector{Int} 6 | Customer() = new(Int[]) 7 | end 8 | 9 | add_drink!(c::Customer, strategy, price, quantity) = 10 | push!(c.drinks, strategy(price * quantity)) 11 | 12 | function print_bill!(c::Customer) 13 | println("total due: ", sum(c.drinks)) 14 | empty!(c.drinks) 15 | end 16 | 17 | function main() 18 | strategy = normal_price 19 | add!(customer, price, quantity) = 20 | add_drink!(customer, strategy, price, quantity) 21 | 22 | first_customer = Customer() 23 | add!(first_customer, 100, 1) 24 | 25 | strategy = happy_hour_price 26 | add!(first_customer, 100, 2) 27 | 28 | second_customer = Customer() 29 | add!(second_customer, 80, 1) 30 | 31 | print_bill!(first_customer) 32 | 33 | strategy = normal_price 34 | add!(second_customer, 130, 2) 35 | add!(second_customer, 250, 1) 36 | print_bill!(second_customer) 37 | end 38 | 39 | main() 40 | -------------------------------------------------------------------------------- /strategy.md: -------------------------------------------------------------------------------- 1 | For example, here is a strategy pattern for calculating the price of 2 | drinks during happy hour in Java. This code even "cheats" a little 3 | because it's using the new Java syntax for anonymous functions: 4 | 5 | ```java 6 | import java.util.ArrayList; 7 | 8 | interface BillingStrategy { 9 | int getActPrice(int rawPrice); 10 | static BillingStrategy normalStrategy() { 11 | return rawPrice -> rawPrice; 12 | } 13 | static BillingStrategy happyHourStrategy() { 14 | return rawPrice -> rawPrice / 2; 15 | } 16 | } 17 | 18 | class Customer { 19 | private final List drinks = new ArrayList<>(); 20 | private BillingStrategy strategy; 21 | 22 | public Customer(BillingStrategy strategy) { 23 | this.strategy = strategy; 24 | } 25 | 26 | public void add(int price, int quantity) { 27 | this.drinks.add(this.strategy.getActPrice(price*quantity)); 28 | } 29 | 30 | public void printBill() { 31 | int sum = this.drinks.stream().mapToInt(v -> v).sum(); 32 | System.out.println("Total due: " + sum); 33 | this.drinks.clear(); 34 | } 35 | 36 | public void setStrategy(BillingStrategy strategy) { 37 | this.strategy = strategy; 38 | } 39 | } 40 | 41 | public class StrategyPattern { 42 | public static void main(String[] arguments) { 43 | BillingStrategy normalStrategy = BillingStrategy.normalStrategy(); 44 | BillingStrategy happyHourStrategy = BillingStrategy.happyHourStrategy(); 45 | 46 | Customer firstCustomer = new Customer(normalStrategy); 47 | firstCustomer.add(100, 1); 48 | 49 | firstCustomer.setStrategy(happyHourStrategy); 50 | firstCustomer.add(100, 2); 51 | 52 | Customer secondCustomer = new Customer(happyHourStrategy); 53 | secondCustomer.add(80, 1); 54 | 55 | firstCustomer.printBill(); 56 | 57 | secondCustomer.setStrategy(normalStrategy); 58 | secondCustomer.add(130, 2); 59 | secondCustomer.add(250, 1); 60 | secondCustomer.printBill(); 61 | } 62 | } 63 | ``` 64 | 65 | Here's a Julia program to do the same thing: 66 | 67 | ```julia 68 | happy_hour_price(x) = x÷2 69 | normal_price(x) = x 70 | 71 | struct Customer 72 | drinks::Vector{Int} 73 | end 74 | Customer() = Customer(Int[]) 75 | 76 | add_drink!(c::Customer, strategy, price, quantity) = 77 | push!(c.drinks, strategy(price * quantity)) 78 | 79 | function print_bill!(c::Customer) 80 | println("total due: ", sum(c.drinks)) 81 | empty!(c.drinks) 82 | end 83 | 84 | function main() 85 | strategy = normal_price 86 | add!(customer, price, quantity) = 87 | add_drink!(customer, strategy, price, quantity) 88 | 89 | first_customer = Customer() 90 | add!(first_customer, 100, 1) 91 | 92 | strategy = happy_hour_price 93 | add!(first_customer, 100, 2) 94 | 95 | second_customer = Customer() 96 | add!(second_customer, 80, 1) 97 | 98 | print_bill!(first_customer) 99 | 100 | strategy = normal_price 101 | add!(second_customer, 130, 2) 102 | add!(second_customer, 250, 1) 103 | print_bill!(second_customer) 104 | end 105 | 106 | main() 107 | ``` 108 | 109 | Let's forget about the main function for a moment because it's 110 | essentially the same in both cases. 111 | 112 | ```java 113 | interface BillingStrategy { 114 | int getActPrice(int rawPrice); 115 | static BillingStrategy normalStrategy() { 116 | return rawPrice -> rawPrice; 117 | } 118 | static BillingStrategy happyHourStrategy() { 119 | return rawPrice -> rawPrice / 2; 120 | } 121 | } 122 | 123 | class Customer { 124 | private final List drinks = new ArrayList<>(); 125 | private BillingStrategy strategy; 126 | 127 | public Customer(BillingStrategy strategy) { 128 | this.strategy = strategy; 129 | } 130 | 131 | public void add(int price, int quantity) { 132 | this.drinks.add(this.strategy.getActPrice(price*quantity)); 133 | } 134 | 135 | public void printBill() { 136 | int sum = this.drinks.stream().mapToInt(v -> v).sum(); 137 | System.out.println("Total due: " + sum); 138 | this.drinks.clear(); 139 | } 140 | 141 | public void setStrategy(BillingStrategy strategy) { 142 | this.strategy = strategy; 143 | } 144 | } 145 | ``` 146 | 147 | Compared with the Julia version: 148 | 149 | ```julia 150 | happy_hour_price(x) = x÷2 151 | normal_price(x) = x 152 | 153 | struct Customer 154 | drinks::Vector{Int} 155 | Customer() = new(Int[]) 156 | end 157 | 158 | add_drink!(c::Customer, strategy, price, quantity) = 159 | push!(c.drinks, strategy(price * quantity)) 160 | 161 | function print_bill!(c::Customer) 162 | println("total due: ", sum(c.drinks)) 163 | empty!(c.drinks) 164 | end 165 | ``` 166 | 167 | The use of first-class functions (i.e. functions as values) makes 168 | implementation of `Customer` much simpler and the implementation of 169 | `BillingStrategy` completely unnecessary. 170 | --------------------------------------------------------------------------------