The intention of this post is not to say one paradigm is better than the other. It is just to show common patterns you run into and their functional equivalents.
If there is a pattern I missed and you would like to see, post it in the comments below. Warning, boil your pattern down to the least common denominators. I can't convert a 100 line function! ;)
if / else
Imperative
const hour = 14
let greeting
if (hour < 18) {
greeting = 'Good day';
} else {
greeting = 'Good evening';
}
Functional
The if
can now be re-usable as function getGreeting
.
A:
const isDay = hour => hour < 18
const getGreeting = hour => isDay(hour) ? 'Good Day' : 'Good Evening'
const greeting = getGreeting (hour)
B:
import ifElse from 'mojiscript/logic/ifElse'
const isDay = hour => hour < 18
const getGreeting = ifElse (isDay) (() => 'Good Day') (() => 'Good evening')
const greeting = getGreeting (hour)
if (no else)
Imperative
let name = 'joel'
if (name != null) {
name = name.toUpperCase()
}
Functional
isNotnull
and toUpperCase
are re-usable functions. name
is not overwritten, instead upperName
is created.
import when from 'mojiscript/logic/when'
const isNotNull = obj => obj != null
const toUpperCase = when (isNotNull) (string => string.toUpperCase ())
const name = 'joel'
const upperName = toUpperCase (name)
Summing an Array
Imperative
const values = [1, 2, 3]
let sum = 0
for (const x of values) {
sum = sum + x
}
Functional
Do not mutate sum
.
A:
const values = [1, 2, 3]
const add = (x, y) => x + y
const sum = values.reduce(add)
B:
import reduce from 'mojiscript/list/reduce'
const add = x => y => x + y
const sum = reduce (add) (0)
const values = [1, 2, 3]
sum (values)
for / if (1)
Imperative
const values = [1, 2, 3, 4, 5]
let evens = []
for (const x of values) {
if (x % 2 === 0) {
evens.push(x)
}
}
Functional
Do not mutate evens
.
import filter from 'mojiscript/list/filter'
const values = [1, 2, 3, 4, 5]
const isEven = num => num % 2 === 0
const evens = filter (isEven) (values)
for / if (2)
Imperative
const values = [1, 2, 3, 4, 5]
for (const x of values) {
if (x % 2 === 0) {
console.log(`${x} isEven`)
}
}
Functional
Use when
for conditional execution.
import map from 'mojiscript/list/map'
import when from 'mojiscript/logic/when'
const isEven = num => num % 2 === 0
const logWhenEven = when (isEven) (x => console.log (`${x} isEven`))
const values = [1, 2, 3, 4, 5]
map (logWhenEven) (values)
Breaking a loop early
Imperative
const values = [1, 2, 3]
let sum = 0
for (const x of values) {
if (x > 3) break
sum = sum + x
}
Functional
reduceWhile
is like reduce
, but accepts a predicate to "break" early.
import reduceWhile from 'mojiscript/list/reduceWhile'
const add = x => y => x + y
const lte3 = num => num <= 3
const sum = reduceWhile (() => lte3) (add) (0) (values)
if / else if / else
Imperative
const fn = temp => {
if (temp === 0) return 'water freezes at 0°C'
else if (temp === 100) return 'water boils at 100°C'
else return `nothing special happens at ${temp}°C`
}
fn(0) //=> 'water freezes at 0°C'
fn(50) //=> 'nothing special happens at 50°C'
fn(100) //=> 'water boils at 100°C'
Functional
import cond from 'mojiscript/logic/cond'
import $ from 'mojiscript/string/template'
const fn = cond([
[0, 'water freezes at 0°C'],
[100, 'water boils at 100°C'],
[() => true, $`nothing special happens at ${0}°C`]
])
fn(0) //=> 'water freezes at 0°C'
fn(50) //=> 'nothing special happens at 50°C'
fn(100) //=> 'water boils at 100°C'
Setting properties
Imperative
const obj = {
one: 1
}
obj.two = 2
Functional
Do not mutate original object, shallow clone it and then add the new prop.
note: When objects are mutable, you must deep clone. If objects are immutable, you can shallow clone, which has obvious performance benefits.
const obj = {
one: 1
}
const newObj = {
...obj,
two: 2
}
Modifying Arrays
Imperative
const values = [1, 2, 3]
values.push(4)
Functional
Do not mutate values
.
A:
const values = [1, 2, 3]
const newValues = [...values, 4]
B:
For large arrays, use an immutable library like list for high performance immutable Arrays.
import L from 'list'
const values = L.from([1, 2, 3])
const newValues = L.append(4, values)
Classes
Imperative
Prone to errors.
class Cat {
constructor() {
this.sound = 'Meow'
}
talk() {
return this.sound
}
}
const cat = new Cat()
const talk = cat.talk
cat.talk() //=> 'Meow'
talk() //=> Error: Cannot read property 'sound' of undefined
Functional
Separation of function from data for maximum reusability.
const cat = {
sound: 'Meow'
}
const dog = {
sound: 'Woof'
}
const talk = animal => animal.sound
talk (cat) //=> 'Meow'
talk (dog) //=> 'Woof'
Nested for loop
Imperative
let box = ''
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 5; x++) {
box = box + '* '
}
box = box + '\n'
}
Functional
No more nesting. Immutable.
import reduce from 'mojiscript/list/reduce'
import range from 'mojiscript/list/range'
const makeCols = cols =>
reduce (acc => () => acc + '* ') ('') (range (0) (cols))
const makeBox = ({ cols, rows }) =>
reduce (acc => () => `${acc}${makeCols (cols)}\n`) ('') (range (0) (rows))
const box = makeBox ({ cols: 5, rows: 5 })
//=> * * * * *
//=> * * * * *
//=> * * * * *
//=> * * * * *
//=> * * * * *
And reusable!
const makeTriangle = length =>
reduce
(acc => i => `${acc}${' '.repeat(length - i)}${makeCols (i + 1)}\n`)
('')
(range (0) (length))
const triangle = makeTriangle (5)
//=> *
//=> * *
//=> * * *
//=> * * * *
//=> * * * * *
Null guard
Imperative
const toUpper = string => {
if (string != null) {
return string.toUpperCase()
}
}
Functional
A:
This example wraps the argument in a Maybe
type and then unwraps it at the end. In a typical FP app, you would be using the Maybe
throughout your app, so you would not need to wrap and unwrap the string
. So this is a little more verbose than you would normally see.
import S from 'sanctuary'
const toUpper = S.pipe ([
S.toMaybe,
S.map (string => string.toUpperCase ()),
S.maybeToNullable
])
// If you use `Maybe` throughout your app, this would be your `toUpper` function.
const toUpper = S.map (string => string.toUpperCase ())
B:
maybe
is a function decorator that executes the function only if an argument is supplied. Now our null guard is reusable. More on function decorators here: Functional JavaScript: Function Decorators Part 2 #JavaScript
const maybe = func => (...args) =>
args.length === 0 || args[0] == null
? args[0]
: func(...args)
const toUpper = maybe(string => string.toUpperCase ())
End
My articles are very Functional JavaScript heavy, if you need more FP, follow me here, or on Twitter @joelnet!
More articles
Ask me dumb questions about functional programming
Let's make a DEV.to CLI... together
Let's talk about auto-generated documentation tools for JavaScript
Top comments (11)
This almost looks like lisp, I like it ❤️
One trick I learned recently to making curried functions readable is to format them with spaces between the parens. I live this style :
I also have
eslint-config-mojiscript
to enforce this with eslint!Cheers!
Yeah, I add the spaces between the parenthesis too, but I don't curry all functions by default, only when I need so to do function composition.
That was one requirement of MojiScript, all functions must be curried. When every function is curried, every function can be composed!
There are also no callbacks. Every callback-style function must be promisified.
Cheers!
If you allow mutations, you have to deep clone. If you disallow mutations, shallow cloning is enough.
This is how libraries like list or immutable.js are so fast. They disallow mutations.
One of the advantages of FP.
I found that for-loop sometimes is more readable than functional version, but functional version is better in most case.
For example, I will use
for-or
along withawait
when I want toawait
multiple promises one by one.vs
I agree with you about that example being easier to read. I would still prefer consistency in the app and ban all for loops in favor of map reduce and filter.
You could also break the one liner down a bit to help with readability and reusability.
Great article, @joelnet !
Thanks! I appreciate the kudos. They keep me writing more :)
Cheers!
I think functional style is nice but not in all cases. For loops and if statements are part of the language, why would u use external library over something that's already available to u?
You can't be immutable with for loop. If statements also encourage side effects and mutations.
When your code base is all functions, you can create new functions from existing functions.
Composability depends on limiting side effects and preventing mutations.
There are many many benefits that aren't fully realized until you inmerse yourself into an FP codebase.