Skip to content

Instantly share code, notes, and snippets.

@mrtnbroder
Forked from masaeedu/traversing_state.js
Created March 29, 2019 10:07
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 mrtnbroder/5890b184f7fc544d69b8d996ec889ec2 to your computer and use it in GitHub Desktop.
Save mrtnbroder/5890b184f7fc544d69b8d996ec889ec2 to your computer and use it in GitHub Desktop.
Refactoring to discover Arr.traverse and the State applicative
// This gist shows how to factor a concrete stateful fold in vanilla JS
// into a number of general purpose utilities that can be combined to recover
// the original behavior
// We're going to start from a particular code snippet and iteratively make
// small changes to it to get a finished product
// While reading this post, it is useful to have a diff tool at hand that allows
// you to compare snippets side by side and see what changed in each iteration
// The goal is not to show a process of refactoring that you should be applying
// to your code on a day to day basis, but rather to show how refactoring from a
// concrete use case to an abstraction can help explain the meaning of that
// abstraction, and teach us how to use it
// 1
{
const f = xxs =>
xxs.reduce(
([i, p], xs) => [i + xs.length, [...p, xs.map((_, j) => i + j)]],
[0, []]
);
const input = [[0, 0], [0, 0, 0], [0]];
const [i, result] = f(input);
console.log(result);
}
// Now we start refactoring. As is usually the case with these sorts of things,
// things are going to get much worse before they get better
// 2
// Factor the reduction expression and seed into separate variables
{
const redex = ([i, p], xs) => [
i + xs.length,
[...p, xs.map((_, j) => i + j)]
];
const z = [0, []];
const f = xxs => xxs.reduce(redex, z);
const input = [[0, 0], [0, 0, 0], [0]];
const [i, result] = f(input);
console.log(result);
}
// 3
// Create a curried foldl function
{
const foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
const redex = ([i, p]) => xs => [
i + xs.length,
[...p, xs.map((_, j) => i + j)]
];
const z = [0, []];
const f = foldl(redex)(z);
const input = [[0, 0], [0, 0, 0], [0]];
const [i, result] = f(input);
console.log(result);
}
// 4
// Use properties instead of a tuple
{
const foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
const redex = ({ i, p }) => xs => ({
i: i + xs.length,
p: [...p, xs.map((_, j) => i + j)]
});
const z = { i: 0, p: [] };
const f = foldl(redex)(z);
const input = [[0, 0], [0, 0, 0], [0]];
const { i, p } = f(input);
console.log(p);
}
// TODO: Add some exposition here about deferring selection of initial state
// 5
// Push the initial state out into a parameter
{
const foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
const redex = st => xs => i_ => {
const { i, p } = st(i_);
return {
i: i + xs.length,
p: [...p, xs.map((_, j) => i + j)]
};
};
const z = i => ({ i, p: [] });
const f = foldl(redex)(z);
const input = [[0, 0], [0, 0, 0], [0]];
const { i, p } = f(input)(0);
console.log(p);
}
// Now let's recognize a particular type of thing called a "state transition"
// :: type StateTransition i p = s -> { i: i, p: p }
// (less verbose alias)
// :: type St = StateTransition
// Notice that the `i => { i, p: [] }` we're passing as the seed for the fold
// above is a state transition; specifically a `StateTransition Int [a]`. It takes
// some integer and produces an array of arbitrary things and another integer
// In this round of refactoring we're not going to change anything, we're just
// going to go around adding type annotations, so we can clearly see where we've
// unwittingly introduced state transitions
// 6
// Annotate everything
{
// :: (b -> a -> b) -> b -> [a] -> b
const foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
// Try validating the type signatures below for yourself and make sure everything
// lines up
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = st => xs => i_ => {
const { i, p } = st(i_);
return {
i: i + xs.length,
p: [...p, xs.map((_, j) => i + j)]
};
};
// :: St Int [a]
const z = i => ({ i, p: [] });
// :: [[Int]] -> St Int [[Int]]
const f = foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// 7
// Factor out some of the array logic
{
// :: (b -> a -> b) -> b -> [a] -> b
const foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
// :: [a]
const empty = [];
// :: [a] -> a -> [a]
const snoc = xs => y => [...xs, y];
// :: Int -> [Int]
const range = n => [...Array(n).keys()];
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = st => xs => i_ => {
const { i, p } = st(i_);
return {
i: i + xs.length,
p: snoc(p)(range(xs.length).map(j => i + j))
};
};
// :: St Int [a]
const z = i => ({ i, p: empty });
// :: [[Int]] -> St Int [[Int]]
const f = foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// At this point you might be starting to grumble: "some refactoring this is; the snippet
// is now several times as long as when we started!". This is a fair criticism, and I will
// deal with this criticism by simply cheating and claiming that most of this snippet is now
// actually part of a library or module, and no longer part of my snippet
const Arr = {};
// :: (b -> a -> b) -> b -> [a] -> b
Arr.foldl = f => z => xs => xs.reduce((p, c) => f(p)(c), z);
// :: [a]
Arr.empty = [];
// :: [a] -> a -> [a]
Arr.snoc = xs => y => [...xs, y];
// :: Int -> [Int]
Arr.range = n => [...Array(n).keys()];
// I will play this dirty trick several more times in the course of this gist
// 8
// Let's just update our snippet to leverage our little sleight of hand
{
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = st => xs => i_ => {
const { i, p } = st(i_);
return {
i: i + xs.length,
p: Arr.snoc(p)(Arr.range(xs.length).map(j => i + j))
};
};
// :: St Int [a]
const z = i => ({ i, p: Arr.empty });
// :: [[Int]] -> St Int [[Int]]
const f = Arr.foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// 9
// Factor out some combinators for manipulating state transitions
{
// :: p -> St i p
const noop = p => i => ({ i, p });
// :: St i p -> (p -> St i q) -> St i q
const after = st => f => i_ => {
// Perform the first transition on the initial state
const { i, p } = st(i_);
// Use the resulting value to produce a new state transition
const st2 = f(p);
// Run the second state transition on the new state
return st2(i);
};
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = st => xs =>
after(st)(p => i => ({
i: i + xs.length,
p: Arr.snoc(p)(Arr.range(xs.length).map(j => i + j))
}));
// :: St Int [a]
const z = noop(Arr.empty);
// :: [[Int]] -> St Int [[Int]]
const f = Arr.foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// Cheat again
const State = {};
// :: p -> St i p
State.noop = p => i => ({ i, p });
// :: St i p -> (p -> St i q) -> St i q
State.after = st => f => i_ => {
// Perform the first transition on the initial state
const { i, p } = st(i_);
// Use the resulting value to produce a new state transition
const st2 = f(p);
// Run the second state transition on the new state
return st2(i);
};
// 10
// Use externalized State things
{
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = st => xs =>
State.after(st)(p => i => ({
i: i + xs.length,
p: Arr.snoc(p)(Arr.range(xs.length).map(j => i + j))
}));
// :: St Int [a]
const z = State.noop(Arr.empty);
// :: [[Int]] -> St Int [[Int]]
const f = Arr.foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// :: (a -> b -> c) -> St i a -> St i b -> St i c
State.combineResults = f => st1 => st2 =>
State.after(st1)(p1 => State.after(st2)(p2 => State.noop(f(p1)(p2))));
// 11
// Decouple the array result collection from the per item action
{
// :: [Int] -> St Int [Int]
const perItemSt = xs => i => ({
i: i + xs.length,
p: Arr.range(xs.length).map(j => i + j)
});
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = prevSt => xs =>
State.combineResults(Arr.snoc)(prevSt)(perItemSt(xs));
// :: St Int [a]
const z = State.noop(Arr.empty);
// :: [[Int]] -> St Int [[Int]]
const f = Arr.foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// TODO: Introduce the concept of applicatives, reveal noop and combineResults are pure and lift2
State.pure = State.noop;
State.lift2 = State.combineResults;
// 12
// Use pure and lift2
{
// :: [Int] -> St Int [Int]
const perItemSt = xs => i => ({
i: i + xs.length,
p: Arr.range(xs.length).map(j => i + j)
});
// :: St Int [Int] -> [Int] -> St Int [Int]
const redex = prevSt => xs => State.lift2(Arr.snoc)(prevSt)(perItemSt(xs));
// :: St Int [a]
const z = State.pure(Arr.empty);
// :: [[Int]] -> St Int [[Int]]
const f = Arr.foldl(redex)(z);
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// 13
// Inline redex and z
{
// :: [Int] -> St Int [Int]
const perItemSt = xs => i => ({
i: i + xs.length,
p: Arr.range(xs.length).map(j => i + j)
});
// :: [[Int]] -> St Int [[Int]]
// prettier-ignore
const f = Arr.foldl
(prevSt => xs => State.lift2(Arr.snoc)(prevSt)(perItemSt(xs)))
(State.pure(Arr.empty));
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
// TODO: Exposition, then factor out traverse
Arr.traverse = A => f =>
Arr.foldl(p => c => A.lift2(Arr.snoc)(p)(f(c)))(A.pure(Arr.empty));
// 14
// Use traverse
{
// :: [[Int]] -> St Int [[Int]]
const f = Arr.traverse(State)(xs => i => ({
i: i + xs.length,
p: Arr.range(xs.length).map(j => i + j)
}));
// :: [[Int]]
const input = [[0, 0], [0, 0, 0], [0]];
// :: { i: Int, p: [[Int]] }
const { i, p } = f(input)(0);
console.log(p);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment