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..6ac6d04 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) Right(123) :: Either ??? Number\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',