Skip to content

Instantly share code, notes, and snippets.

@xgrommx
Forked from jethrolarson/_usage.js
Created April 25, 2016 01:10
Show Gist options
  • Save xgrommx/2bc1e3720351348d7d57d98d65106784 to your computer and use it in GitHub Desktop.
Save xgrommx/2bc1e3720351348d7d57d98d65106784 to your computer and use it in GitHub Desktop.
xface - Functional Interface library for typed common apis
//Some fantasy implementations
const Just = require('./just')
const Cant = require('./cant')
const List = require('./list')
//Spec file that defines what arguments are used for type identification
const fantasy = require('./fantasy')
//The magic.
const {chain, map, index} = require('../../src/xface')(fantasy, [Just, Cant])
//add implementations after the fact if you please. Returns new instance of xface.
.use([List])
//extend the spec after. Also returns new xface
.extendSpec({index: 1})
const mapInc = map(a => a + 1)
const log = console.log.bind(console)
log(mapInc(Just(1)))
//Just(2)
log(mapInc(Cant()))
//Cant()
log(mapInc([1,2]))
//[2,3]
//:: String -> Maybe String
const validatePasswordLen = v => v.length ? Just(v) : Cant()
const validatePasswordHasNumber = v => (/\d/).test(v) ? Just(v) : Cant()
log(chain(validatePasswordHasNumber)(validatePasswordLen('yoa1')))
//Just(yoa1)
log(index(1)([2,3,4]))
//half of an example maybe implementation
function _Cant(){
this['@@type'] = "Cant"
}
//pretty logging
_Cant.prototype.toString =
_Cant.prototype.inspect = () => "Cant()"
// There's only ever one instance.
const nothing = new _Cant()
var Cant = () => nothing
Cant.chain = f => Cant
Cant.map = f => Cant
Cant.ap = a => b => Cant
Cant.empty = Cant
Cant.reduce = f => b => Cant
Cant.extend = Cant
Cant.filter = f => Cant
Cant.of = Cant
Cant.equals = m => m === nothing
Cant.concat = m => m
module.exports = Cant
// Example xface spec file
// keys are the name of the interface function
// vals are the argument index that should be checked for type
module.exports = {
/* Standard Fantasy-land functions follow */
// Setoid
//:: S a -> S b -> Boolean
equals: 0,
// Semigroup
//:: S a -> S b -> S c
concat: 0,
// Monoid
// M a -> M _
empty: 0,
// Functor
//:: (a -> b) -> F a -> F b
map: 1,
// Apply
//:: F (a -> b) -> F a -> F b
ap: 1,
//Foldable
//:: (b -> a -> b) -> b -> [a] -> b
reduce: 2,
// Monad
// Satisfied by chain and applicative
// Applicative
//:: F a -> b -> F b
of: 0,
// Chain
// a.k.a flatmap or bind
//:: (a -> M b) -> M a -> M b
chain: 1,
//Extract
//:: M a -> a
extract: 0
}
//half of an example maybe implementation
/*
Heavily inspired by [ramda-fantasy](https://github.com/ramda/ramda-fantasy) and
[sanctuary](https://github.com/plaid/sanctuary)
This implementation satisfies the following [fantasy-land](https://github.com/fantasyland/fantasy-land) algebras:
* Setoid
* Semigroup
* Monoid
* Functor
* Apply
* Applicative
* Foldable
* Chain
* Monad
* Extend
*/
const {Cant} = require('./cant')
const _Just = function _Just(x){
this['@@type'] = "Just"
this.value = x
}
//Pretty logging
_Just.prototype.toString =
_Just.prototype.inspect = function(){
return `Just(${this.value.toString()})`
}
const Just = a => new _Just(a)
//:: (a -> Maybe b) -> Maybe a -> Maybe b
Just.chain = f => a => f(a.value)
//:: Maybe a => (a -> b) -> Maybe b
Just.map = f => a => Just(f(a.value))
//:: Maybe(a -> b) -> Maybe a -> Maybe b
Just.ap = m => map(m.value)
//:: Maybe => Maybe
Just.empty = Cant
//:: Maybe -> Maybe -> Boolean
Just.equals = a => b => a.value === b.value
//:: Maybe => Maybe -> Maybe
Just.filter = pred => a => pred(a.value) ? a : Cant()
//Value inside both maybes must have a concat method
//:: Maybe => Maybe -> Maybe
Just.concat = a => b =>
b['@@type'] === 'Just' ? Just(a.value.concat(b.value)) : a
//:: (b, a -> b) -> b -> Maybe a -> b
Just.reduce = f => b => a => f(b)(a.value)
Just.extend = f => a => Just(f(a))
Just.of = Just
module.exports = Just
//Example List implementation
//:: (a -> Boolean) -> [a] -> [a]
const filter = pred => list => list.filter(pred)
//:: Int -> [a] -> a
const index = i => list => list[i]
//:: [] -> [] -> []
const concat = a => b => a.concat(b)
//:: * -> Boolean
const isArray = (val) =>
(val != null &&
val.length >= 0 &&
Object.prototype.toString.call(val) === '[object Array]')
//Add fantasy land stuff to arrays
// [(a -> b)] => [a] -> [b]
const ap = a => vals =>
reduce( acc => fn =>
concat(acc)(map(fn)(vals))
)([])(a)
//:: a -> [a]
const of = v => [v]
//:: [] -> []
const empty = a => []
const map = f => a => a.map(f)
const reduce = f => b => a => a.reduce(f, b)
//:: [[]] -> []
const unnest = reduce(concat)([])
//AKA flatmap
//:: [a] => (a -> [b]) -> [b]
const chain = f => a => unnest(map(f)(a))
// Checks if two arrays contain the same values
//:: [] => [] -> Boolean
const equals = a => b => {
var len = a.length
if(len != b.length){
return false
}
for(var i = 0; i < len; i++){
if(a[i] !== b[i]){
return false
}
}
return true
}
module.exports = {map, filter, reduce, index, of, ap, chain, typeEvaluator: isArray, name: "List"}
//:: a -> String
const defaultEvaluator = a => {
//@@type is just a proposed convention
return a['@@type']
}
//:: String -> (a -> Boolean) -> String
const createEvaluator = (name, f) => x => f(x) ? name : ''
// Collects implementations of `is` for type matching
//:: {{typeEvaluator: (a -> Boolean)}} -> [(a -> String)]
const getTypeEvaluators = impls =>
impls.reduce(
((acc, impl) =>
(impl.typeEvaluator) ? acc.concat([createEvaluator(impl.name, impl.typeEvaluator)]) : acc)
, []
)
//:: [{}] -> {}
const getXfaces = impls =>
impls.reduce((acc, impl) => {
acc[impl.name] = impl
return acc
}, {})
// Get the name of an algebraic type's type.
//:: a -> String
const checkType = evaluators => a => {
// check the value against provided evaluator functions
for(var i = 0; i < evaluators.length; i++){
var result = evaluators[i](a)
if(result){
return result
}
}
return ''
}
//:: (a -> String), {} -> String, a -> (a -> b)
const createGetFun = (checkType, xfaces) =>
(name, a) => {
var type = checkType(a)
if(!type) throw 'Type is not defined'
var namespace = xfaces[type]
if(!namespace) throw `Type (${type}) is not registered`;
var method = namespace[name]
if(!method) throw `Function (${name}) is not implemented for (${type})`
return method
}
//dispatchers specify which argument should be used to ascertain the type
// e.g. `dispatch0('map')` checks type of 1st argument and then looks for a
// matching 'map' implementation
// used by head
//:: (a -> (a -> b)) -> b
const dispatch0 = getFun => name =>
a => getFun(name, a)(a)
// used by map and chain
//:: (b -> (a -> b -> c)) -> c
const dispatch1 = getFun => name =>
a => b => getFun(name, b)(a)(b)
// Reduce uses this
//:: (c -> (a -> b -> c -> d)) -> d
const dispatch2 = getFun => name =>
a => b => c => getFun(name, c)(a)(b)(c)
// Just in case
//:: (d -> (a -> b -> c -> d -> e)) -> e
const dispatch3 = getFun => name =>
a => b => c => d => getFun(name, d)(a)(b)(c)(d)
const dispatchers = [dispatch0, dispatch1, dispatch2, dispatch3];
class Xface {
constructor(spec, implementations){
this.implementations = implementations || []
this.spec = spec || {}
const xfaces = implementations ? getXfaces(implementations) : {}
const evaluators =
implementations
? [defaultEvaluator].concat(getTypeEvaluators(implementations))
: [defaultEvaluator]
const getFun = createGetFun(checkType(evaluators), xfaces)
const preparedDispatchers = dispatchers.map(f => f(getFun))
//Add spec functions to public api
if(spec){
for(var k in spec){
if(spec.hasOwnProperty(k)){
this[k] = preparedDispatchers[spec[k]](k)
}
}
}
}
//:: [{}] -> Xface
use(impls) {
return new Xface(this.spec, this.implementations.concat(impls))
}
// Extend fantasy api with generic functions. Returns new copy.
// Use `dispatch` functions to make extensions generic.
//:: {} -> {}
extendSpec(spec) {
return new Xface(Object.assign({}, this.spec, spec), this.implementations)
}
}
module.exports = (spec, implementations) => new Xface(spec, implementations);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment