Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

define $.Function and $.UnaryTypeVariable #79

Merged
merged 1 commit into from
Jul 31, 2016

Conversation

davidchambers
Copy link
Member

TL;DR we can now do this:

const a = $.TypeVariable('a');
const b = $.TypeVariable('b');
const f = $.UnaryTypeVariable('f');

//    Functor :: TypeClass
const Functor = ...;

//    map :: Functor f => (a -> b) -> f a -> f b
const map =
def('map',
    {f: [Functor]},
    [$.Function([a, b]), f(a), f(b)],
    (fn, functor) => functor.map(fn));

This pull request sits atop #78 and should not be merged until #78 has been merged.

sanctuary-js/sanctuary#232 is heavily dependent on this pull request.

This pull request adds support for what I believe to be the two missing pieces of the puzzle:

  • higher-order functions; and
  • parameterized type variables.

A wonderful consequence of this change is that type signatures in error messages will now match the type signatures which appear in documentation – no more map :: Function -> a -> b!

$.Function :: Array Type -> Type

The type formerly known as $.Function is now known as $.AnyFunction. $.Function is now a constructor for function types.

Examples:

  • $.Function([$.Date, $.String]) represents the Date -> String type; and
  • $.Function([a, b, a]) represents the (a, b) -> a type.
//    formatDates :: (Date -> String) -> Array Date -> String
const formatDates =
def('formatDates',
    {},
    [$.Function([$.Date, $.String]), $.Array($.Date), $.String],
    (f, dates) => implementation);

What will happen when one evaluates formatDates(d => d.toISOString(), [new Date(0)])?

  1. A check will be performed to confirm that d => d.toISOString() is in fact a function (of some sort).
  2. A check will be performed to confirm that [new Date(0)] is in fact an array.
  3. A check will be performed to confirm that new Date(0) is in fact a Date object.
  4. f will be decorated along these lines:
function(x) {
  if (arguments.length !== 1) throw new TypeError('...');
  if (!$.Date._test(x)) throw new TypeError('...');
  var output = f(x);
  if (!$.String._test(output)) throw new TypeError('...');
  return output;
}
  1. implementation will be applied to the above function and dates.

$.Function takes an array of types just as def does. $.Function can thus be used to represent functions of any arity (including nullary functions). The downside of this approach is that representing curried functions is awkward: a -> b -> a, for example, is $.Function([a, $.Function([b, a])]). The solution is to define a binary type constructor:

//    Fn :: (Type, Type) -> Type
const Fn = (x, y) => $.Function([x, y]);

One can then express a -> b -> a as Fn(a, Fn(b, a)).

$.UnaryTypeVariable :: String -> Type -> Type

This essentially combines $.TypeVariable and $.UnaryType. See the readme for details.

Limitations

  • Only functions at the top level of the types array are wrapped.

    The function in [$.Array($.Function([a, b])), $.Array(a), $.Array(b)], for example, will not be wrapped. I hope to address this limitation in the future.

Breaking API changes

  • $.Function has been repurposed. When upgrading the most straightforward substitution is :%s/\$\.\<Function\>/$.AnyFunction/g, but in most cases one will wish to faithfully describe the types of one's higher-order functions (using the new $.Function).
  • “Extractors” of parameterized types are no longer exposed via ._1 and ._2. They are now exposed via .types.$1.extractor and .types.$2.extractor.
  • The inner types of parameterized types are no longer exposed via .$1 and .$2. They are now exposed via .types.$1.type and .types.$2.type.
  • RecordType values no longer expose .fields. The type of the x field, for example, was previously exposed via .fields.x. It is now exposed via .types.x.type. The similarity between RecordType values and values of parameterized types is intentional, and allows code sharing where previously branching logic was required. If one squints one can see a type such as Pair a b as a record type: { $1 :: a, $2 :: b }.

Style changes

Refactoring

It took many, many hours but I was successful in creating a function, createType, which makes it clear what is required of a type and removes duplication.

$.TypeVariable, before:

//  TypeVariable :: String -> Type
$.TypeVariable = function(name) {
  return {
    '@@type': 'sanctuary-def/Type',
    type: 'VARIABLE',
    name: name,
    validate: Right,
    _test: K(true),
    toString: always(name)
  };
};

$.TypeVariable, after:

//  TypeVariable :: String -> Type
$.TypeVariable = function(name) {
  return createType(VARIABLE, name, always2(name), K(true), [], {});
};

$.UnaryType, before:

//  UnaryType :: (String, (x -> Boolean), (t a -> [a])) -> Type -> Type
var UnaryType = $.UnaryType = function(name, test, _1) {
  return function($1) {
    var format = function(f, f$1) {
      return f('(' + stripNamespace(name) + ' ') + f$1(String($1)) + f(')');
    };
    var validate = function(x) {
      if (!test(x)) {
        return Left({value: x, typePath: [t], propPath: []});
      }
      for (var idx = 0, xs = _1(x); idx < xs.length; idx += 1) {
        var result = $1.validate(xs[idx]);
        if (result.isLeft) {
          return Left({value: result.value.value,
                       typePath: [t].concat(result.value.typePath),
                       propPath: ['$1'].concat(result.value.propPath)});
        }
      }
      return Right(x);
    };
    var t = {
      '@@type': 'sanctuary-def/Type',
      type: 'UNARY',
      name: name,
      validate: validate,
      _test: testFrom(validate),
      format: format,
      toString: always(format(id, id)),
      _1: _1,
      $1: $1
    };
    return t;
  };
};

$.UnaryType, after:

//  UnaryType :: (String, (x -> Boolean), (t a -> Array a)) -> Type -> Type
var UnaryType = $.UnaryType = function(name, test, _1) {
  return function($1) {
    var format = function(outer, inner) {
      return outer('(' + stripNamespace(name) + ' ') +
             inner('$1')(String($1)) + outer(')');
    };
    var types = {$1: {extractor: _1, type: $1}};
    return createType(UNARY, name, format, test, ['$1'], types);
  };
};

$.Function and $.UnaryTypeVariable are also defined via createType.


This is a great opportunity to familiarize yourself with this codebase if you're interested but haven't known where to begin. The code should be easier to follow now, though there are still several functions which take six, seven, or even nine (!) arguments without many comments beyond the type annotations. I'm happy to answer questions; please let me know if anything is unclear. Future readers (including future me) will appreciate anything we can do now to clarify the code.

@svozza
Copy link
Member

svozza commented Jun 26, 2016

This looks great, David! I should have a chance to dig into the code tomorrow evening.

@@ -219,41 +164,43 @@
};
};

switch (typeOf(x)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason to remove typeOf?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was part of why quest to reduce the number of helper functions. It was only used in two places so I decided it wasn't pulling its weight. toString.call(x) has the advantage of being more explicit too, once one is aware that toString is a reference to Object.prototype.toString.

I'm happy to reinstate typeOf if you think it improves readability.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not at all I think you're right to make things more explicit.

@davidchambers davidchambers force-pushed the dc-higher-order-functions branch from 779fc34 to ccf8b8e Compare June 29, 2016 18:51
var result = t.type.validate(ys[idx2]);
if (result.isLeft) {
var value = result.value.value;
var propPath = [k].concat(result.value.propPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to use local mutation here, I seem to remember some issue from a while back.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may be thinking of #53. I think it would be okay to use local mutation here, but it would take significant mental effort to convince myself that no one else is holding a reference to the array. I don't see a significant downside to creating a new array here, particularly since we're about to short-circuit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, best leave it as it is.

@davidchambers davidchambers force-pushed the dc-higher-order-functions branch 2 times, most recently from 0ba0bb6 to 41b0b77 Compare June 29, 2016 23:54
'\n' +
'1) "foo" :: String\n' +
'\n' +
'The value at position 1 is not a member of ‘FiniteNumber’.\n'));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a result of sanctuary-js/sanctuary#234 I realized we lacked tests for the two cases above. After writing the tests it was apparent that we also lacked logic to handle these two cases. I've made some changes so these tests now pass.

/cc @jdudek

@davidchambers davidchambers force-pushed the dc-higher-order-functions branch from 41b0b77 to 0c69ad6 Compare July 4, 2016 04:50
@davidchambers davidchambers force-pushed the dc-higher-order-functions branch from 0c69ad6 to bc7bf30 Compare July 4, 2016 05:22
@davidchambers davidchambers force-pushed the dc-higher-order-functions branch 16 times, most recently from 26652da to 5f8b5c1 Compare July 4, 2016 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants