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

Initial PR #3

Closed
wants to merge 50 commits into from
Closed

Initial PR #3

wants to merge 50 commits into from

Conversation

JAForbes
Copy link
Member

@JAForbes JAForbes commented Oct 29, 2016

Closes #1

This is a backwards compatible version of @paldepind's brilliant library: union-type.

I haven't started on the readme yet, but I wanted to open the PR as soon as possible so I can get feedback as early as possible.

I've tried to take care of what I could infer first:

  • 100% code coverage
  • Makefile lint+test+setup
  • LICENSE MIT
  • Linting with sanctuary-style
  • Backwards compatible with paldepind/union-type
  • Added circle CI yml

I'm trying to follow suit with existing projects in the organisation.
Apologies in advance for any missing pieces.

- [X] 100% code coverage
- [X] Makefile lint+test+setup
- [X] LICENSE MIT
- [X] Linting with sanctuary-style
- [X] Backwards compatible with paldepind/union-type
- [X] Added circle CI yml

I'm trying to follow suit with existing projects in the organization.  Apologies in advance for any missing pieces.
@JAForbes JAForbes self-assigned this Oct 29, 2016
@davidchambers
Copy link
Member

This is very exciting. Thank you, @JAForbes, and thank you @paldepind for creating union-type!

jsconfig.json
.eslintrc
.vscode
/coverage/
Copy link
Member

Choose a reason for hiding this comment

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

These comments will reveal my obsessive nature:

  • The final line should be terminated by a newline character (\n). You may like to configure your editor to insert this automatically.
  • I like to begin all paths with / so it's clear exactly where we expect a file or directory to reside. (There are situations in which this is not practical.)
  • I like to end paths to directories with /.
  • I like to list directories before files, and order the paths in each of these groups alphabetically.
  • Only files created by the project should be listed. On macOS, for example, directories may contain .DS_Store files. One never wants to commit these, so they should be listed in one's global .gitignore. .vscode falls into this category.
  • I believe jsconfig.json and .eslintrc could be removed from the list.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great points, I'll update my global .gitignore.

Copy link
Member

Choose a reason for hiding this comment

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

@@ -0,0 +1,25 @@
The MIT License (MIT)

Copyright (c) 2016 Sanctuary
Copy link
Member

Choose a reason for hiding this comment

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

Should we include the following as well?

Copyright (c) 2015 Simon Friis Vindum

If you just drew inspiration from union-type this isn't necessary, but if you used its source code as a starting point I believe Simon has copyright. Fortunately union-type is MIT-licensed. :)

/cc @paldepind

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's add Copyright for Simon (unless he objects), I didn't reference the source until the final stages but the API design is his.

Copy link
Member

Choose a reason for hiding this comment

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

WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

Copy link
Member

Choose a reason for hiding this comment

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

Let's delete this extra line. :)

Copy link
Member

Choose a reason for hiding this comment

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

.PHONY: test
test:
$(ISTANBUL) cover "test/test.js" -- --recursive
$(ISTANBUL) check-coverage --branches 98
Copy link
Member

Choose a reason for hiding this comment

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

Let's change the requirement from 98% to 100%. Currently sanctuary-js/sanctuary-set#1 specifies 98 because I integrated Istanbul after the fact and we haven't yet added test cases for the untested paths.

Copy link
Member

Choose a reason for hiding this comment

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

NPM = npm

SRC = $(.)
TEST = $(shell find test -name '*.js' | sort)
Copy link
Member

Choose a reason for hiding this comment

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

SRC and TEST aren't referenced in the makefile. Let's remove them.

Copy link
Member

Choose a reason for hiding this comment

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


.PHONY: lint
lint:
$(ESLINT) --env node index.js test/test.js
Copy link
Member

Choose a reason for hiding this comment

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

It's not necessary in this case, but I make a habit of including -- to separate keyword arguments from positional arguments:

    $(ESLINT) --env node -- index.js test/test.js

Copy link
Member

Choose a reason for hiding this comment

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


test:
override:
- make lint test
Copy link
Member

Choose a reason for hiding this comment

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

\n

Copy link
Member

Choose a reason for hiding this comment

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


if (mapped !== t){
return mapped;
} else if (t.constructor === Function) {
Copy link
Member

Choose a reason for hiding this comment

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

Let's not rely on object identity for the reasons given in sanctuary-js/sanctuary#100. typeof should be fine here.

Copy link
Member

Choose a reason for hiding this comment

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

return staticCase(options, this);
};

const Setup = function({check, ENV = T.env}){
Copy link
Member

Choose a reason for hiding this comment

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

I'm opposed to functions with optional arguments (and records with optional fields). I've seen this pattern used in Haskell:

fooWithOpts :: Opts -> Bar -> Baz
foo :: Bar -> Baz

foo is defined simply by applying fooWithOpts to the default options.

We could follow Sanctuary's lead here. Sanctuary provides create, but the exported module is itself the result of applying create to the default options. The only downside of this approach is that the user would need to depend on sanctuary-def in order to disable type checking:

const $ = require('sanctuary-def');
const {create} = require('sanctuary-union-type');

const UnionType = create({check: false, env: $.env});

We could, though, export env (as Sanctuary does):

const {create, env} = require('sanctuary-union-type');

const UnionType = create({check: false, env: env});

Regardless of what we decide, we should rename the check field to match the name used by sanctuary-def (and Sanctuary).

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with all of this. One note, the naming for check was completely informed by:

https://github.com/paldepind/union-type#disabling-type-checking

But 👍 for having a consistent API within sanctuary libraries.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yes, I had wondered whether check came from union-type. Since we'll be providing a different API, we're free to rename the property for consistency. :)

Copy link
Member

Choose a reason for hiding this comment

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


const Type = T.NullaryType(
typeName,
a => a && a['@@type'] === typeName
Copy link
Member

Choose a reason for hiding this comment

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

Let's make the guard explicit by replacing a && with a != null &&.

Let's use x as the parameter name to avoid shadowing our type variable named a.

Copy link
Member

Choose a reason for hiding this comment

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

env: ENV,
});

const CreateUnionType = function(typeName, rawCases, prototype = {}){
Copy link
Member

Choose a reason for hiding this comment

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

Let's avoid this optional parameter. We can ensure that prototype is always provided by making this change:

 const Named =
   def('UnionType.Named',
       {},
       [T.String, T.StrMap(T.Any), T.Any],
-      CreateUnionType);
+      (x, y) => CreateUnionType(x, y, {}));

x and y could be renamed, of course. :)

Copy link
Member

Choose a reason for hiding this comment

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

Object.keys(rawCases);

const env =
ENV.concat(Type);
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer ENV.concat([Type]).

Copy link
Member

Choose a reason for hiding this comment

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

"main": "index.js",
"directories": {
"test": "test"
},
Copy link
Member

Choose a reason for hiding this comment

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

Is this actually used in any way? The documentation is not particularly enlightening.

Copy link
Member Author

Choose a reason for hiding this comment

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

No idea, I think npm init added that automatically.

Copy link
Member

Choose a reason for hiding this comment

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

Okay. Let's remove it in that case.

Copy link
Member

Choose a reason for hiding this comment

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

Point.Point(4, 'foo');
} catch (e) {
t.equal(err, e.toString());
}
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about this approach. Won't the assertion only be performed if evaluating the try block results in an exception? I think we need assert.throws here.

We could define a function such as this:

//    throws :: (Function, TypeRep a, String) -> Undefined !
const throws = (f, type, message) => {
  assert.throws(f, err => err.constructor === type && err.message === message);
};

We'd use it like so:

throws(() => { Point.Point(4, 'foo'); },
       TypeError,
       '...');

Copy link
Member

Choose a reason for hiding this comment

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

100
, area(rect)
);
}
Copy link
Member

Choose a reason for hiding this comment

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

Very nice use of block scope! I've never seen this before! 😮

/* eslint-disable no-var */
var List =
Type({Nil: [], Cons: [T.Any, List]});
/* eslint-enable no-var */
Copy link
Member

Choose a reason for hiding this comment

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

This is confusing. This would be much clearer:

const List = Type({Nil: [], Cons: [T.Any, undefined]});

I think we can do better though. How about supporting one of the following?

const List = Type({Nil: [], Cons: [T.Any, UnionType.Self]});
const List = Type({Nil: [], Cons: [T.Any, UnionType.Recur]});
const List = Type({Nil: [], Cons: [T.Any, UnionType.Recursive]});

The value of UnionType.Self (or whatever we were to call it) could simply be undefined, but I'd prefer something like {'@@functional/recursive-type-reference': true}.

Copy link
Member

Choose a reason for hiding this comment

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

Let's discuss this in #4.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree its confusing, using undefined could create recursive types by accident, but I would really like this library to be a superset of paldepind/union-type's API.

I'm torn, on the one hand removing the undefined feature seems like the right API design choice, but then we are no longer supporting the existing API and discussions of API design are suddenly warranted. That isn't a path I want to go down.

But on the other hand, if there was ever a juncture to make a breaking change it would be now.

But ultimately I feel there is more value in continuing the trajectory of keeping this library as a super-set of the original. So I'd prefer to continue to support undefined and in addition support UnionType.self = {'@@functional/recursive-type-reference': true}.

Its a similar decision to supporting paldepind's anonymous union type's in addition to named union-types.

Copy link
Member

Choose a reason for hiding this comment

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

If we continue to use undefined as the sentinel value users are free to alias it:

const Self = undefined;

// ...

const List = Type({Nil: [], Cons: [a, Self]});

I could live with this. :)

@@ -4,6 +4,9 @@ const $ = require('sanctuary-def');
const Z = require('sanctuary-type-classes');


// stripNamespace :: String -> String
const stripNamespace = s => s.slice(s.indexOf('/') + 1);
Copy link
Member

@safareli safareli Nov 3, 2016

Choose a reason for hiding this comment

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

What should happen if input is my-package/sub-package/Point ? maybe we should use lastIndexOf?

Copy link
Member

Choose a reason for hiding this comment

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

Good question. I've been assuming type identifiers contain one slash (/) at most.

@@ -176,7 +178,7 @@ test('Destructuring assignment to extract values', () => {
});

test('Recursive Union Types', () => {
const List = Type({Nil: [], Cons: [$.Any, undefined]});
const List = Type({Nil: [], Cons: [a, undefined]});
Copy link
Member

Choose a reason for hiding this comment

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

I think the recursion could be expressed like this:

const List = Type({Nil: [], Cons: (List) => [a, List(a)]});
const Tree = Type({Tip: [a], Node: (Tree) => [Tree(a), Tree(a)]});

@davidchambers
Copy link
Member

@JAForbes, shall we close this pull request now that JAForbes/sum-type exists? I will open an initial pull request for a more Sanctuary-style approach when I complete the dozen or so Sanctuary-related tasks currently on my to-do list.

@JAForbes
Copy link
Member Author

Yes, close away 😊

-----Original Message-----
From: "David Chambers" [email protected]
Sent: ‎11/‎21/‎2016 2:02 PM
To: "sanctuary-js/sanctuary-union-type" [email protected]
Cc: "James Forbes" [email protected]; "Mention" [email protected]
Subject: Re: [sanctuary-js/sanctuary-union-type] Initial PR (#3)

@JAForbes, shall we close this pull request now that JAForbes/sum-type exists? I will open an initial pull request for a more Sanctuary-style approach when I complete the dozen or so Sanctuary-related tasks currently on my to-do list.

You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

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.

3 participants