Functional Programming
Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. It is a declarative programming paradigm, which means that programming is done with expressions or declarations instead of statements.
Some programming languages support functional programming paradigm in addition to other paradigms. For instance, JavaScript supports functional programming paradigm in addition to object-oriented programming paradigm. On the other hand, some programming languages are designed to support only functional programming paradigm. For instance, Haskell is a purely functional programming language.
We will look at some of the core concepts of functional programming in this chapter.
Pure Functions
There is a difference between functions in mathematics and functions in programming. In mathematics, a function is a relation between a set of inputs and a set of possible outputs. Therefore, a function satisfies these two properties:
- A function should return the same output for the same input.
- A function should not have side effects.
However, in programming, a function can have side effects and it can return different outputs for the same input. For instance, a random number generator function is a function that returns different outputs for the same input. It does not take any arguments but it returns a different output each time you call it. (If it would be a function in a mathematical sense, it should return only one value because it does not take any arguments.).
A function that prints something to the screen is a function that has side effects. It does not return anything but it changes the state of the screen. Or a function that send a request to a server is a function that has side effects because it manipulates the "outside world" by using electrical signals. A function that reads a file from the disk is also a function that has side effects because it causes the disk to spin and read the file that is located on a specific location on the disk.
Lets look at some examples;
// Some pure functions
(x, y) => x + y;
(person) => (person.age > 18 ? 'Adult' : 'Child');
(str) => JSON.parse(str);
// Some impure functions
(x, y) => x + y + Math.random();
(x, y) => console.log(x + y);
() => fs.readFileSync('file.txt');
(x, y) => fs.writeFileSync('file.txt', x + y);
(x, y) => {
console.log(x + y);
return x + y;
};
In some functional programming languages, all functions must be pure functions (such as Haskell). Pure functions are very useful because they are easy to test. You can test a pure function by giving it an input and checking the output. If the output is the same as you expected, then the function is working correctly. Also, pure functions are easy to understand and most importantly, they are easy to compose. Therefore, building complex systems with pure functions is much easier than building them with impure functions. Because, they are guaranteed to work correctly for each individual function and composing them will not result any unexpected behavior.
First Class Functions
Immutablity
One of the most bug-prone parts of a code is mutable variables (or variables). Because, you can change the value of a
variable at any time from any place. This can cause a lot of bugs. In order to avoid these bugs, you should use immutable
variables as much as possible. Instead of using let
and var
, you should use const
in order to make your variables
immutable. Also, you should avoid mutating objects and arrays. You should use map
, filter
, reduce
functions to
manipulate arrays.
Also you should be careful about scoping. Because scopes can restrict the usage of a variable. If you are defining a variable in a global scope, you can use it from anywhere. This can cause a lot of bugs. For this reason, please try to use local variables as much as possible and keep your scopes as small as possible.
It may be seem to be impossible to avoid mutable variables. However, you can use some techniques to avoid them. For
instance, you can use Map, Filter and Reduce functions can be used. They are higher order functions which means that they
take a function as an argument and they return a new array. These functions are very useful and they are reduces the
need for loops and mutable variables. You should use these functions as much as possible. Also there are some other
functions in javascript that you can use to manipulate arrays. For instance, forEach
, some
, every
, find
, findIndex
etc. You should use these functions as much as possible. Also, you can use lodash
library to manipulate objects and
arrays.
Also, you should return results of a function with return
statement. Mutating a global variable and then reading it
from another function is a bad practice. Instead of that, you should return the result of a function with return
statement. This will make your code much more clear and easy to understand.
If you want to learn more about immutability, you can check Functional Programming Paradigm.
Common Higher Order Functions
Side Effects with Pure Functions
You may wonder that how is it possible to write a program only with pure functions. Because, in real world, you need to interact with the outside world. You need to read files, send requests to servers, print something to the screen etc. However, this is possible in Haskell. At the end of the day nothing is pure because the nature of the problem that you want to solve is not pure. Lets look at an example from Haskell.
-- You can run this program with `runhaskell` command in your terminal but you need to have ghc installed on your system.
-- putStrLn is a function that takes a string and returns an IO action that DESCRIBES how to print the given string to
-- the screen. Type signiture of putStrLn is:
-- putStrLn :: String -> IO ()
-- main is a DESCRIPTION of the program. It is an IO action that DESCRIBES how to run the program.
-- It is not a function it is just a value that describes how to run the program.
main:: IO ()
main = putStrLn "Hello, World!" -- In haskell, we call functions with space instead of parentheses.
You should think IO
as a recipe that describes what a computer should do. Although, executing instructions in a computer
is not a pure operation, the recipe that includes these instructions itself is a completely pure value. Lets look at
another example.
-- This is a pure function that takes a string and returns a string
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
main:: IO ()
main = putStrLn (greet "John")
Lets examine the code above. The haskell compiler first looks at the main
function. It sees that main
is an IO action
that describes how to run the program. However in order to get a complete list of instructions, it needs to evaluate the
putStrLn (greet "John")
expression. In order to evaluate this expression, it needs to evaluate the greet "John"
expression. Therefore it evaluates the greet
function with the argument "John"
. The greet
function returns a string
that is "Hello, John!"
. Then the compiler evaluates the putStrLn
function with the argument "Hello, John!"
. The
putStrLn
function returns an IO action that describes how to print the given string to the screen. We can see its like
this:
main = `Print "Hello, John!" to the screen`
After that, the compiler runs the instructions that are described in the main
function. It prints the string "Hello, John!"
to the screen. Lets look at a more complex example.
greet :: String -> String
greet name = "Hello, " ++ name ++ "!"
-- Lets write our main function that will ask the name from the user and greet the user
main :: IO ()
main = putStrLn "What is your name?"