Skip to content

Instantly share code, notes, and snippets.

@md2perpe
Forked from thoferon/SideEffectsAndPureCode
Created January 16, 2020 17:36
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 md2perpe/97012fb85f5af3a86143c0f41bb5109e to your computer and use it in GitHub Desktop.
Save md2perpe/97012fb85f5af3a86143c0f41bb5109e to your computer and use it in GitHub Desktop.
import readline from "readline-sync"
// Side effects
class EffectfulImplementation {
getUserInput () {
return readline.question("> ")
}
listTodoItems () {
// Dummy implementation.
return [
{ description: "Laundry", done: false },
{ description: "Groceries", done: true }
]
}
printTodoItems (items) {
for (const item of items) {
console.log(item)
}
}
}
// Logic (pure code)
function parseCommand(input) {
// Dummy implementation.
return {
action: "list",
}
}
function cli(impl) {
const userInput = impl.getUserInput()
const command = parseCommand(userInput)
switch (command.action) {
case "list":
const items = impl.listTodoItems()
impl.printTodoItems(items)
// ...
}
}
// Glue code
cli(new EffectfulImplementation());
import parser from "./nonSequentialActions.js"
function generateDocumentation (parser) {
switch (parser.action) {
case "pure":
return null
case "stringField":
return { [parser.key]: "string" }
case "numberField":
return { [parser.key]: "number" }
case "objectField":
return { [parser.key]: generateDocumentation(parser.parser) }
case "apply":
const doc1 = generateDocumentation(parser.parser1)
const doc2 = generateDocumentation(parser.parser2)
if (doc1 === null) {
return doc2
} else if (doc2 === null) {
return doc1
} else {
return {...doc1, ...doc2}
}
}
}
console.log(generateDocumentation(parser))
function numberField (key) {
return {
action: "numberField",
key,
}
}
function stringField (key) {
return {
action: "stringField",
key,
}
}
function objectField (key, parser) {
return {
action: "objectField",
key,
parser,
}
}
function pure (value) {
return {
action: "pure",
value,
}
}
// Return a new parser that executes both parsers and apply the result of the
// second one to the function returned by the first one.
function apply (parser1, parser2) {
return {
action: "apply",
parser1,
parser2,
}
}
// Map a function to the result of a parser.
function mapParser (parser, f) {
switch (parser.action) {
case "pure":
return pure(f(parser.value))
default:
return apply(pure(f), parser)
}
}
function interpret (parser, json) {
switch (parser.action) {
case "pure":
return parser.value
case "numberField":
if (typeof(json[parser.key]) === "number") {
return json[parser.key]
} else {
throw "ParsingError"
}
case "stringField":
if (typeof(json[parser.key]) === "string") {
return json[parser.key]
} else {
throw "ParsingError"
}
case "objectField":
if (typeof(json[parser.key]) === "object") {
return interpret(parser.parser, json[parser.key])
} else {
throw "ParsingError"
}
case "apply":
const f = interpret(parser.parser1, json)
const x = interpret(parser.parser2, json)
return f(x)
}
}
const someJson = {
database: {
port: 5432,
username: "myapp",
}
}
const parser =
objectField(
"database",
apply(
apply(
pure(function (port) {
return function (username) {
return `postgresql://${username}:secret@localhost:${port}/myapp`
}
}),
numberField("port")
),
stringField("username")
)
)
// To execute the parser, we would call:
// const result = interpret(parser, someJson)
// For later:
export default parser
import readline from "readline-sync"
function read () {
return {
action: "read",
next: pure,
}
}
function print (string) {
return {
action: "print",
string,
next: pure,
}
}
// A pure program that simply returns a value.
function pure (value) {
return {
action: "pure",
value,
}
}
function interpret (program) {
switch (program.action) {
case "read":
const input = readline.question("")
return interpret(program.next(input))
case "print":
console.log(program.string)
return interpret(program.next(null))
case "pure":
return program.value
}
}
// Bind two programs together. Return a new program which is equivalent to
// running the first program and feed its return value to the second one.
function bind (program, next) {
switch (program.action) {
case "pure":
return next(program.value)
default:
return {
...program,
next: (val) => bind(program.next(val), next)
}
}
}
const number = 42;
const game =
bind(print("Enter a guess"), _ => {
return bind(read(), (input) => {
const guess = Number(input)
if (guess === number) {
return print("Congratulations! You found it.")
} else if (guess < number) {
return bind(print("Higher"), _ => game)
} else if (guess > number) {
return bind(print("Lower"), _ => game)
} else {
return print("Just numbers, please.")
}
})
})
// To execute the program, we would just have to call:
// interpret(game)
// For later
export default game
import _ from "underscore"
import game from "./sequentialActions.js"
function testInterpreter (program, inputs, accOutputs) {
switch (program.action) {
case "read":
const input = inputs[0]
const rest = inputs.slice(1)
return testInterpreter(program.next(input), rest, accOutputs)
case "print":
return testInterpreter(
program.next(),
inputs,
accOutputs.concat(program.string)
)
case "pure":
return accOutputs
}
}
const outputs = testInterpreter(game, ["3", "90", "32", "42"], [])
const expectedOutputs = [
"Enter a guess",
"Higher",
"Enter a guess",
"Lower",
"Enter a guess",
"Higher",
"Enter a guess",
"Congratulations! You found it."
]
if (_.isEqual(outputs, expectedOutputs)) {
console.log("All is well")
} else {
console.log("Test failure", outputs, expectedOutputs)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment