13 Sep 2021
7 min read
Co–Star is built by a small team of thirty. Thirty humans, with backgrounds in big fashion, independent publishing, intentional communities, and tech, create software for 25% of young women in the US. We currently have exactly two backend engineers. A single Haskell engineer can support a 2:3+:1 data:FE:BE developer ratio and still have free BE time.
No statements, no instructions, just expressions that don’t ever mutate. Everything returns something. You don’t mutate by default.
Start thinking generally, in broad concepts, about what kinds of info you’re dealing with — the inputs, the outputs, the steps — and then work down to implementation. Start in the center w/ a little implementation and work your way out.
You can't do the wrong thing with this data because this data can’t handle that, it just doesn’t accept that kind of thing.
NO STATEMENTS, NO INSTRUCTIONS, JUST EXPRESSIONS THAT DON’T EVER MUTATE. EVERYTHING RETURNS SOMETHING. YOU DON’T MUTATE BY DEFAULT.
Originally devised as a didactic and experimental test bench for programming language design, Haskell is still often seen as a niche or academic language. While Haskell's been around for a long time, newer languages are just using surface level additions to very old programming paradigms. 1950s von Neumann machines interface with the procedural: they’re imperative and step-based, based on how things are moving around in registers, transistors, and diodes. 70 years later, most programming languages are still ALGOL-derived (C, Python, etc) and maintain visible vestiges of these punch-card assembly languages. It’s still explicit – if you’re decorating your living room, ALGOL-derived languages force you to think about how a couch gets put together. This remains the dominant computing paradigm, hidden in a crowded, fragmented, distracted chattering souk of libraries and forums.
A higher level language assumes the couch has already been put together. This allows a programmer to describe data transformation and constraints on a broad conceptual level and leave the exhaustive details to the compiler. You can abstract away how things are moving around in registers in favor of thinking in terms of functions, math, what data is, how it’s shaped, and how you want to combine it with other things. Ultimately it runs on a VM, but lets you think more abstractly.
The formal abstraction forces better thought and better understanding of how things are related – a rigorous, first principles approach to engineering complex software. The ease and clarity of use, coupled with a high degree of certainty, makes it tenable to precisely express what you want the computer to do. It’s not the only proxy for clarity of thinking, but it’s a good one.
Ultimately, you express what question you want the computer to answer, what concept you want to express by giving constraints and transformations, and let the computer (via the compiler) do the dirty work. E.g. being able to simply say "double all the numbers in this list", rather than saying "start at index zero, get the integer at that index, double it, write it back, increment your iterator, and repeat."
This means that the bar of what counts as a simple task is much higher in Haskell. There isn’t the reliance on a spaghetti of 3rd party libraries that many engineering teams have become used to. The core language supports complex ideas you can snap together to create something with emergent power, greater than the sum of its parts. You can express things by concating a few functions together to do what an entire library is needed for in other languages. Engineering becomes craft, rather than duct taping things together.
For every day with a million daily active users, you hit a one-in-million bug. We see our Haskell codebase as an investment, because we value reliability & having confidence in our code base. Getting a computer to do something is not hard, but you want it to do the right thing. Haskell gives you the ability to know when you're doing the right thing, quicker.
START THINKING GENERALLY, IN BROAD CONCEPTS, ABOUT WHAT KINDS OF INFO YOU’RE DEALING WITH — THE INPUTS, THE OUTPUTS, THE STEPS — AND THEN WORK DOWN TO IMPLEMENTATION. START IN THE CENTER WITH A LITTLE IMPLEMENTATION AND WORK YOUR WAY OUT.
Haskell is a strongly-typed language. Types are a way of expressing knowledge about a piece of data, what its nature is, and by extension, things that you can do with it. A type lets you annotate this, and create a language that you express, how things are allowed to interact. An engineer can quickly outline the broad, conceptual strokes of a feature by stubbing out unimplemented functions by defining their input and output types to confirm that they can be screwed together.
While this can’t replace testing each function individually, it’s a first-pass confirmation that the various moving parts are using a shared vocabulary with shared ideas about what different kinds of data are and what can be done to them. It confirms that every plug matches its outlet. Large classes of errors are rendered impossible at compile time, without run-time error checking or need to write explicit test cases for e.g. null pointers.
Maybe you swap the argument order of a function in your codebase but call it correctly in the test. Python won't complain, it'll gladly accept a STRING STRING where it wanted an int arg to be and then everything breaks. Maybe you upgraded your dependencies & a function now returns an int instead of a string. Haskell & static typing lets you skip writing whole classes of tests because the compiler is the one verifying those things.
Haskell "forces" you to handle all your edge cases. To do something with the a in Maybe a, you have to define handling for the Nothing case. The equivalent in another language would be to put an if myArgument == None: at the start of every single function you write. You can be certain, later on down the line that you're in an environment where that cannot happen. You need not worry about that eventuality: that has been solved somewhere else, to be able to prove that your function will take something and produce something with equal certainty.
It's much easier to model our domain specifics because Haskell makes defining new types cheap & easy.
DATA PLANET = MERCURY | MARS | ETC
IF NATALPLANET == MARS THEN ...
is much more bullet proof than:
IF NATALPLANET == “MARS”
because if you write:
IF NATALPLANET == MRAS (OR “MRAS”)
Haskell will complain (What the hell is Mras????), while python will just go on to the next elif or else statement, because a string can be anything. In Haskell, illegal states are unrepresentable. While a stringly-typed language will accept "Mras" as a planet, a more type-safe paradigm will not. That said, there are ways of dealing with this in languages like Python, they're just a little more verbose and you don't get the compiler checking that you've exhaustively handled all cases, for example. It won’t compile, it won’t accept that idea.
YOU CAN'T DO THE WRONG THING WITH THIS DATA BECAUSE THIS DATA CAN’T HANDLE THAT, IT JUST DOESN’T ACCEPT THAT KIND OF THING.
Software is a series of moving parts and, as it evolves, it becomes more and more complicated until it's less a machine than an organism or ecosystem. It's very hard to keep an eye on all parts of that at the same time. With Haskell, we can implement something, kinda forget about it and its inner workings, but can trust that what the left hand is doing today won't cause the house of cards the right hand built yesterday to come crashing down.
As a small software team, we’re deeply oriented toward rapid experimentation while continuing to scale. We’ve made multiple huge changes to the entire backend in the past 6 months to support millions of users. These changes would have been even bigger, messier, buggier, & more drawn out in something like js/node or python.
Increased abstraction means that you can write things much quicker and more simply than in another language. So for anything that doesn't exist yet, you can very quickly create an interface abstraction in a very expressive language that allows you to get it up very quickly.
When you’re refactoring, it’s easy to change something (e.g. add an argument to a function), and get a list of every place you need to change — the compiler makes sure you've got all your cases covered.
Recently Co—Star added the ability to choose which house system you'd like to use, as opposed to our default of Placidus. We started by making one small change in one place and then let the compiler guide us:
GHC — the primary Haskell compiler used by most users — has error messages which can appear intimidating due to their length, but in most cases give a specific place where there’s a type error problem and a suggestion of exactly what to do in that situation. We have used this on multiple occasions to ease our refactoring process. To begin with, one simply adds, removes, or changes an argument type for a function (or some other constraint thereof), then builds their project and addresses the errors that come up as exposed by the compiler. When the type checker and compiler is satisfied, the user can have a high degree of confidence.
In python, maybe you get an error or warning when you don't pass an argument to a function? Not sure, might just default to None. JS sets it to undefined — another potential failure point. Other languages have implicit type coercion. 1 == "1", for example, will do different things in different languages and maybe even in different contexts in the same language. Haskell will just tell you that that comparison doesn't make sense.
The "check my work but get out of my way" type system & all the other Haskell "accoutrement" lets us develop quickly but safely.
Haskell is optimized for developer efficiency. You can get a lot done, have a high degree of confidence that it runs reasonably without having to do too much thinking or ass-covering. We move fast & need things to be reliable. Rather than hand optimizing Doom 2 for ___, Haskell is the opposite. The computers serve us. Code is for humans to read, and only incidentally for computers to read. Code is how humans express themselves to each other and computers, and it’s up to computers to run it in a reasonably good way.
See other articles by Brit
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!