Typescript and the Three Perspectives of Software Development
I tend to view software engineers as one of three personas: a mathematician, a machinist, or a businessperson. Tableau juggles each of these personas every day in each of our products. For example, Hyper concerns itself with correctness from a theoretical perspective (the mathematician), optimizes queries to run well on SIMD and multi-core processors (the machinist), and sits behind a PostgreSQL API so Tableau products can easily use it (the businessperson). However, these personas also manifest themselves in source code and the tools we use.
To mathematicians, software and language design should reflect the purity and exactness of math. Their tools have extremely powerful type systems, which one might be tempted to think results in much boilerplate. Quite the contrary, code written in functional languages is often terse and features little ceremony. The stereotype of a mathematician is that once they finish writing code for the first time, there’s no need to run it because the fact that it compiles at all means they’ve already arrived at the answer. Simple inspection of the 20 characters and an hour of thinking reveals its correctness. Consider the following Haskell implementation of quicksort from Learn You a Haskell for Great Good:
quicksort :: (Ord a) => [a] -> [a] quicksort  =  quicksort (x:xs) = let smallerSorted = quicksort [a | a <- xs, a <= x] biggerSorted = quicksort [a | a <- xs, a > x] in smallerSorted ++ [x] ++ biggerSorted
This is a fully generic implementation of quicksort that works for any types that support comparison. Strings? Yep. Doubles? Yep. Some user-defined type that implements the
Ord type class? Yep. Another hint as to Haskell’s audience is given in this code:
print $ 7 ^ 401
Which happily prints:
No overflow. No saturation. No round off or underflow. No compromise. Just lots of numbers and a very exact value. The time and space required to arrive at this? Complexity theory is occasionally interesting, but don’t bore me with your clock cycles and bytes. They’re simply a loss of generality.
The next persona is that of the machinist. A machinist concerns themselves very much with mechanics of how all of the moving parts work together and how they perform. Unlike the mathematician, machinists find beauty in pushing hardware and software to their design limits and doing the most with the least.
For most of my college and professional career, I’ve been in this camp. I fondly remember building a temperature sensor with a 68HC11 microcontroller, a breadboard with a few wires and an analog thermometer. In less time than it took to set up a C toolchain for our environment, I had completed the entire project using the assembler that came with the development board.
The entire program was maybe 50 bytes consisting of a simple timer interrupt that wrote values to a 16-byte ring buffer, averaged it, and wrote the average to the byte at address 0. While dereferencing a null pointer may be undefined behavior in C, address 0 is a completely fine address under our 68HC11’s configuration, so why waste it? The microcontroller only had 256 bytes of built-in memory; if you went over that budget, you’d have to add external memory and make things hard. As for the operating system that ran our program, there wasn’t. On boot, the 68HC11 in our configuration executed instructions at address 0x9000, so that’s where you found the first instruction of our program.
Machinists enjoy using low-level tools that don’t obscure the machinery in a black box. C is a machinists’ tool. While the number of moving parts in the language is simple on paper, one must be cognizant that the code will run on a real processor and has real requirements. Watch for overflow on signed integers, it’s undefined behavior in C. If you want to use the faster vector load instructions on x86 processor, your data had better be 128-bit (SSE) or 256-bit (AVX) aligned lest you get a segfault. Core 2 Duo processors trap and emulate denormalized floating point calculations in software, taking hundreds rather than a few clock cycles.
A machinist understands that our example of computing 7 to the 401st power is a futile exercise without using a big-math library. 64-bit unsigned integers overflow at 18 quintillion or so and IEEE-754 double precision integers become infinity after you slap 307 digits on them. Quad-precision floating point would be rounded, which is trade-off you may or may not be okay with. Whatever your needs are, the machinist can tell you how expensive the parts will be.
Finally, we arrive at the third software persona: the businessperson. The businessperson focuses their concerns with the realities of software development and chooses their tools around maximizing productivity and minimizing costs. Beautiful software to the businessperson is that which maximizes re-use, ships quickly, and doesn’t cost a ton.
When choosing a tool, the businessperson doesn’t find elegance in precision per se and mechanics are secondary to delivering value. They quickly go to market by using existing tooling and infrastructure. The business persona minimizes cost and manages complexity by using standard libraries and tools. They provide flexibility for multiple scenarios by leveraging abstraction. Sometimes to a comical fault, such as this factory method that makes factories that produce builders that emit DOM objects from an XML document in the Java standard library.
Drawing from a large talent pool and leveraging an ecosystem by choosing popular rather than “good” (as defined by the mathematician or machinist) languages is just common sense. Haskell has a reasonable ecosystem but isn’t a top-10 language on Github as of 2020. Unless it fits well into the company’s domain, it’s probably not worth the added headache of finding developers.
C, on the other hand, is a top-10 language and you’ll find many developers who know it, but it’s not ideal for most businesses outside the embedded systems and performance-critical application spaces. The bugs you can get are scary: buffer overflows, segfaults, and memory leaks come with the territory. Building and distributing C code for multiple platforms is hard (i.e., compiler toolchains, choosing a portable build system, and integrating said build system), and consuming dependencies is difficult across different operating systems.
Typescript 0.8: a language for businesspeople
I’ve used Typescript professionally since it was called Strada. In its early days, it was built for the business persona and felt like any other no-nonsense object-oriented enterprise-y language (e.g. C# and Java): classes and interfaces were the heavy movers in your type system and your app’s design felt like a C# or Java app. As such, other developers and I learned it fairly quickly.
any and throw type safety to the wind. Alternatively, you could go to Definitely Typed and incorporate a
At first, the now esoteric
/// <reference /> directive was the way to consume types in other files. However, Typescript quickly added explicit support for type resolution using NodeJS’s search rules, options to compile AMD and CommonJS modules, and the far more convenient
Typescript 2.0: math goes mainstream
Compared to the old days, the zeitgeist of Typescript feels radically different: it has become a tool for the mathematician. Typescript has fully embraced the first part of its name and dived very deep on its type system. Tableau as a company has been leveraging language features almost as quickly as Typescript adds them to express invariants and push assertions into the type system. Insofar as we can, we try to prevent buggy code from compiling.
As Typescript began to cater more to the mathematician mindset, we began to embrace more type richness. This delivered significant business value in the form of increased stability and improved error handling. Examples include:
- Dramatically increased product stability by switching on strict null checks.
- Incorporating tagged unions to push invariants into the type system.
- Defining state machines and validating their transitions at compile time. How we do this is at least another twoblog posts :)
- Requiring exhaustive case handling in switch statements using the never type.
Tableau Prep’s UI codebase contains several thousand Typescript files and nearly all of them prefer types with unions and intersections over traditional deep class hierarchies and interfaces. To be sure, we do have many classes, but nearly all of them are React pure components that predated hooks.
Typescript’s machinist future?
any type and number is replaced with integral and floating point types. In doing so, it exposes many of the more advanced things a machinist needs to work in the WASM world. You can force garbage collections, pin objects so the host environment or other modules can safely use them, and carte blanche allocate memory out of the GC’s purview.
Typescript and the 3 personas at Tableau
We went to great lengths to architect Tableau Prep as both a desktop and server product. The decisions we made combined tools and designs from each archetype:
- Our machinist C++ microservice leverages AQL and Hyper to quickly get data to users so they can see their flow changes in real-time.
- The businessperson Java REST-API deploys to online and on-premises environments interchangeably.
- As outlined in this post, Prep’s Typescript UI incorporates mostly the businessperson and mathematician perspectives as Typescript has evolved to do so.
As Typescript, Tableau, and the software ecosystem continue to evolve, it’s possible our next product will accomplish this feat more simply using Typescript and WASM. Perhaps due to Typescript catering to more archetypes, it’s now the 4th most popular on Github and will be a compelling language for businesses for years to come. Needless to say, Tableau will continue to derive value from Typescript’s richness.