Last active
April 25, 2016 01:10
-
-
Save jethrolarson/5eaad6d7b22b231741c9 to your computer and use it in GitHub Desktop.
xface - Functional Interface library for typed common apis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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])) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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"} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//:: 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
Ironically xface is object oriented so that the returned interface doesn't have to be.