Skip to content

Instantly share code, notes, and snippets.

@Raincal
Forked from kentcdodds/README.md
Created November 3, 2016 09:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Raincal/a7a9c459f5b0b1b180de6ac1cf683987 to your computer and use it in GitHub Desktop.
Save Raincal/a7a9c459f5b0b1b180de6ac1cf683987 to your computer and use it in GitHub Desktop.
JavaScript Program Slicing with SliceJS

Learning Code

One of the original goals to this project is to help developers learn code. Take this module for example:

export default clone

function clone(item) {
  if (!item) {
    return item
  }
  const type = typeof item
  const string = Object.prototype.toString.call(item)
  const isPrimitive = type !== "object" && type !== "function"
  let result = item

  if (!isPrimitive) {
      if (string === '[object Array]') {
      result = []
      item.forEach((child, index, array) => {
        result[index] = clone(child)
      })
    } else if (type === 'object') {
      if (item.nodeType && typeof item.cloneNode == 'function') {
        result = item.cloneNode(true)
      } else if (!item.prototype) {
        if (string === '[object Date]') {
          result = new Date(item)
        } else {
          result = {}
          for (const i in item) {
            result[i] = clone(item[i])
          }
        }
      } else {
        if (false && item.constructor) {
          result = new item.constructor()
        } else {
          result = item
        }
      }
    }
  }

  return result
}

The clone function is 38 lines of code and has a cyclomatic complexity of 10. Not exactly the most simple code in the world! All the branches that handle edge cases make learning how this works at least a 10 minute task. But with slice-js, you can learn it much more quickly! Let's use slice-js to learn this code.

slice-js takes two inputs: The source code, and a code coverage report. Based on this information, it can create a slice of the program that's relevant for that coverage. Let's just say that we can generate the coverage based on a given usage module. We'll start with a simple object:

import clone from 'clone'
clone('hello')

Based on this usage, a coverage report could be generated and the resulting code slice would look much easier to learn quickly:

export default clone

function clone(item) {
  return item
}

We've gone from 38 lines of code to 1 and the cyclomatic complexity from 10 to 1. That's considerably more easy to learn! But that's not everything that's important in this code. The original code is definitely important. So let's add more use-cases and see how this slice is changed.

import clone from 'clone'
clone('hello')
clone(null)

With that addition of clone(null), we'll get this difference:

export default clone;

function clone(item) {
+ if (!item) {
+   return item
+ }
+
  return item
}

That's pretty reasonable to learn in addition to what we've already learned about this code. Let's add more now:

import clone from 'clone'
clone('hello')
clone(null)
clone({name: 'Luke'})

And here's what the slice looks like now:

export default clone

function clone(item) {
  if (!item) {
    return item
  }
+ const type = typeof item
+ const isPrimitive = type !== "object" && type !== "function"
+ let result = item

+ if (!isPrimitive) {
+   result = {}
+   for (const i in item) {
+     result[i] = clone(item[i])
+   }
+ }

- return item
+ return result
}

Let's do this one more time:

import clone from 'clone'
clone('hello')
clone(null)
clone({name: 'Luke'})
clone({friends: [{name: 'Rebecca'}]})

And with that, we add yet another edge case.

export default clone

function clone(item) {
  if (!item) {
    return item
  }
  const type = typeof item
+ const string = Object.prototype.toString.call(item)
  const isPrimitive = type !== "object" && type !== "function"
  let result = item

  if (!isPrimitive) {
-   result = {}
-   for (const i in item) {
-     result[i] = clone(item[i])
+   if (string === '[object Array]') {
+     result = []
+     item.forEach((child, index, array) => {
+       result[index] = clone(child)
+     })
+   } else {
+     result = {}
+     for (const i in item) {
+       result[i] = clone(item[i])
+     }
    }
  }

  return result
}

The benefit of this approach is that we learn the code use-case-by-use-case. It's much easier to learn bit by bit like this, and slice-js enables this.

Ultra-Tree Shaking ™

Tree shaking is a super cool concept. Here's a basic example of tree shaking from Webpack or Rollup:

math.js

export {doMath, sayMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

function sayMath() {
  return 'MATH!'
}

app.js

import {doMath}
doMath(2, 3, 'multiply') // 6

The tree-shaken result of math.js would effectively be:

export {doMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

However, with SliceJS, we could remove even more code. Like this:

export {doMath}

const multiply = (a, b) => a * b

function doMath(a, b) {
  return multiply(a, b)
}

Imagine doing this with lodash, jquery or react! Could be some pretty serious savings!

The biggest challenge with this would be getting an accurate measure of code coverage. For most applications, you'd have a hard time making sure that your tests cover all use cases, and if you slice code out that's not covered by your test cases, then your users wont get that code and things will blow up. There's still more work to be done here, but I think that it's possible to make a big difference!

Shaking data

Another thing that I think would be super cool to do would be to not allocate memory for objects that are never used. Right now, with SliceJS, here's an example that could be further optimized:

log.js

const currentLevel = 0

const logLevels = {
  ALL: 100,
  DEBUG: 70,
  ERROR: 50,
  INFO: 30,
  WARN: 20,
  OFF: 0,
}

const setCurrentLevel = level => currentLevel = level

export {log, setCurrentLevel, logLevels}

function log(level, ...args) {
  if (currentLevel > level) {
    console.log(...args)
  }
}

app.js

import {log, logLevels, setCurrentLevel}
setCurrentLevel(logLevels.ERROR)
log(logLevels.WARN, 'This is a warning!')

If we tracked data coverage (in addition to branch/function/statement coverage as we do now), then we could slice out the allocation for some of the properties in the logLevels object as well! This would result in:

const currentLevel = 0

const logLevels = {
  ERROR: 50,
  WARN: 20,
}

const setCurrentLevel = level => currentLevel = level

export {log, setCurrentLevel, logLevels}

function log(level, ...args) {
  if (currentLevel > level) {
    console.log(...args)
  }
}

Which would be even cooler in scenarios where the objects are actually significant in length and amount of memory!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment