In recent years, there’s been a noticeable shift in the programming world. Modern Object-Oriented (OO) languages are increasingly borrowing concepts like first-class & higher-order functions, immutability, pure functions, and more from Functional Programming (FP). Why? Because FP has proven to be a better way to build reliable, maintainable, and scalable software. But here’s the catch: while OO languages like C# are trying to adapt FP concepts, they often fall short because their compilers and runtime environments weren’t designed with these paradigms in mind.
This isn’t just a theoretical shift—it’s happening at the highest levels of industry. Microsoft, for example, has hired Simon Peyton Jones, one of the creators of Haskell, to help guide their functional programming efforts. Tim Sweeney, the founder of Epic Games, is working with Simon on a new language called Verse, which brings FP concepts into game development. Meanwhile, companies like Tesla, SpaceX, and Neuralink are using Haskell to solve some of the most complex and high-stakes problems in the world.
While these languages are very popular, in practice they are a nightmare for large projects. Why? Imagine you have a huge bin of LEGOs. You decide to recruit 4 friends to help you with an ambitious project: building a robot! There are infinitely many ways to put together pieces of lego, as well as infinite ways this project can go right or wrong. While there are many outcomes we can definitely know the characteristics of a successful project vs an unsuccessful one.
If you were to all take on a different component of the body independent of each other you would likely run into plenty of integration issues. For example, the legs may receive power in a different manner than the arms. There's also the balance and coordination of the robot: when it comes to a bump how does it know to position its arms given the movement of the legs. Are the legs and feet to scale to support the weight of the body?
Fast forward a few weeks and your new challenge is how to write an adapter, instead of connecting the arms to the torso you connect the arm to the adapter which is connected to the torso. But you still are up against the same original challenges like how do you make sure it balances and now you have much more weight to consider. At this point you and the amigos are much less happy as some of you think "Maybe we should just start fresh" and others think "I am not throwing away 4 weeks of work! I will make an adapter for the adapter if I have to".
How did we get here?
You would have been successful if from the start you created pieces which were 100% perfect and did everything they need to and nothing more.
That might sound difficult but this is the essence of beautiful engineering, math and design. Addition is built on counting which allows us to define multiplication. Similarly if we started with systems that can be fully defined in terms of all inputs and outputs, then we would know that this independent system would just be like another piece of LEGO that we can easily combine with other LEGOs and other well-built systems.
Ace is a platform which is built entirely using functional programming with the Haskell language. It's allowed for automatic database migration without fear, in-depth video processing through WebRTC, and over 150 unique features. We can easily add new features without worry of breaking old ones. Even when we need to change old features, we can do so easily without introducing new bugs due to the type awareness of the Haskell compiler.
Our team has also been in many projects written in object-oriented languages, which initially seemed quick with the use of libraries but then spent even longer working to adapt the library to some other component and spaghetti code that no one has been willing to touch for years.
The difference in the outcomes these projects have in practice simply comes down to the compiler. As we will see throughout the Ace program, it comes down to the Haskell compiler disallowing numerous problematic ways to code, and the thinking patterns that result from working with such a strongly typed compiler.
After using Haskell, you will have a mental framework to discover the best solution to any given problem. You will also know the difference between strong and weak coding patterns and know whether or not you can trust a function. This mental framework is one that can be applied not only to amazing languages like Haskell and other functional strongly-typed languages but even to languages which don't stop you from writing bad code.
Before we dive into Haskell, let’s talk about why FP is gaining so much traction. The core principles of FP—immutability, pure functions, laziness, and generative testing—offer solutions to many of the challenges that OO programming struggles with or fails to do entirely.
Immutability: In FP, data is immutable by default. This means that once you create a value, it cannot be changed. Compare this to C#, where objects can often change state in unpredictable ways, leading to bugs that are hard to track down. In Haskell, because the value of a particular variable name doesn’t change, you can reason about your code and changes of state more easily, leading to fewer bugs and more robust software.
Pure Functions: A pure function is one that, given the same inputs, always produces the same output and has no side effects. A side effect is anything that the function changes about external state, outside of the function's scope. The core "effect" is the value returned by the function. Ensuring that some functions are pure is a cornerstone of FP and contrasts sharply with Object-Oriented programming, where methods are free to, and often modify the state of objects or interact with global state. For example, an innocent-looking function like addTwoNumbers provides no guarantees that it wont send nukes, and the writer cant guarantee the new hire wont change this function to fire nukes either. Even worse, in an impure context, even a pure function provides limited ability to understand if it's effect will cause code to evaluate a different case which fires these nukes. In C#, methods often have side effects, which can make programs harder to understand and maintain. In Haskell, the use of pure functions means that your code is more predictable and easier to test. The key thing is that impure code is incredibly necessary (imagine not being able to print or take input) but there are definitely cases where we want to guarantee purity (any case it's theoretically possible) like in every case of mathematics. Discrete logic like case statements and if statements are also definitely cases we always can and always should have pure functions.
Laziness: Haskell is a lazy language, meaning that it doesn’t do the work to evaluate expressions until absolutely necessary, if ever necessary. This can lead to performance improvements and allows for the creation of more abstract and reusable code. In contrast, C# is an eager language, evaluating expressions as soon as they are encountered, which can sometimes lead to inefficiencies. As a fun example, both haskell and C# can express an infinite list but only haskell could for example perform an operation on the first 5 or so elements without exploding.
Generative Testing: FP languages often emphasize generative testing, where the program itself generates test cases. This is in contrast to traditional unit testing in OO languages like C#, where developers manually write test cases. Haskell’s QuickCheck library, for example, allows you to describe properties that your functions should satisfy, and then automatically generates test cases to verify those properties. This approach can uncover edge cases that you might never think to test manually.
Let’s look at some specific examples to see how these differences play out in practice.
In C#, immutability is not the default. Here’s how you might write a class that represents a point in 2D space:
public class Point {
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) {
X = x;
Y = y;
}
public void Move(int deltaX, int deltaY) {
X += deltaX;
Y += deltaY;
}
}
In use:
p = new Point(0,0)
p.Move(2,3)
p.Move(5,7)
// Currently p Equals p.X == 7 and p.Y == 10
This class allows you to change the position of the point after it’s created. This mutable state can lead to bugs, especially in larger systems where the state might be modified by different parts of the program at different times.
Now, here’s how you might write a similar structure in Haskell:
data Point = Point { x :: Int, y :: Int }
move :: Point -> Int -> Int -> Point
move (Point x y) deltaX deltaY = Point (x + deltaX) (y + deltaY)
In use:
state1 = Point 0 0
-- Always equals (Point 2 3)
state2 :: Point
state2 = move state1 2 3
-- Always equals (Point (2+5) (7+3))
state3 :: Point
state3 = move state2 5 7
In Haskell, state1, state2, and state3 are immutable. The move function doesn’t change the original point—it returns a new point with the updated coordinates. This immutability makes the code easier to reason about, as you never have to worry about the state of a Point changing unexpectedly.
Consider a method in C# that reads a file and processes its contents:
public string ProcessFile(string filePath) {
string content = File.ReadAllText(filePath);
return content.ToUpper();
}
This method is not pure. It has a side effect (reading a file from the disk) and its output depends on the state of the file system.
In Haskell, you would separate the side effect (reading the file) from the pure function (processing the contents):
processFile :: String -> IO String
processFile filePath = do
content <- readFile filePath
return (map toUpper content)
Here, readFile is an impure IO action that performs the side effect of reading the file, but map toUpper
is a pure function that transforms the string. This separation makes the code easier to test and reason about.
In C#, if you create a list of numbers, the list is fully evaluated as soon as it’s created:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
If this list is large, it might consume a significant amount of memory, even if you only need to process a few elements.
In Haskell, lists are lazy by default:
numbers = [1..]
This creates an infinite list of numbers, but it’s not evaluated until you actually need the elements. You can then take just the first few elements without evaluating the entire list:
take 5 numbers -- [1, 2, 3, 4, 5]
This laziness allows you to work with potentially infinite data structures in a way that’s both memory-efficient and conceptually elegant.
The industry is moving towards functional programming for good reason. As software systems become more complex, and as AI developers become more prevalent, the advantages of Functional Programming (immutability, pure functions, laziness, and generative testing) are becoming increasingly clear. While object-oriented languages like C# are trying to incorporate these concepts, they often do so in a way that feels bolted-on rather than native. Haskell, on the other hand, was designed from the ground up with these principles in mind.
By learning Haskell, you’re not just learning a new language. You’re adopting a new way of thinking about programming. This shift in mindset will make you a better developer, no matter what language you ultimately use. Whether you’re working on high-stakes projects at a company like Tesla, developing the next big game at Epic, or simply trying to write better, more reliable code, the skills you gain from learning Haskell will serve you well.
So let’s dive in and start learning Haskell. You’re about to unlock a powerful new approach to programming that will change the way you think about code forever.