
Functional Programming Fundamentals
Begin
15 pages · ~30 min
Functional Programming Fundamentals
This training introduces functional programming concepts and is designed for developers looking to write cleaner, more predictable code.
My workspace30 minFree to watch
What you’ll learn
- 01Introduction to Functional Programming: Functions, Immutability, and CompositionWelcome. In this course we explore functional programming. We focus on three core ideas: functions, immutable data, and composition. Together they offer a different way to organize code. This approach is not about replacing what you already know. It is a pragmatic response to the challenges of concurrency, scaling costs, and complexity in modern systems. By building programs with pure functions and immutable data, we make reasoning about behavior easier. Testing becomes more straightforward. And parallelism becomes safer, with fewer unintended side effects. Our roadmap today starts with functions treated as values. We then look at immutable state, and how operations like map, filter, and reduce let you transform data declaratively. We end with practical strategies for adopting these ideas incrementally, so you can apply them right away without rewriting your entire codebase. With that in mind, let's begin by contrasting two ways to think about a program. The next slide is titled Two Ways to Think: Imperative vs. Declarative.
diarysphere.comsachith.co.ukyoungju.dev+22 min - 02Two Ways to Think: Imperative vs. DeclarativeNow let's look at two fundamental ways to think about code. In the imperative style, you write a step-by-step recipe that spells out exactly how to compute a result. You manage variables, loops, and control flow in a specific sequence. In a declarative style, you describe what result you want instead. The language figures out the how for you. Think about loops versus map and filter. A loop is imperative; you tell the machine to inspect each item, check a condition, and build a new list. Map and filter are declarative; you say take this list, keep only these items, and transform them. The intent is clearer because the code reads closer to the problem statement itself. This involves a mental shift. You start thinking about data transformations rather than control flow. Instead of tracking a counter and updating a variable, you focus on passing data through pure functions that take inputs and return new outputs. That shift is what makes immutable data and composition feel so natural. Next, we will take that idea further and explore Functions as First-Class Citizens.
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+21 min - 03Functions as First-Class CitizensNow we come to a concept that truly distinguishes the functional style: functions as first-class citizens. In the imperative world, data is what you pass around. Here, the functions themselves become the data. You can assign a function to a variable, pass it as an argument to another function, or even return a function as a result. This opens the door to higher-order functions like map, filter, and reduce, which abstract away entire patterns of control flow. Instead of writing a loop to process a list, you describe what you want to do. For example, you can take a collection of data, map a transformation over it, filter the results, and reduce them to a single value. This forms a declarative chain: data.map, then filter, then reduce. And when you need a tiny, one-off piece of behavior, you can use a lambda expression to create it inline without giving it a name. It keeps your code lightweight and focused. Next, we'll build on this with The Foundation of Predictability: Pure Functions.
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+22 min - 04The Foundation of Predictability: Pure FunctionsNow, let's turn to one of the most fundamental building blocks in this paradigm: the pure function. Think of a pure function as a promise. For the same inputs, you always get the same output, and it does absolutely nothing else. There are no hidden side effects. It won't write to a log, call a network, or silently change a variable somewhere else in your program. This gives us a powerful property called referential transparency. In practice, that means you could replace a function call with its final computed value and your program's behavior would not change at all. Why does this matter? It makes your code predictable. Unit tests become trivial because you don't need complex mocks to simulate the outside world. You just pass data in and check the data coming out. It also makes caching safe and parallelization much simpler, since a pure function never fights for shared resources. Here's a quick contrast. An impure function might read the current time from the system clock internally, giving a different result every time it runs. A pure version would simply receive the time as an explicit parameter. The logic is the same, but its behavior is now fully under your control. This idea of separating pure logic from the messy outside world sets the stage perfectly for our next topic, which is immutability and how it keeps our state safe.
diarysphere.comsachith.co.ukyoungju.dev+22 min - 05Immutability: The Key to Safer StateLet's look at one of the most important ideas in functional programming: immutability. In familiar imperative code, we often change data in place, like pushing a new item onto an existing array. Immutability takes a different path. When you need to 'update' data, you don't modify the original. Instead, you create a brand new copy that reflects the change. The original value stays exactly as it was. This single rule eliminates entire categories of bugs. You never have to worry about a function unexpectedly altering data that another part of your program relies on. That also means you never need to write defensive copies 'just in case.' Now, you might wonder if copying data constantly makes programs slow. That's where persistent data structures come in. They use a technique called structural sharing, which lets new versions reuse most of the existing data behind the scenes. So you get the safety of immutability with strong performance. To make this concrete, compare the imperative 'array dot push of x' with the immutable spread syntax: 'new array... existing, x'. The result is the same, but the immutable version protects your original data. This shift to immutable values is a key reason functional programs are so predictable and safe in concurrent systems. Next, we'll build on this foundation by exploring composition, and see how to combine simple, proven parts into more complex behavior.
diarysphere.comsachith.co.ukyoungju.dev+22 min - 06Composition: Building Complexity from Simple PartsNow let's turn to one of the most powerful ideas in the functional style: composition. In imperative programming, we often write a sequence of steps that modify state. Composition works differently. It lets us build complex logic by combining small, focused pure functions, like connecting building blocks. Think of threading data through a pipeline. Each function transforms the data and passes it to the next. You can do this manually, like writing f of g of x. Or you can use helper utilities often called pipe or compose, which are common in modern languages. This approach gives you a flexible way to organize logic. Instead of relying on deep class hierarchies and inheritance, you favor small, reusable functions that snap together. The result is code that reads like a clear description of what the data does, step by step. Next, we'll look at how to handle the parts of a program that must interact with the outside world, in "Taming the Real World: Managing Side Effects."
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+21 min - 07Taming the Real World: Managing Side EffectsReal programs can't live in a pure bubble forever. They need to read files, write to databases, and generate random numbers. These are side effects. Trying to make everything pure often leads to frustration. So what do we do? We don't fight the real world; we organize it. The pattern is called Functional Core, Imperative Shell. Imagine a walnut. The hard shell handles all the messy input and output—that's the imperative part. Inside, the nut is the pure functional core, where your business logic lives. The core never touches a database or an API. It simply receives plain data and returns a description of what should happen. For example, the core might return an instruction like 'send an email to this address.' The thin outer shell then executes that instruction. This separation gives you the best of both worlds. The core logic remains predictable, easy to test, and immune to network failures. Meanwhile, the shell is so thin that it has very few branches of logic to go wrong. You push side effects to the edges, and let your core be the solid, reliable center. Next, we will look at 'The Functional Core, Imperative Shell in Practice' to see exactly where to draw this line.
github.comkennethlange.comroozbehsam.com+22 min - 08The Functional Core, Imperative Shell in PracticeNow let's see how the functional core and imperative shell work together in practice. Think of a common task: updating a customer profile. In a traditional imperative style, you might read from the database, compare fields, and call update or email services all inside the same method. The logic is tangled with the side effects. Our alternative splits this cleanly. The pure core receives the new customer data and the existing customer data as explicit inputs. It compares the values and returns a decision value, nothing more. It does not touch the database. It does not send emails. It simply returns an instruction, like update the record or send a verification email. The imperative shell handles all the real-world actions. It reads the current profile from the database, passes both records to the pure core, and then executes the returned decision. This pattern maps perfectly to established architectures you may already know, such as Hexagonal Architecture, Ports and Adapters, or Clean Architecture. The result is a thin, predictable shell and a logic core that is remarkably easy to test and reason about. Next, we will look at functional tools hidden in your everyday language.
github.comkennethlange.comroozbehsam.com+22 min - 09Functional Tools in Your Everyday LanguageNow let's bring these ideas directly into the languages you use every day. You do not need to learn a new language to write functionally. The tools are already there. In Python and JavaScript, you can replace a loop over a list with a chain of map, filter, and reduce. In Java, you have the Streams API. In C sharp, you have LINQ. These declarative pipelines state your intent directly. You want to select and transform data, and that is exactly what the code says. For immutability, reach for const over let in JavaScript, use frozen data classes in Python, or spread operators to create new copies instead of mutating old ones. The goal is simple. Refactor one imperative loop into a functional pipeline. See how it reads. Start where you are. Use what your current language already offers. The industry is moving this way because it works. Enterprises report fewer production bugs and faster development cycles by keeping core logic pure. You can adopt a functional core with an imperative shell, all within the tools you know. Next, we will look at how to model data without nulls, using optionals and pattern matching.
diarysphere.comsachith.co.ukyoungju.dev+22 min - 10Modeling Data, Not Nulls: Optionals and Pattern MatchingNow, let's look at how to model data when a value might simply not be there. In functional programming, we don't use null references. Instead, we use a special type, often called Optional or Maybe. This type wraps a value and explicitly represents the possibility of absence. It makes your code safer by preventing null-reference errors at the source. Languages like Java have Optional, and TypeScript uses strict null checks. Libraries like fp-ts offer the Option type. But having the type is only half the picture. We need a clean way to inspect what's inside without a chain of if-else statements. That's where pattern matching comes in. Think of it as a switch statement on steroids. You provide a pattern for the shape of the data—say, 'Some value' or 'None'—and the language safely extracts the value for you in each branch. It is a declarative way to handle complex, nested structures. This is not a niche concept anymore. Pattern matching is now a core feature in Python 3.10 and later, Rust, Kotlin, Swift, Ruby 4.1, and the latest Java releases. For TypeScript, while a native JavaScript proposal is still in development, the ts-pattern library is a robust choice today, with around 900,000 weekly downloads. Up next, we'll step back and look at the broader functional programming landscape in 2026 and see why this is much more than just a niche interest.
docs.oracle.comopen-std.orggithub.com+22 min - 11The 2026 FP Landscape: More Than Just Niche LanguagesBy 2026, functional programming is no longer a niche academic exercise. It has become a pragmatic response to the real-world challenges of building complex, distributed systems and controlling rising maintenance costs. Let's survey the tool landscape. The Haskell family excels in high-assurance fintech where types enforce invariants. In the pragmatic systems space, languages like Gleam and Rust provide safety without a PhD-level learning curve. The BEAM family, with Elixir and Erlang, remains the gold standard for fault-tolerant, concurrent services. For infrastructure and .NET ecosystems, we have OCaml and F#. However, the most dominant trend is what we call 'functional but pragmatic.' You no longer need to switch your entire stack. The core principles of functional programming have been absorbed directly into the tools you probably already use, like TypeScript, Kotlin, Rust, and Swift. The real value today isn't just the isolated language; it is the functional philosophy that helps you build more reliable, maintainable software, even inside an imperative shell. Next, let's talk about incremental adoption and where your existing codebase can start to win.
diarysphere.comsachith.co.ukyoungju.dev+22 min - 12Incremental Adoption: Where Your Codebase WinsSo far we have explored how functional programming looks. The real question is, how do you bring these ideas into an existing codebase without starting over? The answer is incremental adoption. Start by extracting pure functions from your existing domain logic. Identify small calculations that always produce the same output for the same input. Move those into standalone functions. Then, begin replacing loops and mutable state with immutable data and higher-order operations like map, filter, and reduce. This may feel unfamiliar at first, but it makes data transformations easier to read and test. Next, push side effects to the boundaries. Keep your core logic pure and centralize database calls, network requests, and file operations at the edges of your system. Refactor one module at a time. Share clear examples with your team as you go. Track the gains you see in testability and confidence. Up next, we will look at how these patterns make testing and reasoning about code even more straightforward.
2 min - 13Testing and Reasoning with ConfidenceNow let's talk about one of the biggest practical wins: testing and debugging. In imperative code, testing often means building complex mocks to fake databases and services. With functional programming, pure functions need no mocks at all. You simply pass in data and check the output. This makes unit tests fast, isolated, and deterministic. There is a technique called property-based testing that takes this even further. Instead of writing individual examples, you describe a general property that must always hold, and the test framework feeds in hundreds of random inputs to find edge cases you never thought of. For the parts that do interact with the world, we use the functional core, imperative shell pattern. All the business logic lives in a pure functional core, while a thin imperative shell handles side effects like network calls. Integration tests become smaller and far less flaky. Finally, debugging becomes a repeatable science. Since pure functions always produce the same output from the same inputs, you can capture a failing case and replay it instantly without recreating a complex environment. Next, let's explore where to go from here with languages and communities.
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+22 min - 14Where to Go Next: Languages and CommunitiesNow that you have seen the core ideas, you might ask: where do I practice this next? The functional ecosystem in 2026 offers clear paths for different goals. If you want a deep, principled foundation, Haskell remains the gold standard. For highly concurrent, fault-tolerant backends, Elixir and the rising star Gleam on the Erlang virtual machine are excellent choices. If you work in the Java Virtual Machine ecosystem, Scala and Clojure bring functional power directly to your existing tooling. In the dotnet world, F Sharp is the functional-first language that integrates cleanly. And if front-end development is your focus, Elm offers a beginner-friendly entry, though its evolution is currently paused. Many of you may not switch languages at all. TypeScript and Python both support functional patterns you can adopt incrementally. For TypeScript users, the fp-ts exercises playground provides a superb browser-based way to practice, with over one hundred exercises across eighteen modules and no installation needed. To guide your learning, the Mostly Adequate Guide teaches functional thinking directly in JavaScript. The free Haskell MOOC offers a structured, university-backed course from the University of Glasgow. And platforms like Exercism dot io let you solve koans and challenges in the language of your choice, with mentor feedback. The key is to pick one language and one resource, and start writing pure functions. Now let's move on and assemble your practical toolkit with first steps and key takeaways.
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+22 min - 15Practical Toolkit: Your First Steps and Key TakeawaysWe have reached the final slide, and with it, a practical starting point. Think of this as your toolkit. First, try to write pure functions. They make your code predictable because the output depends only on the input. When you need a dependency, pass it in explicitly. Second, treat your data as immutable. Use 'const' declarations, the spread operator, or always return a new copy instead of modifying the original. Third, when you work with lists, reach for 'map', 'filter', and 'reduce'. They often replace simple loops and make your intent clearer. Fourth, push input and output to the edges of your application. Keep the core logic pure and testable. Finally, remember that the ideas come first. Focus on purity, immutability, and composition. The specific language is just a tool. Thank you for joining me. These concepts will sharpen how you write and think about code, no matter what language you use next.
mostly-adequate.gitbook.iohaskell.mooc.fiintrocs-python.cs.luc.edu+21 min
Sources consulted
Web sources consulted while building this course.
- Functional Programming in Enterprise: A Pragmatic Revolution — DiarySphere — diarysphere.com
- Functional programming pragmatism for OOP teams — Migration Playbook — Practical Guide (Jun 28, 2026) - Sachith Dassanayake — sachith.co.uk
- Functional Languages in 2026 — A Camp-by-Camp Survey of Haskell, OCaml, Elm, Gleam, Roc, Unison, Lean 4, F#, Clojure | Chaos and Order — youngju.dev
- Functional Programming Concepts Are Rising Across Modern Software Development — g5world.com
- Functional Programming: Principles, Benefits and Practical Adoption — edana.ch
- https://mostly-adequate.gitbook.io/mostly-adequate-guide — mostly-adequate.gitbook.io
- Haskell MOOC — haskell.mooc.fi
- Functional Programming — Introduction to Computer Science in Python: Principles and Practice (18 Jun 2026) — introcs-python.cs.luc.edu
- Functional programming languages: a complete beginner-friendly guide — DevPebble — devpebble.com
- Introduction - Functional Programming in Lean — leanprover.github.io
- kenneth-lange/ts-functional-core-imperative-shell — github.com
- The Functional Core, Imperative Shell Pattern - Kenneth Lange — kennethlange.com
- Functional Core - Imperative Shell (FCIS) — When to Use It | Roozbeh Sam — roozbehsam.com
- Functional Core, Imperative Shell — destroyallsoftware.com
- Functional core, imperative shell — danigb.github.io
- Pattern Matching — docs.oracle.com
- Pattern Matching: `match` Expression — open-std.org
- tc39/proposal-pattern-matching — github.com
- peps/pep-0635.rst — github.com
- pattern_matching - Documentation for Ruby 4.1 — docs.ruby-lang.org