Skip to content

Instantly share code, notes, and snippets.

@jethrolarson
Last active April 25, 2016 01:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jethrolarson/5eaad6d7b22b231741c9 to your computer and use it in GitHub Desktop.
Save jethrolarson/5eaad6d7b22b231741c9 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);
@jethrolarson
Copy link
Author

Ironically xface is object oriented so that the returned interface doesn't have to be.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment