JavaScript Program Slicing with SliceJS
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.
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!
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!