Sunday, January 1, 2023

Functional Programming – How and Why

Let's start with an example. In Advent of Code 2022 day 1 we are given groups of numbers as a string that look like:

1000
2000
3000

4000

5000
6000

7000
8000
9000

10000

We want to write a program that sums up each group and returns the largest group. The largest in this example is the 4th group, 24000.

Here is an imperative solution:

const input = $('pre').innerText
let runningMax = 0
for (let group of input.split("\n\n")) {
    let runningSum = 0
    for (let num of group.split("\n"))
        runningSum += Number(num)
    runningMax = Math.max(runningMax, runningSum)
}
console.log(runningMax)

And here is a functional solution:

const input = $('pre').innerText
function sumGroup(group) {
    return group
          .split("\n")
          .map(Number)
          .reduce((a,b)=>a+b)
}
const sortedGroups = input
    .split("\n\n")
    .map(sumGroup)
    .sort((a,b)=>a-b)
console.log(sortedGroups.at(-1))

The imperative solution:

  • We declare variables called runningMax and runningSum that we mutate throughout the program
  • We use for loops to iterate

The functional solution:

  • We avoid declaring mutable variables
  • Instead of iterating with for loops, we use higher order functions like map, reduce, and sort

Higher order functions are functions that operate on other functions. map, reduce, and sort take a function as an argument.

How exactly does one do functional programming?

Avoid Favor
mutable state function parameters and immutable data
iterative steps transforming data
loop statements recursion or higher order functions
intertwined data and behavior separated data and behavior

Looking at the two examples above, you may think functional programming is simply using higher level functions. Maybe you are wondering, "map is just a for loop! What if map did not already exist?" A functional programmer would write map themself using recursion. Anything written with loop statements can also be written with recursion.

Imagine we want a range function where range(5) returns [1,2,3,4,5].

Imperative solution:

function range(end) {
    let nums = []
    for (let i=1; i<=end; i++)
          nums.push(i)
    return nums
}

Functional solution:

function range(end, cur=[]) {
    if (cur.length >= end) return cur
    return range(end, [...cur, cur.length+1])
}

Before I started exploring functional programming I would have never thought to implement this function recursively. But now that I compare the two solutions, the recursive solution actually feels more natural.

Why use functional programming

Functional programming has a lot of amazing benefits. Let's explore the biggest things:

Avoid bugs related to mutable state. Just like static typing eliminates the class of bugs caused by dynamic typing, functional programming elimates the class of bugs caused by mutable state. Every programmer has copied an array, modified it, and then spent way too long trying to figure out how the original array also got modified (aka shallow copies vs. deep copies). When mutable state is shared all over code, the potential for bugs like this is increased.

Write declarative code. Imperative code describes how to do things while declarative code describes what we are doing. This means declarative code is often more natural to understand. Think about how the original imperative solution has variables named runningMax and runningSum. It's tempting to name these variables maxGroup and sum, but that would be lying. They start at 0 and change over the course of the program. We are stuck with variable names describing how we do things. In the functional solution, sortedGroups describes exactly what we are doing.

Write code that is easier to test. If you are used to writing imperative code, it might be tempting to write void functions which mutate variables outside of the function's scope. A common way to make games is to declare state as variables and write a void function like update() to mutate those variables each tick. Instead of mutating variables, functional programming favors update taking game state as an argument and returning new state. Now our code is much easier to test. We simply call update with some game state and assert that the result is what we expect.



from Hacker News https://ift.tt/gsFDSnC

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.