A bit of functional fun

Tasking myself with writing a budget tracker, I have got back into a bit of functional programming. This may well bore the pants off you, but just thought I would share.

The following is a small functional library I put together last night along with some comments and examples. Could easily just use ramdajs, lodash or underscore, but it’s nice to play:)

// Functor will enable me to chain methods as well
// as inspect returned values along the way
const Functor = x => ({
    map: f => Functor(f(x)),
    inspect: () => (console.log(x), Functor(x)),
    fold: f => f(x)
})

// A simplified compose function that expects two functions 'f' and 'g' as arguments.
// The returned function expects a value 'x', which first of all will be passed to function 'g'.
// The returned value from function 'g' will then passed to function 'f' returning a final value.
//
// compose example
// const halve = (x) => x / 2
// const sqr = (x) => x * x
// const halveAndSquare = compose(sqr, halve) <- note order right to left
// const squareAndHalve = compose(halve, sqr)
//
// console.log(halveAndSquare(10)) -> 25
// console.log(squareAndHalve(10)) -> 50
const compose = (f, g) => (x) => f(g(x))

// values example
// values({x: 2, y: 5}) -> [2, 5]
const values = (obj) => Object.values(obj)

// Depending on the number of arguments supplied the following
// curried functions will return either a value or a function that expects a value

// multiply example
//
// const multiplyBy5 = multiply(5)
// [1, 2, 3].map(multiplyBy5) -> [5, 10, 15]
const multiply = (x, y) => (y !== undefined) ? x * y : (y) => x * y

// add example
//
// [1, 2, 3].reduce(add, 0) -> 6
const add = (x, y) => (y !== undefined) ? x + y : (y) => x + y


// map example
//
// const double = multiply(2)
// map(double, [1, 2, 3]) -> [2, 4, 6]
const map = (fn, array) => {
    const mapFn = (array) => array.map(fn)

    return (array !== undefined) ? mapFn(array) : mapFn
}

// map, reduce and compose example
//
// const double = multiply(2)
// const doubleAndSum = compose(reduce(add, 0), map(double))
// doubleAndSum([1, 2, 3]) -> 12
const reduce = (fn, init, array) => {
    const reduceFn = (array) => array.reduce(fn, init)

    return (array !== undefined) ? reduceFn(array) : reduceFn
}

// pick example
// pick([x, y], {x: 2, y: 3, z: 4}) -> {x: 2, y: 3}
//
// can also be curried
// const xy = pick(['x', 'y'])
// [{x: 2, y: 3, z: 4}, {x: 5, y: 6, z: 7}].map(xy) -> [{x: 2, y: 3}, {x: 5, y: 6}]
const pick = (keys, obj) => {

    const fn = (sourceObj) => {
        return keys.reduce((obj, key) => {
            obj[key] = sourceObj[key]
            return obj
        }, {})
    }

    return (obj !== undefined) ? fn(obj) : fn
}

A usage example for totaling up amounts. Transaction is either -1(expense) or 1(income)

// example of data acquired from form entries
const entries = () => new Map([
    ['kxw9vxyzh7kuk6lb7', { date: '2022-01-01', description: 'Budget', transaction: '1', amount: '2500' }],
    ['kxvniztqt7088wi5z6', { date: '2022-01-01', description: 'Car Insurance', transaction: '-1', amount: '38' }],
    ['kxvp7fcpvhbgd0fz6vg', { date: '2022-01-01', description: 'Broadband', transaction: '-1', amount: '37' }],
    ['kxw9vnobe37kuk6lb7', { date: '2022-01-01', description: 'Photoshop', transaction: '-1', amount: '12' }]
])

const formatCurrency = (locale, currency) => (amount) => (
    amount.toLocaleString(locale, {
        style: 'currency',
        currency: currency
    })
)

const updateTotal = (() => {

    const toSterling = formatCurrency('en-GB', 'GBP')

    const sumAmount = (sum, [x = 0, y = 1]) => add(sum, multiply(x, y))

    const getPairedValues = compose(values, pick(['transaction', 'amount']))

    return ({ sumTotal, cache }) => {

        const entries = Array.from(cache.values())

        const total = Functor(entries)
            .map(map(getPairedValues))
            .inspect() // [ [ '1', '2500' ], [ '-1', '32' ], [ '-1', '37' ], [ '-1', '12' ] ]
            .map(reduce(sumAmount, 0))
            .inspect() // 2419
            .fold(toSterling)

        sumTotal.textContent = total // £2,419.00
    }
})()

updateTotal({
    sumTotal: {}, // HTMLElement somewhere
    cache: entries()
})

It is maybe over engineered, vanilla JS with destructuring would/does provide a simpler/quicker alternative.

A good exercise though. I like the declarative aspect to it and that with using a functor I am not limited to specific chained methods like for instance with Array. I also like that I can use inspect to monitor the outputs. Another viable alternative would be to use pipe

codepen here

Any feedback, advice appreciated:D

2 Likes

I was in the process of watching a video on ‘lenses’, and was intrigued by his implementation of a curry function. I say ‘intrigued’, I mean ‘stumped’.

This is the function

const curry = (f, arity = f.length, ...args) => (
  arity <= args.length
    ? f(...args)
    : (...argz) => curry(f, arity, ...args, ...argz)
)

To start off, functions have a length property. The length property is equal to the number of parameters or the arity.

So for example

function myFunc(x, y, z) { /* do something */ }

console.dir(myFunc)
/*
ƒ myFunc(x, y, z)
  arguments: null
  caller: null
  length: 3 <- 3 parameters (x, y, z)
  name: "myFunc"
*/

I had to do a bit of logging to try and get my head around the curry function. I’m sure there are ‘rainmen’ out their who can just see the logic at first glance — not me!!

const curry = (f, arity = f.length, ...args) => {
  console.log('function: ', f)
  console.log('function arguments: ', arity)
  console.log('supplied arguments: ', ...args)

  return arity <= args.length
    ? f(...args)
    : (...argz) => curry(f, arity, ...args, ...argz)
}

and tests

// a simple function to be curried
const prop = (key, obj) => obj[key]

const curryProp = curry(prop)
/* Logged Outputs
function:  (key, obj) => obj[key]
function arguments:  2
supplied arguments: // none
*/

arity <= args.length // false
// note: f, arity and ...args will be store in a closure
// no args at this point
return (...argz) => curry(f, arity, ...args, ...argz)

next

const keyA = curryProp('a')
/* Logged Outputs
function:  (key, obj) => obj[key]
function arguments:  2
supplied arguments:  'a' // 1
*/

arity <= args.length // false
// args is 'a'
return (...argz) => curry(f, arity, ...args, ...argz)

finally

keyA({a: 5})
/*
function:  (key, obj) => obj[key]
function arguments:  2
supplied arguments:  'a', {a: 5} // 2
*/

arity <= args.length // true
return f(...args) // call (key, obj) => obj[key] with supplied arguments and return

So to boil it down, until the point that the curry function receives the final argument it returns (...argz) => curry(f, arity, ...args, ...argz). This function keeps a store of the original given function, it’s number of parameters and a store of arguments supplied to the curryied function so far. Does that make sense?

When the final piece of the puzzle is supplied, in the case of our prop function the ‘obj’ argument it will then call that function and return the value.

Now taking the previous post, and ‘multiply’ as an example it can be refactored like so.

const multiply = (x, y) => (y !== undefined) ? x * y : (y) => x * y

// refactored to 

const multiply = curry((x, y) => x * y)

and testing

console.log(multiply(5, 10)) // 50
console.log(multiply(5)(10)) // 50

console.log([1,2,3,4].map(multiply(5))) // [5,10,15,20]

I have to do this stuff to try and cement the knowledge into my brain. I hope it is of at least some interest.

3 Likes

In functional languages such as Haskell this is all there is.

Take for example

sum :: Integer → Integer → Integer
sum a b = a + b

At first glance it would seem this is a function that takes two arguments. It’s not. It’s a function that takes one argument and returns another function that takes one argument.

So for example I can create a new function like so

sum5 = sum 5

This is a new function that adds 5 to whatever argument you supply it.

No need for the curry function, this is built into the language. Sure, you can still call sum with two arguments, but that’s really syntactic sugar :slightly_smiling_face:

This is how you can inject services like database into functions without the caller of the resulting function knowing what database supplied or indeed without having to be aware a database was supplied at all.

Functional programming is a bit harder to get your head around since imperative programming feels more “natural”, but it’s so nice!

1 Like

I suspect I might have to bite the bullet and venture into Haskell. I have seen a few Haskell related posts on stackoverflow and have to confess, a bit over my head.

I’m very much in the shallow end when it come to functional programming, and it is a real mental workout, but yes ‘so nice’ — very much hooked:)

1 Like

Learning to manipulate streams can aid in understanding functional programming. Libraries like RxJs are functional in nature and require embracing functional concepts to create elegant solutions to problems.

https://blog.briebug.com/blog/using-rxjs-to-write-functional-javascript

1 Like

Warning! A bit of a lengthy one.

assocPath

Functional Setter

If you google online for ‘JS setting nested properties’ you can find various implementations.

I wanted to focus on writing something similar, that would form the setting method for the functional getter/setter lensPath. Following functional programmings non-mutational approach, one key feature to assocPath is that it returns a shallow clone of the source object — leaving the source or store object intact.

A good place to start would be a usage example with lenses.

const user = {
    id: 1,
    personalInfo: {
        name: 'Robert',
        address: { city: 'Timbuktu' }
    }
}

// create a lens focusing on the nested name property
const nameLens = lensPath(['personalInfo', 'name'])

// Under the hood the assocPath method comes into play
const updatedUser = set(nameLens, 'Bob', user)

updatedUser.personalInfo.name // -> Bob

// The source object remains intact
user.personalInfo.name // -> Robert

Recursion

Following ramdaJS’s approach I wanted to use recursion. Breaking problems down recursively is still something of an enigma to me, and albeit I have a grasp of the ‘how it works’, it is somewhat a trial and error exercise.

So starting simple I wrote a function that given a path builds nested objects.

const buildObjectTree = function buildTree([key, ...restOfKeys], value, source = {}) {

    // recursively call buildTree until we are down to one key
    source[key] = (restOfKeys.length)
      ? buildTree([restOfKeys], value, {})
      : value

    return source
}

// parameters (path, value, source)
buildObjectTree(['a', 'b', 'c'], 50, {}) // -> {a: {b: {c: 50}}}

Our base condition is reaching the last key of the path, in this example ‘c’ at which point we assign the value 50 and return {c: 50} and so on {b: {c: 50}} -> {a: {b: {c: 50}}}

Objects and Arrays

Moving on to the next step, assocPath also works with arrays. If we feed a number into the path, it is taken as an index rather than a key.

buildObjectTree(['a', 'b', 0], 'first index', {}) // -> {a: {b: ['first index']}}

With a simple additional check using isNaN we can figure out if the next key is an array index or an object key and pass the correct object type to our next recursive call.

const buildObjectsTree = function buildTree(
    [key, nextKey, ...restOfKeys], value, source = {}
) {

    source[key] = (nextKey !== undefined)
        ? buildTree(
              [nextKey, ...restOfKeys],
              value,
              // is nextKey not a number?
              // if so pass an object, otherwise an array
              isNaN(nextKey) ? {} : []
          )
        : value

    return source
}

buildObjectsTree(['a', 0, 'c'], 50, {}) // -> { a: [{c: 50}] }

Merging with source data

So far we have been working with empty objects, but we want to be able to set properties on existing data, either over-writing or creating entirely new properties.

We can see here with the current function, that we are completely over-writing the exisitng object and in the process losing the ‘d’ property.

buildObjectsTree(['a', 'b', 'c'], 48, { a: { d: 50 } }) // { a: { b: { c: 48 } } }

We can fix this by checking if a key already exists with hasOwnProperty and pass the existing property instead of a new empty object.

Object.hasOwnProperty.call(object, key) is a tad verbose, so a helper function would be useful.

const hasOwn = (obj, key) => Object.hasOwnProperty.call(obj, key)
const buildTreeMerge = function buildTree(
    [key, nextKey, ...restOfKeys], value, source = {}
) {

    source[key] = (nextKey !== undefined)
        ? buildTree(
              [nextKey, ...restOfKeys],
              value,
              (hasOwn(source, key))
                  ? source[key] // already exists pass this instead
                  : isNaN(nextKey) ? {} : []
          )
        : value

    return source
}

// now 'd' remains intact
buildTreeMerge(['a', 'b', 'c'], 48, { a: { d: 50 } })
// -> { a: { d: 50, 'b': { c: 48 } } }

Mutation

As per the introduction we don’t want to mutate the source object and the current implementation fails.

const sourceObj = { a: 52 }

const updatedObject = buildTreeMerge(['a', 'b', 'c'], 50, sourceObj)
updatedObject // -> { a: { b: { c: 48 } } }
sourceObj // -> { a: { b: { c: 48 } } } <- No good!!

The solution to this, is instead of directly mutating the source object e.g. source[key] = ... we clone the source first, mutate it and return that instead.

Clone and merging

Interesting we can use Object.assign with a target array and an array-like object to create a new array.

Object.assign([], { 0: 'first index', 1: 'second index' })
/*
Array(2)
  0: "first index"
  1: "second index"
  length: 2
*/

So if we want to clone our source, which maybe an array or an object, and mutate it, this will do the trick!

Object.assign(
    Array.isArray(source) ? [] : {}, // target
    source, // clone the source
    { [key]: value } // add the new index or key with value
)

Final assocPath

Let’s create a merge function first

const mergeObjects = (key, value, source) => (
    Object.assign(
        Array.isArray(source) ? [] : {}, source, { [key]: value }
    )
)

Final function

const assocPath = function buildTree(
  [key, nextKey, ...restOfKeys], value, source = {}
) {

    // assign to a temporary variable
    const newValue = (nextKey !== undefined)
        ? buildTree(
              [nextKey, ...restOfKeys],
              value,
              (hasOwn(source, key))
                  ? source[key]
                  : isNaN(nextKey) ? {} : []
          )
        : value

    // return mutated clone
    return mergeObjects(key, newValue, source)
}

Quick test

const user = {
    id: 1,
    personalInfo: {
        name: 'Robert',
        address: { city: 'Timbuktu' }
    }
}

const updatedUser = assocPath(['personalInfo', 'name'], 'Bob', user)
updatedUser.personInfo.name // -> 'Bob'
user.personInfo.name // -> 'Robert'

Conclustion

It needs some proper testing, but albeit probably not the best solution, it appears to work :slight_smile:

The next stage will be to write a nested getter function, which should be a bit more straight forward.

2 Likes

What is the difference between this and using a state store like redux.

That video is from nearly half a decade ago. Surely there have been advancements since then.

This is just a bit of a programming and learning exercise — A look under the hood.

As I say I don’t know redux, maybe someone here who is more experienced in both functional programming and redux can give you a better answer.

Video deleted.

Shame. I’ve been following this thread and would have checked it out.

1 Like

Sorry James, here you go.

1 Like

The last commit to shades js was over 3 years. Probably not something worth investing time with learning.

Another look at Currying and a Gotcha.

Reading through Mastering Functional Programming in Javascript I was looking at another implementation of curry using function’s bind method.

const curry = (fn) =>
  fn.length === 0 ? fn() : x => curry(fn.bind(null, x))

Bind returns a function with fixed arguments changing the length property accordingly.

const fn1 = (x, y, z) => x + y + z
fn1.length // -> 3
const fn2 = fn1.bind(null, 5)
fn2.length // -> 2
const fn3 = fn2.bind(null, 10)
fn3.length // -> 1

The above curry function is basically a recursive function with the difference being the recursive call is wrapped inside of a returned function. So you end up with a kind of manual recursion where by you invoke the returned functions up untill the base condition fn.length === 0 is met.

At this point the function which now has all arguments bound is invoked — nice!

Default parameters gotcha

This is where I ran into an issue, best illustrated with some code

const sum = curry((x, y = 10) => x + y)
const add5 = sum(5)
add5(20) // -> Uncaught TypeError: add5 is not a function

This error is also thrown with ramdaJs’s curry.

If we look at MDN’s Function.length page, we get an answer as to why this happens.

console.log((function(a, b = 1, c) {}).length)
// 1, only parameters before the first one with
// a default value is counted

Passing an arity argument

The curry function from the earlier post does almost have us covered. Here it is again to save scrolling.

const curry = (f, arity = f.length, ...args) => (
  arity <= args.length
    ? f(...args)
    : (...argz) => curry(f, arity, ...args, ...argz)
)

We can pass a second argument which will fix the number of arguments.

const sum = curry((x, y = 10) => x + y, 2) // fixed to 2 arguments
const add5 = sum(5)
add5(20) // -> 25 Good!

However when we drop the argument in our call to add5, due to this condition arity <= args.length, instead of the default parameter being used the second function is returned instead.

const add5 = sum(5)
add5() // -> (...argz) => curry(f, arity, ...args, ...argz) Bad!

RamdaJS’s curryN appears to run into the same problem.

const sum = R.curryN(2, (x, y = 10) => x + y) // fixed to 2 arguments

const add5 = sum(5)
add5() // -> returns a function

Solution

Well hopefully anyway.

In the aforementioned Mastering Functional Programming book, there is a solution to working with variadic functions which works equally well with default parameters.

const curryN = (fn, arity = 1) =>
  arity === 0
    ? fn()
    : x => curry(fn.bind(null, x), arity - 1) // decrement arity

It appears that bind does carry across the default parameter functionality, and this time we’re good.

const sum = curryN((x, y = 10, z) => x + y + z, 3) // Fixed to 3 arguments

const add5 = sum(5)
const add5andDefault = add5() // default of 10
add5andDefault(10) // -> 25

Conclusion

Prior to this I had considered calling toString on the given function, and regex matching arguments to get the arity, but I think this fixed arity solution will suffice.

I will post a getByPath function breakdown next. It was only on running a curry test with it, that I ran into this issue and thought it was worth looking into.

I came across this article and thought it would be relevant to this discussion. Particularly regarding the organization of functions into modules using mjs files. Even using unbundled module files for small applications instead of classic scripts.

1 Like

I love it when your code unravels, I guess that is the importance of proper testing. I have amended deepclone to handle dates and any primitive values that maybe passed in.

Here is an update. If any howlers stand out I would appreciate the feedback.

Deep Cloning in JS

Using Property Descriptors

We are going to look at a deep clone solution utilising two built-in Object methods getOwnPropertyDescriptors and defineProperties.

Shallow Copying

To start with let’s first have a look at a typical example of shallow copying and the potential pitfalls we may encounter with this approach.

const person = {
   name: ['Fred', 'Flinstone'],
   age: 45
}
// shallow copy using ...spread operator
const clonedPerson = {...person}
clonedPerson.name[0] = 'Barney'
clonedPerson.name[1] = 'Rubble'

console.log(clonedPerson)
// { name: ['Barney', 'Rubble'], age: 45 } all good

console.log(person)
// { name: ['Barney', 'Rubble'], age: 45 } Not good!

We can see by making changes to our cloned object we have also modified our source object.

The reason for this is that we are dealing with a reference type in the form of a name array property. The property doesn’t hold data or values, but rather a pointer or an address to a particular place in memory where the array data is held.

When we make a shallow copy, the name property on the cloned person object also points to that same address in memory. Therefore any changes we make to that data will be reflected in both objects.

Property Descriptors

Carrying on for a moment with shallow copying we are going to look at an alternative approach to the spread operator focusing on property descriptors.

Let’s enhance our original object by adding a getter accessor method which will return the full name of person.

const person = {
   name: ['Fred', 'Flinstone'],
   age: 45,
   // getter function
   get fullName() {
     return this.name.join(' ')
   }
}

console.log(person.fullName)
// Fred Flinstone

Again we will make a clone of the person object and modify it’s properties

const clonedPerson = {...person}
clonedPerson.name[0] = 'Barney'
clonedPerson.name[1] = 'Rubble'

console.log(person.fullName)
// Barney Rubble - Not what we wanted, but to be expected

console.log(clonedPerson.fullName)
// Fred Flinstone - What's going on here?

An odd result. Albeit we modified our cloned object, fullName is returning the original source object’s name. If we inspect the property descriptor focusing in on the getter function our issue becomes a little bit clearer.

Person

// source object
console.dir(Object.getOwnPropertyDescriptor(person, 'fullName'))

Object
    configurable: true
    enumerable: true
    get: ƒ fullName() // getter function
    set: undefined

Cloned Person

// cloned object
console.dir(Object.getOwnPropertyDescriptor(clonedPerson, 'fullName'))

Object
    configurable: true
    enumerable: true
    value: "Fred Flinstone" // replaced with value
    writable: true

On copying the source object the getter function has been evaluated and replaced with a value. You may also notice that albeit we didn’t create a setter function it has been replaced as well.

To solve this issue we can use getOwnPropertyDescriptors and defineProperties instead to make a shallow copy.

const clonedPerson = Object.defineProperties(
    {}, // target object to clone to
    Object.getOwnPropertyDescriptors(person)
)

console.dir(Object.getOwnPropertyDescriptor(clonedPerson, 'fullName'))

Object
    configurable: true
    enumerable: true
    get: ƒ fullName() // getter function intact
    set: undefined

Clone Target Object

Before moving on we need to tackle one issue and that is the target object we are cloning to. Again the best way to illustrate this is with an example.

const person = ['Fred']

const clonedPerson = Object.defineProperties(
    {}, // target object to clone to
    Object.getOwnPropertyDescriptors(person)
)

console.log(clonedPerson)
// {0: 'Fred', length: 1}

clonedObject.push('Rubble') // TypeError: clonedObject.push is not a function

You can see by looking at the curly parentheses on the logged output that the original Array has been converted to an Object, or what is more commonly called an Array-like object. In the process we have lost access to the Array.prototype and it’s methods e.g. push. We therefore need a way of checking the source object type to make sure our target matches that type.

Conveniently the solution to this comes in the form of Object.prototype.constructor, which returns an empty object matching the object the constructor is called on.

const obj = {a: 1, b: 2}
const arr = [1, 2, 3]

arr.constructor() // -> [] empty array
obj.constructor() // -> {} empty object

The clone can now be amended accordingly. Let’s wrap this up with a shallow clone function.

const shallowClone = (sourceObject) =>
    Object.defineProperties(
        sourceObject.constructor(),
        Object.getOwnPropertyDescriptors(sourceObject)
    )

Working with Nested Properties

On to the meat and potatoes of our deep clone function — dealing with nested properties. These nested properties will be held inside our two reference types Arrays and Objects.

On running through the object properties if we come across either of these types we will know we need to dig a bit deeper into our object, and to do this we will use recursion. By using a combination of recursion and shallow copying we will end up with two objects with distinct object properties.

Final Script

The best way I think to explain the process is to have a look at the final script and do a bit of a breakdown. We will start with writing a couple of helper functions we can utilise in our final script to determine if the current property is an Array or an Object.

// Helper functions
const isArray = obj => Array.isArray(obj)
const isObject = obj => ({}.toString.call(obj) === '[object Object]')
// final deepClone script
const deepClone = function deepClone (source) {
    if (source instanceof Date) return new Date(source.getTime())

    if (isArray(source) || isObject(source)) {
        const clone = shallowClone(source)

        for (const [key, prop] of Object.entries(clone)) {
            if (typeof prop === 'object') clone[key] = deepClone(prop)
        }
        return clone
    }
    return source
}

The first line of deepClone’s execution checks to see if the source object is a Date and if so returns a copy.

The second line checks to see whether we have a collection of properties in the form of an Object or an Array and if so we make a shallow clone of that object.

Using Object.entries an array of [key, value] pairs from the shallow clone is created which we then loop through. If the current property in the loop is an object type the property is passed to a recursive call of deepClone deepClone(prop) and whatever is eventually returned replaces the shallow clone property with that key clone[key] as a new value.

The recursive process is repeated, digging deeper and deeper into the object until it reaches the innermost properties. In other words we have reached a point where the passed source object doesn’t contain any properties matching a type of object.

It is this that serves as what is known as a base condition and is the point at which clones are finally returned one after another or more accurately with the clone key assignment one inside another. Here is an example.

// object to clone
{ a: { b: { c: 2 }, d: 3 } e: 4 }

// From the tailend
shallow clone of { c: 2 } is returned overwriting 'b'
shallow clone of { b: { c: 2 }, d: 3 } is returned overwriting 'a'
shallow clone of { a: { b: { c: 2 }, d: 3 } e: 4 } is returned out of the function

The final line in the code return source returns any primitive values that may have been passed to deepClone.

On to testing.

Source test object

Carrying on the theme the following person object has a personalInfo property which contains a name and address. In addition it has a getter and setter that will read and update the nested address property.

const person = {
    date: new Date(),

    personalInfo: {
        name: ['Fred', 'Flinstone'],
        address: {
            road: '222 Rocky Way',
            city: 'Bedrock 70777'
        }
    },

    get address () {
        return this.personalInfo.address
    },

    set address (newAddress) {
        const { personalInfo } = this
        personalInfo.address = {...personalInfo.address, ...newAddress}
    }
}

const clonedPerson = deepClone(person)

Running some basic tests

We will test to see if the cloned object has functional getters and setters and that we are able to modify nested properties of the clone without mutating the sources object.

To start with a test to confim that the accessors are indeed functions.

// using a simple helper function for readibility
const address = getOwnPropDescriptor(clonedPerson, 'address')

console.log(typeof address.get === 'function') // true
console.log(typeof address.set === 'function') // true

Then a simple modification to the cloned object’s address property and some test outputs.

clonedPerson.address = {road: '301 Cobblestone Way'}

console.log(clonedPerson.address)
// {road: '301 Cobblestone Way', city: 'Bedrock 70777'}

console.log(person.address)
// {road: '222 Rocky Way', city: 'Bedrock 70777'}

We can see that we are able to successfully modify the nested address property on our clone without affecting the same property on our source object.

Conclusion

The examples I have given here of copying objects and their accessors do fall very much into ‘edge case scenario’. That said given the relative simplicity of using property descriptors with it’s ability to copy all own properties of an object I do think it is a worthy exploration.

Edit: I’ve opted to remove the recursive option and it’s labored breakdown. I think we’ve done enough of that. Instead I have incorporated optional chaining.

Get by Path

Getting a nested property by path

Following on from deepClone we are going to create a method which will enable us to access and return a nested property.

The function will take two arguments, a path and a source object. The path will consist of an Array that can contain strings and numbers e.g. ['personalInfo', 'name', 0]. These will correspond to object properties and indexes in an Array.

Let’s start with an example.

const person = {
    id: 1,
    personalInfo: {
        name: ['Fred', 'Flinstone'],
        address: {
            road: '222 Rocky Way',
            city: 'Bedrock 70777'
        }
    }
}

console.log(getByPath(['personalInfo', 'address', 'road'], person))
// '222 Rocky Way'

console.log(getByPath(['personalInfo', 'name', 1], person))
// 'Flinstone'

Using a for of loop

With deepClone recursion was the appropriate choice. Here we can just use a simple loop.

const getByPath = (keys, source) => {
    let value = source

    for (const key of keys) {
        if (!hasOwn(value, key)) return undefined
        value = value[key]
    }

    return deepClone(value)
}

A reference to the source object is assigned to value.

We then loop through the path keys. If the property in our path doesn’t exist undefined is returned otherwise we re-assign value to the next property.

On successful completion of the loop and path a deepClone of the property is returned.

Optional Chaining (?.)

A modern approach to accessing nested properties is to use optional chaining. If a property in the chain isn’t valid instead of throwing a type error it short-circuits returning undefined.

Here is an example where we try to access name on the undefined property info.

console.log(person.info.name)
// TypeError: Cannot read properties of undefined (reading 'name')

// Optional Chaining
console.log(person?.info?.name)
// undefined

This removes the need for explicitly checking if a property exists as we did with hasOwnProperty in the previous example.

const getByPath = (keys, source) => {
    let value = source
    for (const key of keys) value = value?.[key]

    return deepClone(value)
}

Here is an alternative using Array.reduce

const getByPath = (keys, source) => (
    deepClone(keys.reduce((prop, key) => prop?.[key], source))
)

A Bit of Currying

The above getByPath may seem a bit pedestrian, after all why not just access the property manually with const name = person?.personalInfo?.name. It is when we introduce composition — the process of combining functions to make a new function — that these simple little functions come into their own.

Lets illustrate how we might use currying with getByPath, starting with a simple collection of objects.

const characters = [
    {
        id: 1,
        personalInfo: {
            name: ['Fred', 'Flinstone'],
            address: {
                road: '222 Rocky Way',
                city: 'Bedrock 70777'
            }
        }
    },
    {
        id: 2,
        personalInfo: {
            name: ['Barney', 'Rubble'],
            address: {
                road: '223 Rocky Way',
                city: 'Bedrock 70777'
            }
        }
    }
]

We will then use our curried getByPath to access the addresses in the characters collection.

const addresses = getByPath(['personalInfo', 'address'])
console.log(characters.map(addresses))
/*
[ 
    { road: '222 Rocky Way', city: 'Bedrock 70777' },
    { road: '223 Rocky Way', city: 'Bedrock 70777' } 
]
*/

By using currying and getByPath we create a new function addresses with the path as a fixed argument. This enables us map out the addresses in what is almost plain English or what is known as programming declaratively.

Conclusion

We have looked at a few implementations of what is essentially a very simple function. I wanted to give you an example of currying to illustrate how through composition we can take a simple function and turn it into something a little more special.

Where I would take things from here is to compare how this deep clone does up against the top 5 deep clone libraries that has been developed, and compare how well it stands up against all the features that are supported by those other libraries, and the in-depth tests that they use.

2 Likes

Thanks for the feedback Paul, I confess I am starting to lose the plot. lol

Sorting Table Columns

A Functional Approach

I was toying with whether to post this in the ‘Sorting data in ascending and descending order’ thread, but I think given the added complexity and that getBypath and currying are involved it would be better placed here.

For reference here is a link to that thread. Anyway onwards…

Ascend and Descend

Looking through RamdaJS’ docs I came across two functions ascend and decend.

They are comparator functions that have three parameters (fn, a, b). The first is a callback that will be invoked on the other two arguments a and b.

It is maybe helpful if I show an example from the official RamdaJS site

const byAge = R.ascend(R.prop('age'));
const people = [
  { name: 'Emma', age: 70 },
  { name: 'Peter', age: 78 },
  { name: 'Mikhail', age: 62 },
];
const peopleByYoungestFirst = R.sort(byAge, people);
  //=> [{ name: 'Mikhail', age: 62 },{ name: 'Emma', age: 70 }, { name: 'Peter', age: 78 }]

If we look under the hood, you can maybe get a better idea. These are basic functions with the added benefit of being curried.

var ascend = _curry3(function ascend(fn, a, b) {
  var aa = fn(a);
  var bb = fn(b);
  return aa < bb ? -1 : aa > bb ? 1 : 0;
})

The currying enables us to pass in a callback, returning a binary function we can then pass straight into sort. To illustrate.

// pass in a callback that will be invoked on both a and b
const lettersAscending = ascend((letter) => letter.toLowerCase())

['d', 'C', 'b', 'a'].sort(lettersAscending) // ['a', 'b', 'C', 'd']

I appreciate that keeping things ‘stupid simple’, is often the best way, but given what I have learned about localCompare and the pitfalls of an (a < b) and (a > b) approach, I opted for a re-write. In fact I opted to go with Intl.Collator.

Intl.Collator is very similar to localeCompare, with the difference being that rather than calling it directly we call it’s constructor and it returns an object with a compare method e.g.

const comparator = new Intl.Collator(undefined, { numeric: true }).compare
// or with destructuring
const { compare } = new Intl.Collator(undefined, { numeric: true })

['d', 'C', 'b', 'a'].sort(compare)
// ['a', 'b', 'C', 'd']

['101', '212', '3', '2'].sort(compare)
// ['2', '3', '101', '212']

With that sorted I was then able to re-write the ascend and descend functions.

const ascend = curry3((fn, a, b) => compare(fn(a), fn(b)))
const descend = curry3((fn, a, b) => compare(fn(b), fn(a)))

Isolating the Table Columns

Sorting the columns involves clicking on a table heading, this then toggles the column between ascending and descending order.

The table headings have cell indexes and these indexes correspond with the table row cell indexes for a particular column.

tableHead  HeadingA    HeadingB    HeadingC
         cellindex 0 cellindex 1 cellindex 2
tabelRow1  cells[0]    cells[1]    cells[2]
tabelRow2  cells[0]    cells[1]    cells[2]

So we access the textcontent from a specific column with

currTableRow.cells[currHeading.cellIndex].textContent

Given we have a function handy, I opted to use getByPath to do this. That way I could pass the returned function into ascend or descend as a callback.

const getTextFromColumn = getByPath(['cells', currentHeading.cellIndex, 'textContent'])

rows.sort(ascend(getTextFromColumn))

Sort Handler

To cut to the chase my final handler module ended up looking like this

// import Ascend and Descend comparator functions
import { getByPath, ascend, descend } from './functional.js'

const tBody = document.querySelector('#table-people tbody')

const appendRow = tBody.appendChild.bind(tBody) // bind appendChild method to tBody

export const sortHandler = function (event) {
    const currentHeading = event.currentTarget
    // if no data-ascend attribute leave
    if (!currentHeading.hasAttribute('data-ascend')) return

    const getTextFromColumn = getByPath(['cells', currentHeading.cellIndex, 'textContent'])
    const ascending = (currentHeading.dataset.ascend === 'true')
    const compare = (ascending) ? descend : ascend

    Array
        .from(tBody.rows)
        .sort(compare(getTextFromColumn))
        .forEach(appendRow)

    currentHeading.dataset.ascend = !ascending // toggle order direction
}

Here is a codepen. The data comes from a handy API called Mockaroo.

If you are interested here is a Github Repo. There are two versions on there, one Vanilla JS which uses the fetch API and a Node JS version using Axios to pull in the data from Mockaroo. Getting a bit of practice in :slight_smile:

Conclusion

I know there are quicker and dirtier ways to do this, but again love the simple declarative code in the final Array sort. Add to that the re-usability and composability of these functions.

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.