From 2f94dbc09cdc4e0dab79f754084343f5adbb57fe Mon Sep 17 00:00:00 2001 From: David Chambers Date: Sun, 5 Jun 2016 19:32:09 -0700 Subject: [PATCH] improve performance of _determineActualTypes The current algorithm is inefficient for unary and binary types. Given an array containing 1000 elements, for example, we first determine the types of each element, resulting in an array of 1000 arrays of types. We then find the intersection of these "sets". This commit introduces a different approach: for each element, we refine the set of types of which all previous elements are members rather than filtering the whole environment each time. This is significantly more efficient for large arrays. --- index.js | 144 +++++++++++++------------------------------------- test/index.js | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 106 deletions(-) diff --git a/index.js b/index.js index d55d96d..7eef60a 100644 --- a/index.js +++ b/index.js @@ -762,85 +762,15 @@ } }; - // unexpectedType :: Any -> TypeError - var unexpectedType = /* istanbul ignore next */ function(x) { - return new TypeError( - 'Unexpected type ' + - LEFT_SINGLE_QUOTATION_MARK + x + RIGHT_SINGLE_QUOTATION_MARK - ); - }; - - // equalTypes :: (Type, Type, Boolean) -> Boolean - var equalTypes = function equalTypes(t1, t2, loose) { - if (t1.type === 'INCONSISTENT' || t2.type === 'INCONSISTENT') return loose; - if (t1.type === 'UNKNOWN' || t2.type === 'UNKNOWN') return true; - switch (t1.type) { - case 'NULLARY': - return t1.type === t2.type && t1.name === t2.name; - case 'UNARY': - return t1.type === t2.type && t1.name === t2.name && - equalTypes(t1.$1, t2.$1, loose); - case 'BINARY': - return t1.type === t2.type && t1.name === t2.name && - equalTypes(t1.$1, t2.$1, loose) && - equalTypes(t1.$2, t2.$2, loose); - case 'ENUM': - return t1.type === t2.type && show(t1) === show(t2); - case 'RECORD': - return t1.type === t2.type && show(t1) === show(t2); - /* istanbul ignore next */ - default: - throw unexpectedType(t1.type); - } - }; - - // chooseType :: (Type, Type) -> Type - var chooseType = function(t1, t2) { - return t1.type === 'UNKNOWN' ? t2 : t1; - }; - - // mergeTypes :: (Type, Type) -> Type - // - // Either String ??? `mergeTypes` Either ??? Number = Either String Number - var mergeTypes = function(t1, t2) { - return ( - t1.type === 'UNARY' ? - UnaryType.from(t1)(chooseType(t1.$1, t2.$1)) : - t1.type === 'BINARY' ? - BinaryType.from(t1)(chooseType(t1.$1, t2.$1), - chooseType(t1.$2, t2.$2)) : - // else - chooseType(t1, t2) - ); - }; - - // commonTypes :: ([[Type]], Boolean) -> [Type] - // - // [[String, RegexFlags], [String, RegexFlags]] ---> [String, RegexFlags] - // - // [[Boolean], [Boolean], [Boolean], [Number]] ---> [] - // - // [[Array ???], [Array String], [Array ???]] ---> [Array String] - // - // [[Either String ???], [Either ??? Number]] ---> [Either String Number] - var commonTypes = function(typeses, loose) { - return reduce(typeses[0], [], function(types, t) { - var st = reduce(typeses, {ok: true, type: t}, function(st, types) { - var st$ = reduce(types, {ok: false, type: st.type}, function(st, t) { - var equal = equalTypes(st.type, t, loose); - return {ok: equal || st.ok, - type: equal ? mergeTypes(st.type, t) : st.type}; - }); - return {type: st$.type, ok: st.ok && st$.ok}; - }); - return st.ok ? types.concat([st.type]) : types; - }); - }; - - // _determineActualTypes :: (Boolean, [Type], [Object], [Any]) -> [Type] - var _determineActualTypes = function recur(loose, env, seen, values) { - // typeses :: [[Type]] - var typeses = map(values, function(value) { + // _determineActualTypes :: ... -> [Type] + var _determineActualTypes = function recur( + loose, // :: Boolean + env, // :: [Type] + types, // :: [Type] + seen, // :: [Object] + values // :: [Any] + ) { + var refine = function(types, value) { var seen$; if (typeof value === 'object' && value != null || typeof value === 'function') { @@ -851,24 +781,30 @@ } else { seen$ = seen; } - return chain(env, function(t) { + return chain(types, function(t) { return ( t.name === 'sanctuary-def/Nullable' || !t._test(value) ? [] : t.type === 'UNARY' ? - map(recur(loose, env, seen$, t._1(value)), UnaryType.from(t)) : + map(recur(loose, env, env, seen$, t._1(value)), + UnaryType.from(t)) : t.type === 'BINARY' ? BinaryType.xprod(t, - recur(loose, env, seen$, t._1(value)), - recur(loose, env, seen$, t._2(value))) : + t.$1.type === 'UNKNOWN' ? + recur(loose, env, env, seen$, t._1(value)) : + [t.$1], + t.$2.type === 'UNKNOWN' ? + recur(loose, env, env, seen$, t._2(value)) : + [t.$2]) : // else [t] ); }); - }); + }; + return isEmpty(values) ? [Unknown] : - or(commonTypes(typeses, loose), [Inconsistent]); + or(reduce(values, types, refine), loose ? [Inconsistent] : []); }; // rejectInconsistent :: [Type] -> [Type] @@ -878,20 +814,22 @@ }); }; - // determineActualTypesStrict :: ([Type], [Any]) -> [Type] - var determineActualTypesStrict = function(env, values) { - return rejectInconsistent(_determineActualTypes(false, env, [], values)); + // determineActualTypesStrict :: ([Type], [Type], [Any]) -> [Type] + var determineActualTypesStrict = function(env, types, values) { + var types$ = _determineActualTypes(false, env, types, [], values); + return rejectInconsistent(types$); }; - // determineActualTypesLoose :: ([Type], [Any]) -> [Type] - var determineActualTypesLoose = function(env, values) { - return rejectInconsistent(_determineActualTypes(true, env, [], values)); + // determineActualTypesLoose :: ([Type], [Type], [Any]) -> [Type] + var determineActualTypesLoose = function(env, types, values) { + var types$ = _determineActualTypes(true, env, types, [], values); + return rejectInconsistent(types$); }; // valuesToPairs :: ([Type], [Any]) -> [Pair Any [Type]] var valuesToPairs = function(env, values) { return map(values, function(x) { - return [x, determineActualTypesLoose(env, [x])]; + return [x, determineActualTypesLoose(env, env, [x])]; }); }; @@ -942,8 +880,11 @@ } } if (has(typeVarName, typeVarMap)) { - okTypes = filterTypesByValues(typeVarMap[typeVarName].types, - values); + okTypes = _determineActualTypes(false, + env, + typeVarMap[typeVarName].types, + [], + values); if (isEmpty(okTypes)) { return Left(typeVarConstraintViolation2( name, @@ -958,7 +899,7 @@ )); } } else { - okTypes = determineActualTypesStrict(env, values); + okTypes = determineActualTypesStrict(env, env, values); if (isEmpty(okTypes) && !isEmpty(values)) { return Left(typeVarConstraintViolation( name, @@ -1032,7 +973,7 @@ default: return Right({typeVarMap: typeVarMap, - types: determineActualTypesStrict(env, values)}); + types: determineActualTypesStrict(env, env, values)}); } }; }; @@ -1064,21 +1005,12 @@ }; // test :: ([Type], Type, Any) -> Boolean - var test = $.test = function(_env, t, x) { + $.test = function(_env, t, x) { var env = applyParameterizedTypes(_env); var f = _satisfactoryTypes(env, 'name', {}, [t], 0); return f({}, t, [x], [], []).isRight; }; - // filterTypesByValues :: ([Type], [Any]) -> [Type] - var filterTypesByValues = function(env, values) { - return filter(env, function(t) { - return all(values, function(x) { - return test(env, t, x); - }); - }); - }; - // invalidArgumentsLength :: (String, Integer, Integer) -> Error var invalidArgumentsLength = function(name, expectedLength, actualLength) { return new TypeError( diff --git a/test/index.js b/test/index.js index dfcc4e4..3266a36 100644 --- a/test/index.js +++ b/test/index.js @@ -828,6 +828,33 @@ describe('def', function() { eq(length(vm.runInNewContext('["foo", "bar", "baz"]')), 3); }); + it('accommodates circular references', function() { + // id :: a -> a + var id = def('id', {}, [a, a], R.identity); + + var x = {name: 'x'}; + var y = {name: 'y'}; + x.y = y; + y.x = x; + + eq(id(x), x); + + var z = []; + z.push(z); + + throws(function() { id(z); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'id :: a -> a\n' + + ' ^\n' + + ' 1\n' + + '\n' + + '1) [] :: Array ???\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + }); + it('supports custom types', function() { // AnonJust :: a -> AnonMaybe a var AnonJust = function(x) { @@ -1711,6 +1738,65 @@ describe('def', function() { '2) [1, 2, 3] :: Array Number\n' + '\n' + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + + // f :: a -> a -> a -> a + var f = def('f', {}, [a, a, a, a], function(x, y, z) { return x; }); + + throws(function() { f(Left('abc'), Left(/XXX/)); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'f :: a -> a -> a -> a\n' + + ' ^ ^\n' + + ' 1 2\n' + + '\n' + + '1) Left("abc") :: Either String ???\n' + + '\n' + + '2) Left(/XXX/) :: Either RegExp ???\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + + throws(function() { f(Right(123), Right(/XXX/)); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'f :: a -> a -> a -> a\n' + + ' ^ ^\n' + + ' 1 2\n' + + '\n' + + '1) Right(123) :: Either ??? Number\n' + + '\n' + + '2) Right(/XXX/) :: Either ??? RegExp\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + +// throws(function() { f(Left('abc'), Right(123), Left(/XXX/)); }, +// errorEq(TypeError, +// 'Type-variable constraint violation\n' + +// '\n' + +// 'f :: a -> a -> a -> a\n' + +// ' ^ ^\n' + +// ' 1 2\n' + +// '\n' + +// '1) Left("abc") :: Either String ???\n' + +// '\n' + +// '2) Left(/XXX/) :: Either RegExp ???\n' + +// '\n' + +// 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + + throws(function() { f(Left('abc'), Right(123), Right(/XXX/)); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'f :: a -> a -> a -> a\n' + + ' ^ ^\n' + + ' 1 2\n' + + '\n' + + '1) Right(123) :: Either ??? Number\n' + + '\n' + + '2) Right(/XXX/) :: Either ??? RegExp\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); }); it('supports arbitrary nesting of types', function() { @@ -1835,6 +1921,19 @@ describe('def', function() { '\n' + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + throws(function() { concat([[1, 2, 3], [Right(42), Left('XXX')]]); }, + errorEq(TypeError, + 'Type-variable constraint violation\n' + + '\n' + + 'concat :: Array a -> Array a -> Array a\n' + + ' ^\n' + + ' 1\n' + + '\n' + + '1) [1, 2, 3] :: Array Number\n' + + ' [Right(42), Left("XXX")] :: Array (Either String Number)\n' + + '\n' + + 'Since there is no type of which all the above values are members, the type-variable constraint has been violated.\n')); + // concatNested :: [[a]] -> [[a]] -> [[a]] var concatNested = def('concatNested',