mezzanine
Version:
Fantasy land union types with pattern matching
364 lines (314 loc) • 9.3 kB
JavaScript
//@flow
/**
* Union of types
*
* @module mezzanine/union
*/
import { zip, difference, isEmpty, values as getValues } from 'ramda'
import /*Type, */{ typeContainer } from '../type'
import isOrthogonal from '../ortho'
import { rename } from '../decorators'
import { typeMark } from '../config'
import { isMezzanine } from '../type/fixtures'
import { addProperties } from '../utils/props'
import { type TypeRecord } from '../type/type-container'
import { curry2 } from '../utils/fp'
import { append } from '../utils/list'
import { type FieldMap } from '../utils/props'
import { getInitialValue, applyStack } from '../virtual-stack'
/* eslint-disable */
//$FlowIssue
export interface UnionType extends Iterable<[string, *]> {
ಠ_ಠ: Symbol,
keys: string[],
isMono: boolean,
typeName: string,
value: TypeRecord<any>,
toJSON(): mixed,
is(val: mixed): boolean,
map(mapFunction: <T>(val: T) => T): UnionType,
case(cases: {[string]: (val: mixed) => mixed}): mixed,
chain<T>(chainFunction: (val: T) => UnionType): UnionType,
}
export interface UnionStatic extends Iterable<[string, *]> {
(data: *): UnionType,
$call(data: *): UnionType,
ಠ_ಠ: Symbol,
keys: string[],
isMono: boolean,
typeName: string,
toJSON(): Object,
is(val: mixed): boolean,
case(cases: {[string]: (val: mixed) => mixed}): (subtype: *) => mixed,
case(cases: {[string]: (val: mixed) => mixed}, subtype: *): mixed,
contramap<T, S>(prependFunction: (...vals: S[]) => T): (...data: S[]) => UnionStatic
}
const staticFantasy = addProperties({
stackUpdate: (Ctx: UnionStatic) =>
(newStack: Array<(val: mixed) => mixed>) => {
//$FlowIssue
const { typeName, desc, func } = Ctx
return Union([typeName])(desc, func, newStack)
},
contramap: (Ctx: UnionStatic) =>
function contramap<T, S>(prependFunction: (...vals: S[]) => T) {
//$FlowIssue
const newStack = append(prependFunction, Ctx.stack)
//$FlowIssue
const NewRecord = Ctx.stackUpdate(newStack)
return NewRecord
}
})
const instanceFantasy = addProperties({
map: (ctx: UnionType, Ctx: UnionStatic) =>
function map(mapFunction: <T>(val: T) => T) {
return Ctx(ctx.chain(mapFunction))
},
chain: (ctx: UnionType, Ctx: UnionStatic) =>
function chain(chainFunction: (val: mixed) => UnionType) {
return chainFunction(ctx.toJSON())
},
})
const instProps = addProperties({
case: (ctx: UnionType, Ctx: UnionStatic) =>
(cases: {[string]: (val: mixed) => mixed}) =>
Ctx.case(cases, ctx.value),
})
const subclassReference = addProperties({
keys : (_: any, child) => child.keys,
isMono: (_: any, child) => child.isMono,
toJSON(_: any, child) {
if (typeof child.toJSON === 'function')
return child.toJSON()
return _.toJSON()
},
})
function unrelevantCaseError(union: UnionStatic, diff: string[], data: *) {
console.error(union)
console.error(data)
throw new Error(`Unrelevant case types ${diff.toString()} on type ${union.typeName}`)
}
function caseSelector(
union: UnionStatic,
subtypesMap: *,
uniqMark: Symbol,
cases: {[key: string]: (val: *) => *},
subtype: *) {
const {
_ = defaultCase,
...realCases
} = cases
const diff = difference(Object.keys(realCases), union.keys)
if (!isEmpty(diff))
unrelevantCaseError(union, diff, subtype)
if (isMezzanine(subtype) && (subtype.ಠ_ಠ === uniqMark)) {
const currentCase = realCases[subtype.type]
// console.log(currentCase)
if (typeof currentCase === 'function')
return currentCase(subtype.toJSON(), union)
else return _(subtype)
}
for (const variant of Object.keys(realCases)) {
const childType = subtypesMap[variant]
const currentCase = realCases[variant]
if (childType.is(subtype)) {
const finalValue = childType.ಠ_ಠ === subtype.ಠ_ಠ
? subtype
: childType(subtype)
return currentCase(finalValue.toJSON(), union)
}
}
return _(subtype)
}
//$FlowIssue
const caseFactory = (union: UnionStatic, subtypesMap: *, uniqMark: Symbol) =>
(cases: {[key: string]: (val: *) => *}, subtype: *) =>
caseSelector(union, subtypesMap, uniqMark, cases, subtype)
/**
* Make type union
*
* @param {string[]} [typeName]
* @returns
*
* @example
* Union`User`({ Account: String, Guest: {} })
*/
function Union([typeName]: string[]) {
return function UnionFabric(
desc: {[name: string]: *},
funcBlob: FieldMap = {},
stack: Array<(val: mixed) => mixed> = []
) {
const canMatch = isOrthogonal(desc)
const uniqMark = Symbol(typeName)
const keys = Object.keys(desc)
const values = getValues(desc)
const subtypes = zip(keys, values)
const subtypesMap = {}
for (const [key, arg] of subtypes)
subtypesMap[key] = rename(key)(typeContainer(key, typeName, arg, {}))
const caseWith = caseFactory(UnionClass, subtypesMap, uniqMark)
function* iterator() {
for (const key of keys)
yield ([key, subtypesMap[key]])
}
const needTransform = stack.length !== 0
function is(obj: *) {
let val = obj
if (needTransform === true) {
const initial = getInitialValue(stack, val)
if (initial.succ === false) return false
val = initial.val
}
for (const [key, pattern] of iterator()) {
if (pattern.is) {
if (pattern.is(val)) return true
} else if (typeof pattern === 'function' && pattern(val))
return true
}
return false
}
const staticProps = addProperties({
funcs: {
value : funcBlob,
enumerable: false,
},
desc: {
value : desc,
enumerable: false,
},
name: {
value : typeName,
enumerable: false,
},
case: {
value : curry2(caseWith),
enumerable: false,
},
stack: {
value : stack,
enumerable: false,
},
})
const mainProps = addProperties({
ಠ_ಠ: {
value : uniqMark,
enumerable: false,
},
//$FlowIssue
[typeMark]: {
get : () => true,
enumerable: false,
},
//$ FlowIssue
[Symbol.iterator]: () => iterator,
is : {
value : () => is,
inject : true,
enumerable: true,
},
typeName: {
value : typeName,
enumerable: true,
},
canMatch: {
value : canMatch,
enumerable: true,
},
types: {
value : keys,
enumerable: true,
},
})
const userMeth = addProperties(funcBlob)
//$FlowIssue
function UnionClass(obj: *): UnionType {
//$FlowIssue
if (new.target !== UnionClass)
return new UnionClass(obj)
const arg = applyStack(stack, obj)
const data = arg && arg.value
? arg.value
: arg
let matched = false
for (const [key, pattern] of iterator()) {
if (pattern.is(data)) {
const fin = pattern(data)
Object.assign(this, fin)
if (fin.isMono)
this.value = fin.value
subclassReference(this, fin)
matched = true
break
}
}
if (matched === false)
unmatchError(UnionClass, data)
instProps(this, UnionClass)
mainProps(this, UnionClass)
instanceFantasy(this, UnionClass)
userMeth(this, UnionClass)
}
staticProps(UnionClass, UnionClass)
mainProps(UnionClass, UnionClass)
staticFantasy(UnionClass, UnionClass)
Object.assign(UnionClass, subtypesMap)
// console.log(UnionClass)
return UnionClass
}
}
//$FlowIssue
function unmatchError(union: UnionStatic, data: mixed) {
console.error(union)
console.error(data)
throw new TypeError('Unmatched pattern')
}
const defaultCase = (instance: *) => {
throw new Error(`Unmatched case on union ${instance.typeName}`)
}
export default Union
// const Boy = Type`Boy`({
// name: String,
// alive: Boolean
// }, {
// mutableKill: (ctx) => () => { ctx.alive = false }
// })
// mutable function
// const Fred = Boy({ name: 'Fred', alive: true })
// console.log(Fred)
// Fred.mutableKill()
// console.log(Fred)
// const Brothers = Type`Brothers`({
// elder : Boy,
// younger: Boy
// })
// const Childs = Union`Childs`({
// Single: Boy,
// Couple: Brothers,
// })
// const rawData = { name: 'Fred' }
// // Fred.equals(Boy({ name: 'Fred', alive: true })); /*?*/
// // Fred.equals(rawData) /*?*/
// //transforms
// // Fred.map(({ name }) => ({ name: name+'dy' }))/*?*/
// deep patterns
// const family1 = Childs({ name: 'John', alive: true })
// const family2 = Childs({
// elder : Fred,
// younger: { name: 'Bob', alive: true }
// })
// console.log(family2)
// family2 /*?*/
// const onlyOne = family2.map((data) =>( console.log(data), data, family2));
// onlyOne
// family1.type; /*?*/
// family2.type; /*?*/
// onlyOne.type; /*?*/
// [...family1].map(([key,value])=>key)/*?*/
// //pattern-switch
// const actions = {
// Single:(child) => child.name,
// Couple:({ elder, younger }) => [elder.name, younger.name]
// }
// console.log(family2.case(actions))
// console.log(family1.case(actions))