From the course: Advanced C#: Functional Programming Patterns

Core principles of functional programming - C# Tutorial

From the course: Advanced C#: Functional Programming Patterns

Core principles of functional programming

Functional programming is a programming paradigm for developing software using functions. Adopting a functional philosophy means relinquishing concepts like shared states, mutable data, and side effects. As an experienced C# developer, you are comfortable with .NET types and reading C# code. But when you dive into pure functional languages like Haskell, you'll be in unfamiliar territory. Not only are you learning a new way of thinking about coding, but you'll also notice that functional code is often more condensed, which can make it seem dense or hard to follow at first. It takes time to become comfortable reading functional-style code. Be patient as you adapt to this new way of thinking. You do have the advantage of knowing the C# syntax. You're not learning an entire new programming language. Another area that may seem unfamiliar is arrow notation. This is commonly seen in functional languages when defining a function signature. Just as UML diagrams visually represent the structure and relationships in object-oriented programming, arrow notation provides a clear and concise way to represent function signatures and transformations in functional programming. In this example, it shows a transformation. The function takes a double parameter and returns an int. Arrow notation shouldn't be confused with the C# lambda operator, even though they look similar. Arrow notation is a simplified way to define or notate function signatures at a high level. The C# lambda operator is used to define lambda expressions, which are anonymous functions that can contain expressions or statements. Here, you can see both. On top is the arrow notation, showing a function that takes in an integer and yields a string. The C# version is more verbose, but that is because it is actual working code. When learning functional programming, you'll often come across arrow notation. Functional programming is a declarative paradigm, which means it focuses on what the program should accomplish rather than how to accomplish it. This approach contrasts with imperative programming, where the focus is on step-by-step instructions or code statements that change the program state. In functional programming, you rely on expressions and declarations instead of sequences of commands. An expression is evaluated to produce a value, and this value is the primary focus. For example, instead of writing loops with mutable state, you might use a higher-order function like map, filter, or reduce to transform data in a clear and predictable way. Like any technology, functional programming has its own special terminology. Let's spend a few minutes looking at some of the jargon associated with functional programming. First, let's talk about methods and functions. In C#, the term method is more commonly used, but methods and functions are closely related concepts. In C#, a method is a block of code that belongs to a class or an object. It can be static, belonging to the class itself, or instance-based, belonging to an object. Methods can return a value or perform an action without returning anything; what we call void methods. They often interact with the state of the object or class, which is why they're typically called methods rather than functions. The term function has been around for a long time, and it is fundamental in many programming paradigms, including both procedural and functional programming. A well-built function in functional programming languages describes a block of code that takes inputs and produces an output without altering any state. In C#, the correct term for a function is a method. However, when discussing concepts from functional programming such as pure functions, higher-order functions, or function composition, the term function is often used even in the context of C#. In this course, I'll use the term function to keep things simple and focused on the functional programming concepts. However, remember that C# offers multiple ways to implement functions. One way is a method on a type. This is a function that belongs to a class or struct, and is called directly on the type like static methods. A method on the object is a function that is called on an instance of a class or struct. An anonymous method is a method without a name. It allows you to define a block of code inline without needing to formally declare a method elsewhere in your type. Anonymous methods are typically used where you would otherwise pass a delegate instance, enabling you to avoid the need for separate method definitions. Lambda expressions are a short way to define anonymous functions often used with delegates and LINQ. Local functions are functions that are defined inside another method to keep logic localized within a function. Are you surprised that functions are a key part of functional programming? Probably not. It's called functional programming after all. What that means for us is that functions are building blocks of the code that can be dynamically created, stored in variables and collections, passed as parameters into and out of other functions. In other words, you can perform all the operations you do on other value types with a function. In functional programming, the ideal function format is called a pure function. They closely resemble mathematical functions. They compute a value based on the input values. When called multiple times with the same set of inputs, the result is always the same answer. Plus, the function itself causes no side effects. I'll define side effects in more detail soon. A higher-order function is a function that takes one or more function parameters as input or returns a function as the output. The opposite of this type of function is known as a first-order function. In other words, functions that take standard parameters are first order. Since functional programming is built on functions, there are times when it is desirable to organize the functions into a composite. This is called functional composition. It is similar to chaining functions together by call order. Essentially, it is creating a new function that is comprised of other functions. In an imperative language, this would be accomplished by creating a wrapper function and calling the other functions from within it. Shared state is convenient in some scenarios, but it has major drawbacks. As a young programmer, my mentors told me to avoid global variables. This is still good advice, and it is a key principle of functional programming. This is even more important when writing parallel code or working with multiple threads. Avoiding shared state ensures that the timing and order of function calls do not impact the results of the calculation. It helps avoid coupling between functions, and it makes refactoring your code easier. A natural outcome from avoiding shared state is to consider making all variables and data immutable. If the data cannot change in a variable, it doesn't matter if it is shared between functions because no function or thread can alter the data inadvertently. Functional programming languages build this into their feature set. For example, in F#, a variable is initialized with a value. Any attempts to change that value are not allowed. I know this question is rising in your mind. If we can't change the variable value or mutate data, how can we build any real-world applications? That answer has many dimensions, but there's one important way to think about it. When all items are immutable, you start thinking differently about programming. It is helpful to change your mindset to transforming the data rather than mutating it in place. SQL queries and LINQ queries are good examples of this transformational approach. In both cases, you transform the original data through various functions like select, sort, and filters, rather than modifying the original data. In other words, you start with some data, transform it with a filter, and now you have a transformed and separate version of it. Another principle is to avoid side effects. Ideals like no shared state and immutability reduce the likelihood of side effects. Even so, we should take a minute to discuss what this means. That's the topic for the next video.

Contents