UNPKG

fpg

Version:

Tiny JavaScript Functional Programming Library

840 lines (603 loc) 19.6 kB
# FPG (Functional Programming) ## Motivation I needed something tiny to work on my projects. I wanted to avoid complex functional programming constructs and libraries. **Important:** Data comes last! ### Implemented API methods: `pipe()` `asyncPipe()` `map()` :: maps a function to a type inside Either, Array, Object, Task, Promise (.map is the js function to map over arrays) `mapLeft()` `chain()` `chainLeft()` `log()` `Either.Left()` `Either.Right()` `Either.of()` `Either.tryCatch()` `fold()` `Task.of()` `Task.map()` `Task.chain()` `Task.ap()` `Task.rejected()` `Task.fork()` ## Either.Left / Either.Right ```js const { Either, asyncPipe, pipe, map, fold, chain, mapLeft } = require("fpg") const data = [{name: "Dimitri", city: "Berlin"}, {name: "Vlad", city: "Stuttgart"}] const program = pipe([ map (x => x.map(e => ({name: e.name + " Tarasowski", city: e.city}))), Either.of, chain (x => (x || {}).city || Either.Left("No city")), mapLeft (y => y + "!!!"), map (x => x.map(i => ({name: i.name, city: i.city, message: "success"}))), fold (x => console.log("from left: " + JSON.stringify(x)), x => console.log("from right: " + JSON.stringify(x))) ]) program(data) // from left: "No city !!!" const program2 = asyncPipe([ Either.tryCatch (x => x.message.city.name), map (x => x.data), fold (x => console.log("from left: left active -> error"), x => console.log("from right: right not active -> error")) ]) program2(data) // from left: left active -> error ``` --- ## anyncPipe ```js const fetch = () => new Promise((res, rej) => res([{userId: 1, name: "Dimitri"},{ userId: 2, name: "Joel"}])) const save = data => new Promise((res, rej) => rej(new Error("Something went wrong with saving user data!"))) .then(x => Either.Right(x)) .catch(e => Either.Left(e.message)) const program3 = asyncPipe([ fetch, Either.of, map (x => x.map(u => ({...u, name: u.name + "!!!"}))), chain (save), fold (x => console.log("from left: " + JSON.stringify(x)), x => console.log("from right: " + JSON.stringify(x))) ]) program3() // from left: "Something went wrong with saving user data!" ``` --- ## Algebraic Typologies ```js // Source: https://github.com/DrBoolean // Functor Box.of(20).map(x => x / 2) //Box(10) // Monad Box.of(true).chain(x => Box.of(!x)) // Box(false) // Monoid Box.of('small').concat(Box.of('pox')) // Box('smallpox') // Applicative Box.of(x => x + 1).ap(2) // Box(3) // Traversable Box.of(3).traverse(Either.x, x => fromNullable(x)) // Right(Box(3)) // Natural transformation eitherToBox(fromNullable(null)) // Box(null) ``` --- ## Code that never fails! ```js const { Task, Either, Box, compose, map, fold, chain, ap, fork, trace } = require('ramda-x') const fs = require('fs') // we are using Task in order not to grab the state directly // by doing so we isolate the side-effects and make our app more safely // Use Task for side-effects: console.log, process.arg, http calls, db calls, read/write -> ASYNCHRONOUS CODE const argv = Task((reject, resolve) => resolve(process.argv)) const httpGet = Task((reject, resolve) => request(url, (err, res, body) => err ? reject(err) : resolve(body))) const readFile = enc => file => Task((reject, resolve) => fs.readFile(file, enc, (err, content) => err ? reject(err) : resolve(content))) const file = readFile('utf-8') file('config.json').fork(console.error, console.log) // Use Eiter.try for JSON.parse/JSON.stringify -> SYNCHRONOUS CODE const parse = Either.try(JSON.parse) const readFileSync = Either.try(fs.readFileSync) const result = readFileSync('config.json') result.fold(console.error, console.log) // Use Either.fromNullable when you are trying to get properties out of an object object.property const first = ({ name }) => Either.fromNullable(name) const name = compose( chain(first), Either.of ) const myName = name({ name: 'Dimitri' }) const herName = name({ name: 'Anastasia' }) myName.fold(console.error, console.log) herName.fold(console.error, console.log) ``` ## Either.fromNullable - Code that never fails! ```js const { Task, Either, prop, compose, trace, map, fold, chain } = require('ramda-x') const findColor = name => ({ red: '#ff4444', green: '#36599', blue: '#fff68f' })[name] const getColor = name => Either.fromNullable(findColor(name)) const sliceBy = str => str.slice(1) const upperCaseValue = str => str.toUpperCase() const reportError = err => 'no color' const showResult = fold(reportError, upperCaseValue) const sliceByTwo = map(sliceBy) const result = compose( showResult, sliceByTwo, getColor) result('green') // 36599 result('red') // FF4444 result('blue') // FFF68F result('yellow') // no color result('orange') // no color result('white') // no color ``` ## ap && chain (Reigth/Left.chain || Right/Left.ap) - Code that never fails ```js const { Task, Either, prop, compose, trace, map, fold, chain } = require('ramda-x') const fs = require('fs') // Important: If you'll try to get a non-existing property out of the object, //the app would not return undefined it will return 3000 as default value of the error function, defined in showResult() const getProperty = o => Either.of(p => p).ap(Either.fromNullable(o.port)) const readFile = Either.try(fs.readFileSync) const parseJSON = Either.try(JSON.parse) const isPortAvailable = chain(getProperty) const parse = chain(parseJSON) const showResult = fold( err => 3000, c => c) const result = compose( showResult, isPortAvailable, parse, readFile ) result('config.json') // 8888 result('confffig.json') // 3000 ``` ## Currying with Types (Boxes & Either) ```js const { Box } = require('ramda-x') const add = x => y => x + y const res = Box(add).ap(Box(20)).ap(Box(20)).fold(x => x) console.log( res // 40 ) //---------------------------------- // ---------------------------------- const { Task, Either, prop, compose, trace, map, fold, chain, ap } = require('ramda-x') const $ = selector => Either.of({ selector, height: 10 }) const getScreenSize = screen => head => foot => screen - (head.height + foot.height) const result = compose( fold(err => 'error', x => x), ap($('hooter')), ap($('header')), Either.of, getScreenSize ) console.log( result(800), // 780 result(1500) // 1480 ) ``` ## ap - safe and concurent IO operations - Code that never fails ```js const { Task, Either, prop, compose, trace, map, fold, chain, ap } = require('ramda-x') const request = require('request') const url1 = 'https://jsonplaceholder.typicode.com/posts/4' const url2 = 'https://jsonplaceholder.typicode.com/posts/2' const e = e => 'error' const identity = x => x const getTitle = o => Either.fromNullable(o.title).fold(e, identity) const getId = o => Either.fromNullable(o.id).fold(e, identity) const reportHeader = p1 => p2 => `Report: ${p1.fold(e, body => getTitle(body))} compared to ${p2.fold(e, body => getTitle(body))}` const reportId = p1 => p2 => `Report: ${p1.fold(e, body => getId(body))} compared to ${p2.fold(e, body => getId(body))}` const parse = Either.try(JSON.parse) const httpGet = url => Task((reject, resolve) => request(url, (err, response, body) => err ? reject(err) : resolve(parse(body))) ) const res = compose( ap(httpGet(url2)), ap(httpGet(url1)), Task.of ) res(reportHeader).fork(err => 'error', data => console.log(data)) // Report: eum et est occaecati compared to qui est esse res(reportId).fork(err => 'error', data => console.log(data)) // Report: 4 compared to 2 ``` ## ap Redux - concurrent IO operations - Code that never fails ```js const { Task, Either, prop, compose, trace, map, fold, chain, ap } = require('ramda-x') const request = require('request') const update = msg => model => msg.type === 'UPDATE' ? { ...model, report: msg.payload } : msg.type === 'ERROR' ? { ...model, error: msg.error } : model const dispatch = msg => { let model = {} model = update(msg)(model) reportHeader(model) console.log('UPDATED MODEL', model) } const updateModelMsg = payload => ({ type: 'UPDATE', payload }) const updateModelErrorMsg = error => ({ type: 'ERROR', error }) const url1 = 'https://jsonplaceholder.typicode.com/posts/4' const url2 = 'https://jsonplaceholder.typicode.com/posts/2' const e = () => dispatch(updateModelErrorMsg('value not found')) const identity = x => x const getTitle = o => Either.fromNullable(o.title) const getId = o => Either.fromNullable(o.id) const reportHeader = p1 => p2 => `Report: ${p1.chain(getTitle).fold(e, identity)} compared to ${p2.chain(getTitle).fold(e, identity)}` const reportId = p1 => p2 => `Report: ${p1.chain(getId).fold(e, identity)} compared to ${p2.chain(getId).fold(e, identity)}` const parse = Either.try(JSON.parse) const httpGet = url => Task((reject, resolve) => request(url, (err, response, body) => err ? reject(err) : resolve(parse(body))) ) const res = compose( ap(httpGet(url2)), ap(httpGet(url1)), Task.of ) res(reportHeader).fork(err => dispatch(updateModelErrorMsg(err)), data => dispatch(updateModelMsg(data))) res(reportId).fork(err => 'error', data => dispatch(updateModelMsg(data))) // UPDATED MODEL { report: 'Report: 4 compared to 2' } // UPDATED MODEL { report: 'Report: eum et est occaecati compared to qui est esse' } ``` ## safe I/O Operations with Parsing - Code that never fails ```js const { Task, Either, prop, compose, trace, map, fold, chain, ap, fork } = require('ramda-x') const fs = require('fs') const readFile = enc => file => Task((reject, resolve) => fs.readFile(file, enc, (err, content) => err ? reject(err) : resolve(content) ) ) const writeFile = file => content => Task((reject, resolve) => fs.writeFile(file, content, (err, success) => err ? reject(err) : resolve('success') ) ) const writeToConfigTwo = writeFile('config2.json') const parse = Either.try(JSON.parse) const stringify = Either.try(JSON.stringify)/:/ const getProperty = b => Task.of(c => Either.fromNullable(c.port)).ap(b) const eitherToTask = e => e.fold(Task.rejected, Task.of) const error = e => console.log('from error:', e) const success = c => console.log('from success', c) const transformation = compose( fork(error)(success), chain(writeToConfigTwo), // Task(Task(value)) -> Task(value) chain(eitherToTask), // Task(Right(value)) -> Task(Task(value)) -> Task(value) map(stringify), // returns: Task(Right(value)) chain(eitherToTask), // Task(Right(value)) -> Task(Task(value)) -> Task(value) getProperty, // returns: Task(Right(value)) chain(eitherToTask), // Task(Right(value)) -> Task(Task(value)) -> Task(value) map(parse), // returns: Task(Right(value)) readFile('utf-8') // returns: Task(value) ) transformation('configs.json') ``` ## Traversable with Lists ```js const { Task } = require('ramda-x') const fs = require('fs') const readFile = file => Task((reject, resolve) => fs.readFile(file, 'utf-8', (err, content) => err ? reject(err) : resolve(content))) const List = xs => ({ concat: x => List(xs.concat(x)), map: fn => List(xs.map(fn)), reduce: (f, i) => List(xs.reduce(f, i)), fold: f => f(xs), traverse(of, fn) { return xs.reduce( (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f), of(List([])) ) } }) const files = List(['config.json', 'config2.json']) files.traverse(Task.of, fn => readFile(fn)).fork(console.error, x => x.map(x => x + '!!!').fold(x => x)) ``` ### Example ```js const { map, prop, compose, trace } = require('ramda-x') const users = [ { name: 'Dimitri', isAdmin: true }, { name: 'John', isAdmin: true }, { name: 'Mike', isAdmin: false } ] const names = map(prop('name'), users) console.log(names) // [ 'Dimitri', 'John', 'Mike' ] const message = { Records: [ { name: 'Dimitri', isAdmin: true }, { name: 'John', isAdmin: true }, { name: 'Mike', isAdmin: false } ] } const extractNamesFromMessage = compose( map(prop('name')), trace('AFTER PLUCK'), prop('Records') ) console.log(extractNamesFromMessage(message)) // AFTER PLUCK: [ // { // "name": "Dimitri", // "isAdmin": true // }, // { // "name": "John", // "isAdmin": true // }, // { // "name": "Mike", // "isAdmin": false // } // ] //[ 'Dimitri', 'John', 'Mike' ] const filterNamesFromMessage = compose( filter(propEq('name', 'Dimitri')), prop('Records') ) console.log(filterNamesFromMessage(message)) // [ { name: 'Dimitri', isAdmin: true } ] ``` ## Task - Lazy Evaluation / Isolation of Side Effects ```js const {Task} = require('ramda-x') // pure function const readFile = (filename, enc) => Task((reject, resolve) => fs.readFile(filename, enc, (err, content) => err ? reject(err) : resolve(content)) ) // pure function const writeFile = (filename, contents) => Task((reject, resolve) => fs.writeFile(filename, contents, (err, success) => err ? reject(err) : resolve(success)) ) // pure function const app = readFile('config.json', 'utf-8') .map(content => content.replace(/8/g, '9')) .chain(contents => writeFile('config2.json', contents)) // impure function with side effects app.fork(e => console.log('error', e), succes => console.log('success')) ``` ## Lifting a value into a type ```js const f = x => x.concat('!!!') const res = Task.of('hello').map(f) res.fork(e => 'error', d => console.log(d)) // hello!!! Either.of('hello').map(f).fold(_ => _, d => console.log(d)) // hello!!! ``` ## Try/Catch Examples ```js const {Either} = require('ramda-x') const fs = require('fs') const tryCatch = f => { try { return Either.Right(f()) } catch (e) { return Either.Left(e) } } const getPort = () => tryCatch(() => fs.readFileSync('config.json')) .chain(c => tryCatch(() => JSON.parse(c))) .fold(e => 3000, c => c.port) const res = getPort() ``` ## Try with Either.try(f) ```js const parse = Either.try(JSON.parse) const eitherToTask = e => e.fold(Task.rejected, Task.of) const findPost = id => httpGet(url(id)) .map(parse) .chain(eitherToTask) const main = ([id]) => Task.of(title => [title]).ap(findPost(id)) id.chain(main).fork(console.error, x => console.log('success', x)) ``` ## Either - Instead of If/Else + Composition ```js import {Either} = require('ramda-x') const fromNullable = x => x !== null ? Either.Right(x) : Either.Left(x) // You can also import Either.fromNullable() and use it instead of fromNullable() const findColor = name => fromNullable({ red: '#ff4444', blue: '#3b5998', yellow: '#fffG8F' }[name]) const result = findColor('yellow').map(c => c.slice(1)).fold(err => 'nothing found', c => c.toUpperCase()) ``` ## Either - Instead of If/Else + Composition ```js const { Task, Either, prop, compose, trace, map, fold, chain } = require('ramda-x') const { fromNullable, Right, Left } = Either const dispatch = x => console.log('action was dispatched', x) const getItem = o => prop('item', o) const someAction2 = dispatch => data => compose( fold(e => 'comes from the err function', x => x), map(item => item), map(item => item + 2), chain(item => fromNullable(prop('item3', item))), fromNullable )(data) const toUpperCase = str => str.toUpperCase() const someAction3 = dispatch => data => compose( toUpperCase, getItem )(data) const prepeareAction = someAction2(dispatch) const prepareNewAction = someAction3(dispatch) prepeareAction(null) // comes from the err function -> the application runs without exiting prepareNewAction(null) // TypeError: Cannot read property 'item' of null ``` ## deepFreeze Examples - Immutable data ```js const expect = require('expect') const deepFreeze = require('./node_modules/deep-freeze') const addCounter = list => [...list, 0] const removeCounter = list => index => [ ...list.slice(0, index), ...list.slice(index + 1) ] const incrementCounter = list => index => [ ...list.slice(0, index), list[index] + 1, ...list.slice(index + 1) ] const testAddCounter = before => after => expect( addCounter(deepFreeze(before)) // deepFreeze makes the value immutable ).toEqual(after) testAddCounter([])([0]) const testRemoveCounter = before => after => expect( removeCounter(deepFreeze(before))(1) ).toEqual(after) testRemoveCounter([0, 10, 20])([0, 20]) const testIncrementCounter = before => after => expect( incrementCounter(deepFreeze(before))(1) // deepFreeze makes the value immutable ).toEqual(after) testIncrementCounter([0, 10, 20])([0, 11, 20]) ``` ## Some other examples ```js const fromNullable = x => x !== null ? Either.Right(x) : Either.Left(x) const tryCatch = f => { try { return Either.Right(f()) } catch (e) { return Either.Left(e) } } // imperative code const openSite = () => { if (current_user) { return renderPage(current_user) } else { return showLogin() } } // declarative code const openSite = () => { fromNullable(current_user) .fold(showLogin, renderPage) } // imperative code const getPrefs = user => { if (user.premium) { return loadPrefs(user.preferences) } else { return defaultPrefs } } // declarative code const getPrefs = user => (user.premium ? Right(user) : Left('not premium')) .map(u => u.preferences) .fold(() => defaultPrefs, prefs => loadPrefs(prefs)) // imperative code const streetName = user => { const address = user.address if (address) { const street = address.street if (street) { return street.name } } return 'no street!' } // declarative code const streetName = user => fromNullable(user.address) .chain(a => fromNullable(a.street)) .map(s => s.name) .fold(e => 'no street', n => n) // imperative code const concatUniq = (x, ys) => { const found = ys.filter(y => y === x)[0] return found ? ys : ys.concat(x) } // declarative code const concatUniq = (x, ys) => fromNullable(ys.filter(y => y === x)[0]) .fold(() => ys.concat(x), y => ys) // imperative code const wrapExamples = example => { if (example.previewPath) { try { example.preview = fs.readFileSync(example.previewPath) } catch (e) { } } return example } const readFile = x => tryCatch(() => fs.readFileSync(x)) // declarative code const wrapExamples = example => { fromNullable(example.previewPath) .chain(readFile) .fold(() => example, ex => Object.assign({ preview: p }, ex)) } // imperative code const parseDbUrl = cfg => { try { const c = JSON.parse(cfg) if(c.url) { return c.url.match(/*....*/) } } catch(e) { return null } } // declarative code const parseDbUrl = cfg => { tryCatch(() => JSON.parse(cfg)) .chain(c => fromNullable(c.url)) .fold(e => null, u => u.match(/*...*/)) } ```