~
38 | - Concepts TS
39 |
40 | #+BEGIN_NOTES
41 | Some of the features added to modern C++ to deal with types.
42 |
43 | Obviously someone thinks that types are important. And the ability to wrangle
44 | them and mould them to our purposes is an important part of C++.
45 |
46 | Types are the currency of metaprogramming, but also important for safety in
47 | "regular" programming; hence we see things like the GSL's ~owner<>~ and ~not_null<>~.
48 | #+END_NOTES
49 |
50 | ** FP isn't (only) about
51 | #+REVEAL_HTML:
52 | #+ATTR_REVEAL: :frag (appear)
53 | - first class functions
54 | - higher order functions
55 | - lexical scoping, closures
56 | - pattern matching
57 | - value semantics
58 | - immutability
59 | - concurrency through immutability
60 | - laziness
61 | - garbage collection
62 | - boxed data types / "inefficient" runtime models
63 | - the M-word
64 |
65 | #+BEGIN_NOTES
66 | Functional languages can teach us a thing or two about types. (After all, they
67 | seem to be teaching us everything else.)
68 |
69 | Here are some things you may think of when functional programming is mentioned...
70 | #+END_NOTES
71 |
72 | ** FP is (also, importantly) about
73 | #+REVEAL_HTML:
74 | #+ATTR_REVEAL: :frag (appear)
75 | - using types effectively and expressively
76 | - making illegal states unrepresentable
77 | - making illegal behaviour result in a type error
78 | - using total functions for easier to use, harder to misuse interfaces
79 |
80 | #+BEGIN_NOTES
81 | These aspects of functional programming are often overlooked.
82 |
83 | Many functional languages have well-developed, expressive type systems.
84 |
85 | C++ is moving in that direction.
86 | #+END_NOTES
87 |
88 | ** Why does C++ have a type system? :noexport:
89 | #+REVEAL_HTML:
90 | #+ATTR_REVEAL: :frag (appear appear appear appear) :frag_idx (1 2 3 4)
91 | - to help programmers?
92 | - to hinder programmers?
93 | - because objects?
94 | - for the compiler's benefit?
95 | #+REVEAL_HTML:
96 | #+ATTR_REVEAL: :frag appear :frag_idx 5
97 | What has a type system ever done for us?
98 |
99 | #+BEGIN_NOTES
100 | C++ has a stronger type system than C. Why?
101 |
102 | Is it so the compiler can complain about signed/unsigned comparisons?
103 |
104 | What use is a type system? This is the question this talk will help to answer.
105 | #+END_NOTES
106 |
107 | ** Why does C have a type system? :noexport:
108 | #+REVEAL_HTML:
109 | "The machines on which we first used BCPL and then B were word-addressed, and
110 | these languages' single data type, the 'cell,' comfortably equated with the
111 | hardware machine word. The advent of the PDP-11 exposed several inadequacies of
112 | B's semantic model.
113 |
114 | First, its character-handling mechanisms were clumsy. Second, although the
115 | original PDP-11 did not provide for floating-point arithmetic, the manufacturer
116 | promised that it would soon be available. Finally, the B and BCPL model implied
117 | overhead in dealing with pointers.
118 |
119 | For all these reasons, it seemed that a typing scheme was necessary. Other
120 | issues, particularly type safety and interface checking, did not seem as
121 | important then as they became later."
122 |
123 | #+REVEAL_HTML:
124 | -- dmr, [[https://www.bell-labs.com/usr/dmr/www/chist.html][/The Development of the C Language/]]
125 | #+REVEAL_HTML:
126 |
127 |
128 | The PDP-10 was old, with its 36-bit words.
129 |
130 | 1971/72: The PDP-11 was the new hotness.
131 |
132 | It could operate on (8-bit) bytes and (16-bit) words. If your language only
133 | operates on words ('cells'), string/char handling is awkward.
134 |
135 | In B, pointers were indices into arrays rather than naked addresses. So scale
136 | conversion would be needed at runtime.
137 |
138 | DEC promised floating point capability in hardware! So the C compiler would need
139 | to know about types in order to output the correct instructions.
140 | #+END_NOTES
141 |
142 | ** What is a type?
143 | #+REVEAL_HTML:
144 | #+ATTR_REVEAL: :frag (appear)
145 | - A way for the compiler to know what opcodes to output (dmr's motivation)?
146 | - The way data is stored (representational)?
147 | - Characterised by what operations are possible (behavioural)?
148 | - Determines the values that can be assigned?
149 | - Determines the meaning of the data?
150 |
151 | ** What is a type?
152 | [[./int_bool_1.png]]
153 |
154 | "Only Lua would have '~1 == true~' evaluate to ~false~. #wantmydayback"
155 |
156 | #+REVEAL_HTML:
157 | [[./int_bool_2.png]]
158 |
159 | "But, how can ~1~ be equal to ~true~? ~1~ is an integer, and ~true~ is a boolean. Lua
160 | seems to be correct here. It's your view of the world that has been warped."
161 |
162 | (Smiley faces make criticism OK!)
163 | #+REVEAL_HTML:
164 |
165 | ** What is a type?
166 | #+REVEAL_HTML:
167 | #+ATTR_REVEAL: :frag (appear)
168 | - The set of values that can inhabit an expression
169 | - may be finite or "infinite"
170 | - characterized by cardinality
171 | - Expressions have types
172 | - A program has a type
173 |
174 | ** Let's play a game
175 | #+ATTR_REVEAL: :frag appear
176 | To help us get thinking about types.
177 |
178 | #+ATTR_REVEAL: :frag appear
179 | I'll tell you a type.
180 |
181 | #+ATTR_REVEAL: :frag appear
182 | You tell me how many values it has.
183 |
184 | #+ATTR_REVEAL: :frag appear
185 | There are no tricks: if it seems obvious, it is!
186 |
187 | ** Level 1
188 | #+REVEAL_HTML:
189 | Types as sets of values
190 |
191 | ** Level 1
192 | How many values?
193 | #+BEGIN_SRC cpp
194 | bool;
195 | #+END_SRC
196 |
197 | #+ATTR_REVEAL: :frag appear
198 | 2 (~true~ and ~false~)
199 |
200 | ** Level 1
201 | How many values?
202 | #+BEGIN_SRC cpp
203 | char;
204 | #+END_SRC
205 |
206 | #+ATTR_REVEAL: :frag appear
207 | 256
208 |
209 | ** Level 1
210 | How many values?
211 | #+BEGIN_SRC cpp
212 | void;
213 | #+END_SRC
214 |
215 | #+ATTR_REVEAL: :frag appear
216 | 0
217 |
218 | #+ATTR_REVEAL: :frag appear
219 | #+BEGIN_SRC cpp
220 | struct Foo { Foo() = delete; };
221 | #+END_SRC
222 |
223 | #+ATTR_REVEAL: :frag appear
224 | #+BEGIN_SRC cpp
225 | struct Bar { template Bar(); };
226 | #+END_SRC
227 |
228 | #+BEGIN_NOTES
229 | cf BASIC's function vs procedure
230 | #+END_NOTES
231 |
232 | ** Level 1
233 | How many values?
234 | #+BEGIN_SRC cpp
235 | struct Foo {};
236 | #+END_SRC
237 |
238 | #+ATTR_REVEAL: :frag appear
239 | 1
240 |
241 | ** Level 1
242 | How many values?
243 | #+BEGIN_SRC cpp
244 | enum FireSwampDangers : int8_t {
245 | FLAME_SPURTS,
246 | LIGHTNING_SAND,
247 | ROUSES
248 | };
249 | #+END_SRC
250 |
251 | #+ATTR_REVEAL: :frag appear
252 | 3
253 |
254 | #+BEGIN_NOTES
255 | It is possible to put something into FireSwampDangers that fits
256 | representationally (eg. the value 4). But that would be meaningless: there would
257 | be no connection between the value represented and its interpretation. Because
258 | there is no interpretation: cf. an unconstructed object.
259 | #+END_NOTES
260 |
261 | ** Level 1
262 | How many values?
263 | #+BEGIN_SRC cpp
264 | template
265 | struct Foo {
266 | T m_t;
267 | };
268 | #+END_SRC
269 |
270 | #+ATTR_REVEAL: :frag appear
271 | ~Foo~ has as many values as ~T~
272 |
273 | ** End of Level 1
274 | Algebraically, a type is the number of values that inhabit it.
275 |
276 | These types are equivalent:
277 | #+BEGIN_SRC cpp
278 | bool;
279 |
280 | enum class InatorButtons {
281 | ON_OFF,
282 | SELF_DESTRUCT
283 | };
284 | #+END_SRC
285 |
286 | #+ATTR_REVEAL: :frag appear
287 | Let's move on to level 2.
288 |
289 | ** Level 2
290 | #+REVEAL_HTML:
291 | Aggregating Types
292 |
293 | ** Level 2
294 | How many values?
295 | #+BEGIN_SRC cpp
296 | std::pair;
297 | #+END_SRC
298 |
299 | #+ATTR_REVEAL: :frag appear
300 | 256 * 2 = 512
301 |
302 | ** Level 2
303 | How many values?
304 | #+BEGIN_SRC cpp
305 | struct Foo {
306 | char a;
307 | bool b;
308 | };
309 | #+END_SRC
310 |
311 | #+ATTR_REVEAL: :frag appear
312 | 256 * 2 = 512
313 |
314 | ** Level 2
315 | How many values?
316 | #+BEGIN_SRC cpp
317 | std::tuple;
318 | #+END_SRC
319 |
320 | #+ATTR_REVEAL: :frag appear
321 | 2 * 2 * 2 = 8
322 |
323 | ** Level 2
324 | How many values?
325 | #+BEGIN_SRC cpp
326 | template
327 | struct Foo {
328 | T m_t;
329 | U m_u;
330 | };
331 | #+END_SRC
332 |
333 | #+ATTR_REVEAL: :frag appear
334 | (# of values in ~T~) * (# of values in ~U~)
335 |
336 | ** End of Level 2
337 | When two types are "concatenated" into one compound type, we _multiply_ the # of
338 | inhabitants of the components.
339 |
340 | This kind of compounding gives us a _product type_.
341 |
342 | #+ATTR_REVEAL: :frag appear
343 | On to Level 3.
344 |
345 | ** Level 3
346 | #+REVEAL_HTML:
347 | Alternating Types
348 |
349 | ** Level 3
350 | How many values?
351 | #+BEGIN_SRC cpp
352 | std::optional;
353 | #+END_SRC
354 |
355 | #+ATTR_REVEAL: :frag appear
356 | 256 + 1 = 257
357 |
358 | ** Level 3
359 | How many values?
360 | #+BEGIN_SRC cpp
361 | std::variant;
362 | #+END_SRC
363 |
364 | #+ATTR_REVEAL: :frag appear
365 | 256 + 2 = 258
366 |
367 | ** Level 3
368 | How many values?
369 | #+BEGIN_SRC cpp
370 | template
371 | struct Foo {
372 | std::variant;
373 | }
374 | #+END_SRC
375 |
376 | #+ATTR_REVEAL: :frag appear
377 | (# of values in ~T~) + (# of values in ~U~)
378 |
379 | ** End of Level 3
380 | When two types are "alternated" into one compound type, we _add_ the # of
381 | inhabitants of the components.
382 |
383 | This kind of compounding gives us a _sum type_.
384 |
385 | ** Level 4
386 | #+REVEAL_HTML:
387 | Function Types
388 |
389 | ** Level 4
390 | How many values?
391 | #+begin_src c++
392 | bool f(bool);
393 | #+end_src
394 |
395 | #+ATTR_REVEAL: :frag appear
396 | 4
397 |
398 | ** Level 4
399 | Four possible values
400 | [[./function_bool.svg]]
401 |
402 | ** Level 4
403 | #+begin_src c++
404 | bool f1(bool b) { return b; }
405 | bool f2(bool) { return true; }
406 | bool f3(bool) { return false; }
407 | bool f4(bool b) { return !b; }
408 | #+end_src
409 |
410 | ** Level 4
411 | How many values?
412 | #+begin_src c++
413 | char f(bool);
414 | #+end_src
415 |
416 | #+ATTR_REVEAL: :frag appear
417 | 256 * 256 = 65,536
418 |
419 | ** Level 4
420 | How many values (for ~f~)?
421 | #+begin_src c++
422 | enum class Foo
423 | {
424 | BAR,
425 | BAZ,
426 | QUUX
427 | };
428 | char f(Foo);
429 | #+end_src
430 |
431 | #+ATTR_REVEAL: :frag appear
432 | 256 * 256 * 256 = 16,777,216
433 |
434 | ** Level 4
435 | The number of values of a function is the number of different ways we can draw
436 | arrows between the inputs and the outputs.
437 | [[./function.svg]]
438 |
439 | ** Level 4
440 | How many values?
441 | #+begin_src c++
442 | template
443 | U f(T);
444 | #+end_src
445 |
446 | #+ATTR_REVEAL: :frag appear
447 | $|U|^{|T|}$
448 |
449 | ** End of Level 4
450 | When we have a _function_ from $A$ to $B$, we raise the # of inhabitants of
451 | $B$ to the power of the # of inhabitants of $A$.
452 |
453 | ** End of Level 4 (corollary)
454 | Hence a curried function is equivalent to its uncurried alternative.
455 |
456 |
457 | $$\begin{align*}
458 | F_{uncurried}::(A,B) \rightarrow C & \Leftrightarrow C^{A*B} \\
459 | & = C^{B*A} \\
460 | & = (C^B)^A \\
461 | & \Leftrightarrow (B \rightarrow C)^A \\
462 | & \Leftrightarrow F_{curried}::A \rightarrow (B \rightarrow C)
463 | \end{align*}$$
464 |
465 | ** Victory!
466 |
467 | #+REVEAL_HTML:
🏆
ACHIEVEMENT UNLOCKED
Algebraic Datatypes 101
468 |
469 | ** Equivalences
470 | #+BEGIN_SRC cpp
471 | template
472 | struct Foo {
473 | std::variant m_v;
474 | };
475 |
476 | template
477 | struct Bar {
478 | T m_t;
479 | bool m_b;
480 | };
481 | #+END_SRC
482 |
483 | We have a choice over how to represent values. ~std::variant~ will quickly
484 | become a very important tool for proper expression of states.
485 |
486 | This is one reason why ~std::variant~'s "never-empty" guarantee is important.
487 |
488 | #+BEGIN_NOTES
489 | T + T = 2T.
490 |
491 | But note that in ~Bar~, we need to manually keep the two variables "in sync".
492 | #+END_NOTES
493 |
494 | ** Algebraic Datatypes
495 | This is what it means to have an algebra of datatypes.
496 |
497 | #+ATTR_REVEAL: :frag (appear)
498 | - the ability to reason about equality of types
499 | - to find equivalent formulations
500 | - more natural
501 | - more easily understood
502 | - more efficient
503 | - to identify mismatches between state spaces and the types used to implement
504 | them
505 | - to eliminate illegal states by making them inexpressible
506 |
507 | ** Making Illegal States Unrepresentable
508 | ~std::variant~ is a game changer because it allows us to (more) properly express
509 | types, so that (more) illegal states are unrepresentable.
510 |
511 | [[./variant-tweet.png]]
512 |
513 | #+BEGIN_NOTES
514 | C++'s type system is still not perfect by a long shot. But ~std::variant~ is an
515 | amazing upgrade.
516 | #+END_NOTES
517 |
518 | ** Making Illegal States Unrepresentable
519 | Let's look at some possible alternative data formulations, using sum types
520 | (~variant~, ~optional~) as well as product types (structs).
521 |
522 | ** Example: Connection State
523 | #+BEGIN_SRC cpp
524 | enum class ConnectionState {
525 | DISCONNECTED,
526 | CONNECTING,
527 | CONNECTED,
528 | CONNECTION_INTERRUPTED
529 | };
530 |
531 | struct Connection {
532 | ConnectionState m_connectionState;
533 |
534 | std::string m_serverAddress;
535 | ConnectionId m_id;
536 | std::chrono::system_clock::time_point m_connectedTime;
537 | std::chrono::milliseconds m_lastPingTime;
538 | Timer m_reconnectTimer;
539 | };
540 | #+END_SRC
541 |
542 | #+BEGIN_NOTES
543 | A very simple example of what a connection class might look like today.
544 |
545 | Functions interacting with this class would typically use a switch statement
546 | over the ~ConnectionState~.
547 |
548 | There are hidden invariants here that aren't enforced by the Connection type.
549 |
550 | Some of the fields are dependent on the connection state (reconnect time, last
551 | ping time). So it seems that some of these fields need sentinel values (eg
552 | invalid connection id).
553 |
554 | Worse, there is temptation to reuse fields for multiple states. Connected
555 | timestamp is perhaps likely to get reused to mean the instant of connection and
556 | the instant of disconnection.
557 | #+END_NOTES
558 |
559 | ** Example: Connection State
560 | #+BEGIN_SRC cpp
561 | struct Connection {
562 | std::string m_serverAddress;
563 |
564 | struct Disconnected {};
565 | struct Connecting {};
566 | struct Connected {
567 | ConnectionId m_id;
568 | std::chrono::system_clock::time_point m_connectedTime;
569 | std::optional m_lastPingTime;
570 | };
571 | struct ConnectionInterrupted {
572 | std::chrono::system_clock::time_point m_disconnectedTime;
573 | Timer m_reconnectTimer;
574 | };
575 |
576 | std::variant m_connection;
580 | };
581 | #+END_SRC
582 |
583 | #+BEGIN_NOTES
584 | With types structured correctly, it's not possible to express illegal states.
585 |
586 | e.g. Ping time does not exist if we're not connected.
587 |
588 | (There are still things that are common to all states, e.g. perhaps this class
589 | represents connection to a specific server.)
590 |
591 | A switch statement could still exist, switching on the ~variant~'s ~index()~, or
592 | a visitor-based approach could be used.
593 | #+END_NOTES
594 |
595 | ** Example: Nullable field
596 | #+REVEAL_HTML:
597 | #+BEGIN_SRC cpp
598 | class Friend {
599 | std::string m_alias;
600 | bool m_aliasPopulated;
601 | ...
602 | };
603 | #+END_SRC
604 | These two fields need to be kept in sync everywhere.
605 |
606 | #+BEGIN_NOTES
607 | Here, a field is populated from a remote source and happens lazily and/or
608 | asynchronously. It is possible that the field never gets populated.
609 |
610 | All the code that deals with this field has to ensure that both variables are
611 | kept up to date in sync with each other.
612 | #+END_NOTES
613 |
614 | ** Example: Nullable field
615 | #+REVEAL_HTML:
616 | #+BEGIN_SRC cpp
617 | class Friend {
618 | std::optional m_alias;
619 | ...
620 | };
621 | #+END_SRC
622 | ~std::optional~ provides a sentinel value that is outside the type.
623 |
624 | #+BEGIN_NOTES
625 | ~std::optional~ captures the true state space of the variable. It is not
626 | possible for two fields to get out of step now.
627 | #+END_NOTES
628 |
629 | ** Example: Monster AI
630 | #+REVEAL_HTML:
631 | #+BEGIN_SRC cpp
632 | enum class AggroState {
633 | IDLE,
634 | CHASING,
635 | FIGHTING
636 | };
637 |
638 | class MonsterAI {
639 | AggroState m_aggroState;
640 |
641 | float m_aggroRadius;
642 | PlayerId m_target;
643 | Timer m_chaseTimer;
644 | };
645 | #+END_SRC
646 |
647 | #+BEGIN_NOTES
648 | Once again, presumably PlayerId has some invalid sentinel value.
649 | #+END_NOTES
650 |
651 | ** Example: Monster AI
652 | #+REVEAL_HTML:
653 | #+BEGIN_SRC cpp
654 | class MonsterAI {
655 | struct Idle {
656 | float m_aggroRadius;
657 | };
658 | struct Chasing {
659 | PlayerId m_target;
660 | Timer m_chaseTimer;
661 | };
662 | struct Fighting {
663 | PlayerId m_target;
664 | };
665 |
666 | std::variant m_aggroState;
667 | };
668 | #+END_SRC
669 |
670 | #+BEGIN_NOTES
671 | Now the variables are properly placed into the states that use them.
672 |
673 | Chasing and Fighting states could inherit from an Aggroed state that holds a target.
674 | #+END_NOTES
675 |
676 | ** Example: Design Patterns
677 | The addition of sum types to C++ offers an alternative formulation for some
678 | design patterns.
679 |
680 | State machines and expressions are naturally modelled with sum types.
681 |
682 | #+BEGIN_NOTES
683 | Traditional runtime polymorphism approach can lead to bloated base class issue.
684 |
685 | Type erasure is another way to go.
686 |
687 | Sum types + visitor/pattern matching is a third possibility, particularly
688 | natural for things like ASTs.
689 | #+END_NOTES
690 |
691 | ** Example: Design Patterns
692 | - Command
693 | - Composite
694 | - State
695 | - Interpreter
696 |
697 | #+BEGIN_NOTES
698 | Command uses a flat, wide class hierarchy to encapsulate requests in objects.
699 |
700 | Composite: model part-whole hierarchies with uniform interface.
701 |
702 | State is obvious: simply replace the contained polymorphic object with a
703 | variant.
704 |
705 | Sum types are especially good for representing expressions (think JSON).
706 |
707 | Interpreter tackles the expression problem: easy to add new classes (use
708 | OO/interfaces) or new operations (use sum types/visitors)?
709 | #+END_NOTES
710 |
711 | ** Sum types vs Runtime Polymorphism :noexport:
712 | Runtime polymorphism (i.e. regular OO interface/implementation) allows manipulation of
713 | heterogeneous state with a uniform interface.
714 |
715 | Sum types allow manipulation of heterogenous state /and/ interface in a homogeneous way.
716 |
717 | #+BEGIN_NOTES
718 | This slide paraphrased from Andrei Alexandrescu's Dr Dobbs article, April 2002 (!)
719 | #+END_NOTES
720 |
721 | ** Designing with Types
722 | #+REVEAL_HTML:
723 | ~std::variant~ and ~std::optional~ are valuable tools that allow us to model the
724 | state of our business logic more accurately.
725 |
726 | When you match the types to the domain accurately, certain categories of tests
727 | just disappear.
728 |
729 | #+BEGIN_NOTES
730 | You don't have to test the edge cases where the representation can fall outside
731 | your reality - because that can't happen.
732 | #+END_NOTES
733 |
734 | ** Designing with Types
735 | #+REVEAL_HTML:
736 | Fitting types to their function more accurately makes code easier to understand
737 | and removes pitfalls.
738 |
739 | The bigger the codebase and the more vital the functionality, the more value
740 | there is in correct representation with types.
741 |
742 | #+BEGIN_NOTES
743 | When illegal states are unrepresentable, you don't have to worry about other
744 | programmers misunderstanding the code, or misusing data. In a sense, they
745 | /cannot/ write something that is wrong.
746 |
747 | And when I say "other programmers" of course I mean myself in 3 months...
748 |
749 | Questionably reusing fields, bending semantics, etc. These are bad practices.
750 | But they happen when we're chasing a deadline.
751 | #+END_NOTES
752 |
753 | ** Using Types to Constrain Behaviour
754 | #+REVEAL_HTML:
755 | We've seen how an expressive type system (with product and sum types) allows us
756 | to model state more accurately.
757 |
758 | "Phantom types" is one technique that helps us to model the /behaviour/ of our
759 | business logic in the type system. Illegal behaviour becomes a type error.
760 |
761 | ** Phantom Types: Before
762 | #+REVEAL_HTML:
763 | #+BEGIN_SRC cpp
764 | std::string GetFormData();
765 |
766 | std::string SanitizeFormData(const std::string&);
767 |
768 | void ExecuteQuery(const std::string&);
769 | #+END_SRC
770 | An injection bug waiting to happen.
771 |
772 | #+BEGIN_NOTES
773 | Let's hope we don't meet little Bobby Tables, and that everywhere we execute a
774 | query we remembered to sanitize the data provided by the user.
775 |
776 | The type system is not helping us here. How can we use types to make sure that
777 | we stay safe?
778 | #+END_NOTES
779 |
780 | ** Phantom Types: The setup
781 | #+REVEAL_HTML:
782 | #+BEGIN_SRC cpp
783 | template
784 | struct FormData {
785 | explicit FormData(const string& input) : m_input(input) {}
786 | std::string m_input;
787 | };
788 |
789 | struct sanitized {};
790 | struct unsanitized {};
791 | #+END_SRC
792 | ~T~ is the "Phantom Type" here.
793 |
794 | #+BEGIN_NOTES
795 | Note that the template argument is unused. It exists _only_ for compile time
796 | type checking. There is no runtime overhead.
797 | #+END_NOTES
798 |
799 | ** Phantom Types: After
800 | #+REVEAL_HTML:
801 | #+BEGIN_SRC cpp
802 | FormData GetFormData();
803 |
804 | std::optional>
805 | SanitizeFormData(const FormData&);
806 |
807 | void ExecuteQuery(const FormData&);
808 | #+END_SRC
809 |
810 | #+BEGIN_NOTES
811 | User input is born unsanitized.
812 |
813 | It is impossible for us to execute unsanitized input. The compiler simply won't
814 | compile it.
815 |
816 | We've used types to help enforce the business logic.
817 |
818 | This is something similar to a strong typedef, or what enum class effectively
819 | does for integral types. This technique can also be used e.g. in a units
820 | library.
821 | #+END_NOTES
822 |
823 | ** Total Functions
824 | #+REVEAL_HTML:
825 | A /total function/ is a function that is defined for all inputs in its domain.
826 |
827 | #+ATTR_REVEAL: :frag appear
828 | ~template
829 | const T& min(const T& a, const T& b);~
830 |
831 | #+ATTR_REVEAL: :frag appear
832 | ~float sqrt(float f);~
833 |
834 | #+BEGIN_NOTES
835 | We are straying into the realm of Concepts here.
836 |
837 | I'm not saying that total is the same thing as "no preconditions". The type must
838 | satisfy the requirements on it. But you can see that with functions like ~sqrt~
839 | there is a clear mismatch between the type of the function and the actual type
840 | of its domain.
841 | #+END_NOTES
842 |
843 | ** Let's play another game
844 | #+ATTR_REVEAL: :frag appear
845 | To help us see how total functions with the right types can result in
846 | unsurprising code.
847 |
848 | #+ATTR_REVEAL: :frag appear
849 | I'll give you a function signature with no names attached.
850 |
851 | #+ATTR_REVEAL: :frag appear
852 | You tell me what it's called... (and you'll even know how to implement it).
853 |
854 | #+ATTR_REVEAL: :frag appear
855 | The only rule... it must be a /total/ function.
856 |
857 | #+BEGIN_NOTES
858 | Assume regular types. Assume that the function is doing something "interesting"
859 | rather than "boring" when you have a choice. (ie. that it uses its argument).
860 | But you needn't assume anything else.
861 |
862 | And there are always ways to make things unexpected in C++. But assume nothing
863 | surprising here.
864 | #+END_NOTES
865 |
866 | ** Name That Function
867 | #+REVEAL_HTML:
868 | #+BEGIN_SRC cpp
869 | template
870 | T f(T);
871 | #+END_SRC
872 |
873 | #+ATTR_REVEAL: :frag appear
874 | ~identity~
875 |
876 | #+ATTR_REVEAL: :frag appear
877 | #+BEGIN_SRC cpp
878 | int f(int);
879 | #+END_SRC
880 |
881 | #+BEGIN_NOTES
882 | Note the odd situation here: we know more about ~f(T)~ than we do about
883 | ~f(int)~.
884 | #+END_NOTES
885 |
886 | ** Name That Function
887 | #+REVEAL_HTML:
888 | #+BEGIN_SRC cpp
889 | template
890 | T f(pair);
891 | #+END_SRC
892 |
893 | #+ATTR_REVEAL: :frag appear
894 | ~first~
895 |
896 | ** Name That Function
897 | #+REVEAL_HTML:
898 | #+BEGIN_SRC cpp
899 | template
900 | T f(bool, T, T);
901 | #+END_SRC
902 |
903 | #+ATTR_REVEAL: :frag appear
904 | ~select~
905 |
906 | ** Name That Function
907 | #+REVEAL_HTML:
908 | #+BEGIN_SRC cpp
909 | template
910 | U f(function, T);
911 | #+END_SRC
912 |
913 | #+ATTR_REVEAL: :frag appear
914 | ~apply~ or ~call~
915 |
916 | ** Name That Function
917 | #+REVEAL_HTML:
918 | #+BEGIN_SRC cpp
919 | template
920 | vector f(vector);
921 | #+END_SRC
922 |
923 | #+ATTR_REVEAL: :frag appear
924 | ~reverse~, ~shuffle~, ...
925 |
926 | #+BEGIN_NOTES
927 | For simplicity, I haven't written this signature in terms of iterators, but it
928 | would be just the same.
929 | #+END_NOTES
930 |
931 | ** Name That Function
932 | #+REVEAL_HTML:
933 | #+BEGIN_SRC cpp
934 | template
935 | T f(vector);
936 | #+END_SRC
937 |
938 | #+ATTR_REVEAL: :frag appear
939 | Not possible! It's a partial function - the ~vector~ might be empty.
940 |
941 | #+ATTR_REVEAL: :frag appear
942 | #+BEGIN_SRC cpp
943 | T& vector::front();
944 | #+END_SRC
945 |
946 | ** Name That Function
947 | #+REVEAL_HTML:
948 | #+BEGIN_SRC cpp
949 | template
950 | optional f(vector);
951 | #+END_SRC
952 |
953 | ** Name That Function
954 | #+REVEAL_HTML:
955 | #+BEGIN_SRC cpp
956 | template
957 | vector f(function, vector);
958 | #+END_SRC
959 |
960 | #+ATTR_REVEAL: :frag appear
961 | ~transform~
962 |
963 | ** Name That Function
964 | #+REVEAL_HTML:
965 | #+BEGIN_SRC cpp
966 | template
967 | vector f(function, vector);
968 | #+END_SRC
969 |
970 | #+ATTR_REVEAL: :frag appear
971 | ~remove_if~, ~partition~, ...
972 |
973 | ** Name That Function
974 | #+REVEAL_HTML:
975 | #+BEGIN_SRC cpp
976 | template
977 | T f(optional);
978 | #+END_SRC
979 |
980 | #+ATTR_REVEAL: :frag appear
981 | Not possible!
982 |
983 | ** Name That Function
984 | #+REVEAL_HTML:
985 | #+BEGIN_SRC cpp
986 | template
987 | V f(map, K);
988 | #+END_SRC
989 |
990 | #+ATTR_REVEAL: :frag appear
991 | Not possible! (The key might not be in the ~map~.)
992 |
993 | #+ATTR_REVEAL: :frag appear
994 | #+BEGIN_SRC cpp
995 | V& map::operator[](const K&);
996 | #+END_SRC
997 |
998 | ** Name That Function
999 | #+REVEAL_HTML:
1000 | #+BEGIN_SRC cpp
1001 | template
1002 | optional f(map, K);
1003 | #+END_SRC
1004 |
1005 | #+ATTR_REVEAL: :frag appear
1006 | ~lookup~
1007 |
1008 | ** What Just Happened?
1009 | I gave you /almost nothing/.
1010 |
1011 | No variable names. No function names. No type names.
1012 |
1013 | Just bare type signatures.
1014 |
1015 | #+ATTR_REVEAL: :frag appear
1016 | You were able to tell me exactly what the functions should be called, and likely
1017 | knew instantly how to implement them.
1018 |
1019 | #+ATTR_REVEAL: :frag appear
1020 | You will note that partial functions gave us some issues...
1021 |
1022 | #+BEGIN_NOTES
1023 | Naming is one of the hardest problems in Comp Sci. Getting the types right is
1024 | much easier. And if your types model the logic properly, perhaps you have
1025 | "self-documenting code"?
1026 | #+END_NOTES
1027 |
1028 | ** Well-typed Functions
1029 | #+REVEAL_HTML:
1030 | Writing /total functions/ with well-typed signatures can tell us a lot about
1031 | functionality.
1032 |
1033 | Using types appropriately makes interfaces unsurprising, safer to use and harder
1034 | to misuse.
1035 |
1036 | Total functions make more test categories vanish.
1037 |
1038 | ** About Testing...
1039 | In a previous talk, I talked about unit testing and in particular property-based testing.
1040 |
1041 | #+ATTR_REVEAL: :frag appear
1042 | Effectively using types can reduce test code.
1043 |
1044 | #+ATTR_REVEAL: :frag appear
1045 | Property-based tests say "for all values, this property is true".
1046 |
1047 | #+ATTR_REVEAL: :frag appear
1048 | That is exactly what types /are/: universal quantifications about what can be
1049 | done with data.
1050 |
1051 | #+ATTR_REVEAL: :frag appear
1052 | Types scale better than tests. Instead of TDD, maybe try TDD!
1053 |
1054 | #+BEGIN_NOTES
1055 | C++'s type system isn't yet powerful enough to be able to say goodbye to tests,
1056 | but it is powerful enough that used effectively, we can reduce some of the
1057 | drudgery of writing tests.
1058 |
1059 | Any time you're thinking something is true for all values, that's what a type
1060 | can do.
1061 | #+END_NOTES
1062 |
1063 | ** Further Down the Rabbit Hole
1064 | #+REVEAL_HTML:
1065 | - http://en.wikipedia.org/wiki/Algebraic_data_type
1066 | - http://chris-taylor.github.io/blog/2013/02/10/the-algebra-of-algebraic-data-types/
1067 | - https://vimeo.com/14313378 (Effective ML: Making Illegal States Unrepresentable)
1068 | - http://www.infoq.com/presentations/Types-Tests (Types vs Tests: Strange Loop 2012)
1069 |
1070 | ** Thanks For Listening
1071 | #+REVEAL_HTML:
1072 | "On the whole, I'm inclined to say that when in doubt, make a new type."
1073 | #+REVEAL_HTML:
1074 | -- Martin Fowler, [[http://martinfowler.com/ieeeSoftware/whenType.pdf][/When to Make a Type/]]
1075 | #+REVEAL_HTML:
1076 | "Don't set a flag; set the data."
1077 | #+REVEAL_HTML:
1078 | -- Leo Brodie, /[[http://thinking-forth.sourceforge.net/][Thinking Forth]]/
1079 | #+REVEAL_HTML:
1080 |
1081 | ** Goals for Well-typed Code
1082 | - Make illegal states unrepresentable
1083 | - Use ~std::variant~ and ~std::optional~ for formulations that
1084 | - are more natural
1085 | - fit the business logic state better
1086 | - Use phantom types for safety
1087 | - Make illegal behaviour a compile error
1088 | - Write total functions
1089 | - Unsurprising behaviour
1090 | - Easy to use, hard to misuse
1091 |
1092 | ** Epilogue
1093 |
1094 | A taste of algebra with datatypes
1095 |
1096 | ** A Taste of Algebra with Datatypes
1097 | How many values?
1098 | #+BEGIN_SRC cpp
1099 | template
1100 | class vector;
1101 | #+END_SRC
1102 |
1103 | #+ATTR_REVEAL: :frag appear
1104 | We can define a ~vector~ recursively:
1105 |
1106 | #+ATTR_REVEAL: :frag appear
1107 | ${v(t)} = {1 + t v(t)}$
1108 |
1109 | #+ATTR_REVEAL: :frag appear
1110 | (empty vector or (+) head element and (*) tail vector)
1111 |
1112 | ** A Taste of Algebra with Datatypes
1113 | And rearrange...
1114 |
1115 | ${v(t)} = {1 + t v(t)}$
1116 | #+ATTR_REVEAL: :frag appear
1117 | ${v(t) - t v(t)} = {1}$
1118 | #+ATTR_REVEAL: :frag appear
1119 | ${v(t) (1-t)} = {1}$
1120 | #+ATTR_REVEAL: :frag appear
1121 | ${v(t)} = {{1} \over {1-t}}$
1122 |
1123 | #+ATTR_REVEAL: :frag appear
1124 | What does that mean? Subtracting and dividing types?
1125 |
1126 | ** A Taste of Algebra with Datatypes
1127 | When we don't know how to interpret something mathematical?
1128 |
1129 | ${v(t)} = {{1} \over {1-t}}$
1130 |
1131 | #+REVEAL_HTML: Let's ask Wolfram Alpha.
1132 |
1133 | ** A Taste of Algebra with Datatypes
1134 | Series expansion at ${t = 0}$:
1135 |
1136 | ${1 + t + t^2 + t^3 + t^4 +{ }...}$
1137 |
1138 | #+ATTR_REVEAL: :frag appear
1139 | A ~vector~ can have:
1140 | #+ATTR_REVEAL: :frag (appear)
1141 | - 0 elements (${1}$)
1142 | - or (+) 1 element (${t}$)
1143 | - or (+) 2 elements (${t^2}$)
1144 | - etc.
1145 |
1146 | ** Goals for Well-typed Code
1147 | - Make illegal states unrepresentable
1148 | - Use ~std::variant~ and ~std::optional~ for formulations that
1149 | - are more natural
1150 | - fit the business logic state better
1151 | - Use phantom types for safety
1152 | - Make illegal behaviour a compile error
1153 | - Write total functions
1154 | - Unsurprising behaviour
1155 | - Easy to use, hard to misuse
1156 |
--------------------------------------------------------------------------------
/submission.org:
--------------------------------------------------------------------------------
1 | Career (20+ years) games industry programmer; generic and functional programming
2 | enthusiast. So I'm keen on performance (of course, I use C++) and I like using
3 | theory to gaining insights into practical problems. Currently a principal
4 | engineer at Blizzard Entertainment where I drive a lot of internal C++ learning.
5 | I study everything I can because the world is interesting. I've previously
6 | (2015) presented at C++Now and CppCon.
7 |
8 | --
9 |
10 | Using Types Effectively
11 |
12 | C++ has a pretty good type system, and modern C++ gives us a greater ability
13 | than ever before to use that type system for good: to make APIs easier to use
14 | and harder to misuse, to make our datatypes more closely express our intent, and
15 | generally to make code safer, more obvious in function and perhaps even faster.
16 |
17 | This is an interactive session - incorporating games played between presenter
18 | and audience, even - taking a look at choices available to us as datatype and
19 | API designers, and examining how a little knowledge about the algebra of
20 | algebraic datatypes can help. We'll see why std::optional and (hopefully soon)
21 | std::variant will quickly become an essential part of everyone's toolbox, and
22 | also explore how types can be used to express not just the structure of data,
23 | but also the behaviour of objects and functions.
24 |
25 | --
26 |
27 | * Introduction
28 | ** What is a type?
29 | ** Why do we have types?
30 |
31 | * Game 1: Types as sets of values
32 | ** Integral types
33 | ** Equivalence by equal cardinality
34 |
35 | * Game 2: Product types
36 | ** std::pair
37 | ** structs
38 | ** std::tuple
39 | ** reasoning about generic product types
40 |
41 | * Game 3: Sum types
42 | ** std::optional
43 | ** std::variant
44 | ** reasoning about generic sum types
45 |
46 | * Algebraic datatypes
47 | ** Equivalences
48 | ** Choices of formulation
49 | ** An aside: how to interpret std::vector
50 |
51 | * Making illegal states unrepresentable
52 | ** Example: a network connection in various states
53 | ** Example: a nullable field, asynchronously fetched
54 | ** Example: monster AI
55 |
56 | * Designing with type algebra
57 | ** A look at some design patterns
58 | ** More naturally modelling design patterns
59 | ** Sum types vs runtime polymorphism
60 | ** Designing with std::optional and std::variant
61 |
62 | * Constraining behaviour with types
63 | ** Phantom types
64 | ** Example: using phantom types to prevent injection attacks
65 |
66 | * Game 4: Name that function
67 | ** How appropriate typing makes functionality obvious
68 | ** Total vs partial functions
69 | ** Pitfalls of partial functions
70 |
71 | * TDD (Type-driven development)
72 | ** Make tests disappear
73 | ** Further down the rabbit hole
74 | ** Goals for well-typed code
75 |
--------------------------------------------------------------------------------
/variant-tweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elbeno/using-types-effectively/1b5c61f7013d228e1207cf05c61e51db50f042eb/variant-tweet.png
--------------------------------------------------------------------------------