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

require static methods to be defined on type representatives #180

Merged

Conversation

davidchambers
Copy link
Member

Closes #176

Type representatives

Certain behaviours are defined from the perspective of a member of a type. Other behaviours do not require a member. Thus certain algebras require a type to provide a value-level representative (with certain properties). The Identity type, for example, could provide Id as its type representative: Id :: TypeRep Identity.

If a type provides a type representative, each member of the type must have a constructor property which is a reference to the type representative.

I imagine there are several things I neglected to update. Let me know if you think of one. :)

#### `empty` method
<div id="empty-method"></div>

#### `empty` value
Copy link
Member Author

Choose a reason for hiding this comment

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

The empty div allows existing links to this section to continue to work.

@davidchambers
Copy link
Member Author

The term "type representative" was suggested by @tel in sanctuary-js/sanctuary#64 (comment). Type representatives are also described in the Sanctuary documentation.

Other behaviours do not require a member. Thus certain algebras require a
type to provide a value-level representative (with certain properties). The
Identity type, for example, could provide `Id` as its type representative:
`Id :: TypeRep Identity`.
Copy link
Member

@safareli safareli Oct 3, 2016

Choose a reason for hiding this comment

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

I did not quite get last sentence, especially Id :: TypeRep Identity

Copy link
Member Author

Choose a reason for hiding this comment

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

In Haskell types are "real" in the sense that one actually references types in one's programs:

inc :: Int -> Int
inc x = x + 1

In JavaScript types are not referenced (except in comments):

//    inc :: Number -> Number
const inc = x => x + 1;

In JavaScript the idea of types exists—as evidenced by the use of type signatures in comments—but there's no "type level"; only a "value level". Since we can't reference types at the type level, we must reference them at the value level. So, for each type we'd like a value which represents it. We call such values type representatives.

Let's consider some examples:

  • Number is the representative of the Number type. Number is the sole inhabitant of the TypeRep Number type.
  • Id is the representative of the Identity type. Id is the sole representative of the TypeRep Identity type.

The formatting above is very much intentional. The Number type exists only as an idea, so should not be formatted as code. Number, on the other hand, is a JavaScript value which represents the Number type (in addition to its role as a function from Any to Number).

It took me a while to come to terms with the idea of type representatives. This sort of thinking is not natural in a dynamically typed language. I suggest reading sanctuary-js/sanctuary#64 if you have not already done so.

Copy link
Member

@safareli safareli Oct 3, 2016

Choose a reason for hiding this comment

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

The part which confused me was Id :: TypeRep Identity (Id and Identity are different when Number and Number are same) is it because we assume definition of Identity would be something like this type Identity a = Id a?

So if we have:

type Maybe a = Just a | Nothing

Then is this true:?

Just :: TypeRep (Maybe a)
Nothing :: TypeRep (Maybe a)

will take a look at that issue more carefully

Copy link

Choose a reason for hiding this comment

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

In Haskell at least those last equations are wrong.

  • Just constructs values at the Maybe type, it's not a representative of the type. In particular, a type should be represented in the shape of the type, not its values.
  • Maybe is an interesting example since it's a type constructor, a type function. If we want to represent this is a value-level type representative then TypeRep Maybe and TypeRep X must be able to be combined into TypeRep (Maybe X). Perhaps the type reps of constructors are functions, or probably better (since it'll be easier to analyze) you could have something that gives you ($$) :: TypeRep f -> TypeRep a -> TypeRep (f a)

Copy link

Choose a reason for hiding this comment

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

If that $$ is data then you can easily analyze it!

Copy link
Member Author

Choose a reason for hiding this comment

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

The part which confused me was Id :: TypeRep Identity (Id and Identity are different when Number and Number are same) is it because we assume definition of Identity would be something like this type Identity a = Id a?

When we talk about "the Identity type" we're referring to a concept with no manifestation in JavaScript. When we talk about "Id" we're referring to a real thing: an identifier we could evaluate in a REPL. Id lives in the world of values to the left of the colons; Identity lives in the world of types to the right of the colons.

Since the world of values and the world of types are separate, we needn't worry about naming collisions. This is valid:

Identity :: a -> Identity a

Identity on the left of the :: is a value, whereas Identity on the right is a type (a type constructor, actually).

Similarly, we could name our type representative Identity rather than Id without conflict:

Identity :: TypeRep Identity

I hope this clarifies things a little. :)

@rpominov
Copy link
Member

rpominov commented Oct 3, 2016

I like a lot this idea. Still wonder maybe we could make this not a breaking change or at least less breaking? The piece that concerns me is transition from empty function to empty value. What if we hold this particular change until the next major release? This way it will be less burden for implementers, their types most likely to just continue to be compatible, if we don't do the empty() -> empty part.

@safareli
Copy link
Member

safareli commented Oct 3, 2016

It makes sense with chainRec and of as they either exist for some Type or not and they don't need instance value to work properly.

We have Semigroup which needs instances to exist and Identity can be Semigroup if value in it is also a Semigroup but what I don't like is that now we can't define Monoid instance for Identity which holds Monoid value. One way would be to have specific Identities for each type like this:

const makeId = M => {
  function Identity(value){...}
  Identity.empty = Identity(M.empty)
  return Identity
}

Do you see any way to make Monoid m => Identity m a monoid?

@rpominov
Copy link
Member

rpominov commented Oct 3, 2016

now we can't define Monoid instance for Identity which holds Monoid value.

Could we do it before though? Technically maybe, but I don't think it was very useful Monoid in practice.

@davidchambers
Copy link
Member Author

Still wonder maybe we could make this not a breaking change or at least less breaking? The piece that concerns me is transition from empty function to empty value.

I believe I can provide irrefutable logical reasoning for the empty change. Let's find out. 😄

First, let's consider the question of breaking versus less breaking. From a SemVer perspective there's no distinction: any breaking change requires a major version increment. Were we to stick with an empty method for now, would this pull request still be breaking? The answer is yes (emphasis mine):

A value which has an Applicative must provide an of method on itself or its constructor object.

Currently it's valid to define of only on members of a type. If this pull request is merged, types defined in this way will no longer satisfy the requirements of Applicative. The location changes for empty and chainRec are also breaking changes.

Even without the empty change, this pull request will necessitate a major version increment if merged.

We should also consider the burden changes will place on authors of ADT libraries. I imagine this is what you had in mind when you wrote "less breaking".

Aside from the empty change, the burden this pull request may place on authors of ADT libraries is small. In most cases nothing would be required (though obsolete methods should be deleted to avoid confusion).

The next question, then, is whether we greatly increase the burden on authors of ADT libraries by changing the type of empty from Monoid m => () -> m to Monoid m => m.

I believe the answer is no. Simply by moving the method from the members to the type representative we preclude certain types—Identity, for example—from satisfying the requirements of Monoid. For some ADT libraries this will require documentation updates and major version increments of their own. This may require a significant amount of work, but this work would be required whether or not empty is changed from a method to a value.

The question then becomes why it was decided that empty should be a method in the first place. The reason was that certain types can be monoidal when the "inner" type is monoidal. This allows Id('abc').empty() to evaluate to Id(''). Since empty will no longer be an instance method, though, this will no longer be possible. If this is no longer possible, one of the two arguments against #82 will no longer be valid. The other argument against #82 was that it would be a breaking change necessitating a major version increment. This argument no longer applies either as the other changes in this pull request already commit us to a major version increment.

I'd love to merge this pull request and publish fantasy-land@2.0.0. Upgrading from 1.x.x to 2.x.x should not be difficult, in most cases, and many projects will be able to go straight from 0.2.x to 2.x.x.

@safareli
Copy link
Member

safareli commented Oct 3, 2016

@rpominov yes we could:

Id.prototype[fl.empty] = function() {
    return new Id(typeof this.value[fl.empty] == 'function' ? this.value[fl.empty]() : this.value.constructor[fl.empty]());
};      

looks like it's not that useful don't know, and this technique would not work for those types which don't hold value in moment of calling empty (IO/Task/Function)

@rpominov
Copy link
Member

rpominov commented Oct 3, 2016

I imagine this is what you had in mind when you wrote "less breaking".

Yea.

I'd love to merge this pull request and publish fantasy-land@2.0.0. Upgrading from 1.x.x to 2.x.x should not be difficult, in most cases, and many projects will be able to go straight from 0.2.x to 2.x.x.

But on the second thought, I agree. This should be done eventually, and not to much of a burden in any case.

@rpominov
Copy link
Member

rpominov commented Oct 3, 2016

@safareli Yea, but seems like it can be done only for Identity type. And even though Identity itself can be useful sometimes (as a noop type), that particular Monoid isn't useful at all.

@davidchambers
Copy link
Member Author

now we can't define Monoid instance for Identity which holds Monoid value.

Could we do it before though? Technically maybe, but I don't think it was very useful Monoid in practice.

This pull request has several predecessors. The first one I submitted was #162, which required empty to be defined on members to enable Identity to satisfy the requirements of Monoid. In the discussion thread @scott-christopher made an insightful comment which began:

I find empty most useful when an instance is not available, so I would still want a way to obtain an empty value statically.

If empty is to be defined in one place only (as I strongly believe it should be), we must choose between allowing Identity (and possibly other types) to satisfy the requirements of Monoid, and allowing Const (and possibly other types) to do so. I can't recall ever being led astray by Scott's suggestions—he has a much better grasp of this stuff than I do—so I'm inclined to believe the Const use case is more important. @rpominov's comments above lend credence to this view.

It certainly seems odd to have to go from a type representative to a member of the type (via of) in order to get to the empty member (via empty). Furthermore, this approach doesn't work for types which satisfy Monoid but not Applicative.

@safareli
Copy link
Member

safareli commented Oct 4, 2016

I tried to make Function a Monoid as in Haskell:

instance Monoid b => Monoid (a -> b) where
        mempty _ = mempty
        mappend f g x = f x `mappend` g x

but we can't:

 // what `empty` should be we dont know?
function.prototype.empty = (x) => empty
function.prototype.concat = function(g) {
  return (x) => this(x).concat(g(x))
}

And this problem would be with IO/Task as we don't have a value in it.


inspiration: Monads in Dynamically-Typed Languages by @tonyg

not sure if it would be correct but, as empty is only usable in concat what if had a special partial empty value and check against it in concat. so we can make Identity, Function a monoid again:

const isEmpty = e => e['@functional/empty'] === true
// this value is a empty value for any Monoid
const empty = ({
  '@functional/empty': true,
  'constructor': { 'empty': empty},
  'concat': a => a,
})

Function.empty = _ => empty
Function.prototype.concat = function(g) {
  return (x) => {
    const b = g(x)
    if (isEmpty(b)) return this(x)
    else this(x).concat(b)
  }
}
const f1 = (a) => [a, a]
const f2 = (a) => [a+1, a-1]
f1.concat(f2)(1)
Function.empty.concat(f2)(1)  // same as  `f2(1)`

Identity.empty = Identity(empty)
Identity.prototype.concat = function(idB) {
  const a = this.value
  const b = idB.value
  if (typeof a.concat !== 'function' || typeof b.concat !== 'function') {
    throw new TypeError('`concat` called on Identity which does not hold Monoid value')
  }
  if (isEmpty(b)) return Identity(a)
  else Identity(a.concat(b))
}

This way even Identity could conform to Monoid. This technique could be used for universal return or of method which just takes a value and no container. but returns a "partial" value like this:

const of = (a) => ({
  '@functional/of': true,
  value: a,
  chain: (f) => f(a),
  constructor: {'of': of},
  ap: (f) => {
    if (isOf(f)) return f.value(a)
    if (typeof f.constructor.of !== 'function') {
      throw new TypeError('`ap` called on value which is not Applicative')
    }
  return f.constructor.of(a).ap(f)
})
isOf = a => a['@functional/of']
Identity.prototype.ap = (f) => {
  if(isOf(f) return this.map(f.value)
  else Identity(f.value(this.value))
}
of(10) // [1]
  .ap(of(a => a)) // [2]
  .chain(a => of(a)) // [3]
  .chain(a => Http.get(a)) // [4]
// [1,2,3] - we don't know what in which type we are wrapping value we have just
//           default implementation for some interfaces a value could possibly hold
// [4]- now as Http.get returns Task value would be of that type, we don't know
//      and don't need to know it type upfront.

We could have this isEmpty/empty/of/isOf in libs like ramda as well.

p.s. Implementations might not be 100% correct, I have not checked them but it gives a general idea.

update: fixed some issues in example implementations and added some comments. (still not tested, will test them in couple hours)

@safareli
Copy link
Member

safareli commented Oct 5, 2016

@rpominov @davidchambers what are your thoughts on this "trick" ?

@rpominov
Copy link
Member

rpominov commented Oct 5, 2016

@safareli I don't know. It could work but it seems so different from what we've been doing before. Also seems quite complicated.

@davidchambers
Copy link
Member Author

I haven't yet wrapped my head around your suggestion, @safareli. It deserves its own thread. :)

I'd love to know what @puffnfresh, @SimonRichardson, @joneshf, or one of the other old hands thinks of this pull request. It's a significant change, but so far it hasn't encountered significant opposition. Is losing the ability to define fantasy-land/empty for the Identity type really the biggest downside? Are we overlooking something important?

@safareli
Copy link
Member

safareli commented Oct 5, 2016

With trick, I provided, we don't even loos ability to define monoid instance for Identity.

Here I have created a gist on that idea.

@safareli
Copy link
Member

safareli commented Oct 9, 2016

Here you can check how Function, Identity, Pair, Task could be Monoids. I would need to test them too but it gives an idea of what implementation looks like (it's a bit boilerplate thou). In this PR you can comment/suggest anything ❤️.

@safareli
Copy link
Member

@puffnfresh, @SimonRichardson, @joneshf any thoughts on this? I think this is exciting change and safareli/quasi/examples proves that with this changes we lose nothing 🌮

@SimonRichardson
Copy link
Member

I like it, but want to think about it if you don't mind... Ping me if I
forget to respond..

On Tue, 11 Oct 2016, 16:43 Irakli Safareli, notifications@github.com
wrote:

@puffnfresh https://github.com/puffnfresh, @SimonRichardson
https://github.com/SimonRichardson, @joneshf
https://github.com/joneshf any thoughts on this? I think this is
exciting change and safareli/quasi/examples
https://github.com/safareli/quasi/tree/initial/examples proves that
with this changes we lose nothing 🌮


You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub
#180 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACcaGJzrWRr9wzK2PuXwPHj0IYTCLvwfks5qy661gaJpZM4KMnaI
.

@rpominov
Copy link
Member

Wrote some question regarding quasi here safareli/quasi#1 (comment) . Probably would be best to move discussion there, this is kinda off-topic for this PR.

@davidchambers
Copy link
Member Author

((:bell:)) @SimonRichardson, have you given this matter some thought?

@SimonRichardson
Copy link
Member

Nope 😱

On Wed, 19 Oct 2016, 19:20 David Chambers, notifications@github.com wrote:

((🔔)) @SimonRichardson https://github.com/SimonRichardson, have you
given this matter some thought?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#180 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACcaGJ1rrO609Ae-hFFJhxG9LX7K3LgHks5q1l9YgaJpZM4KMnaI
.

Sum[of] = (x) => Sum(x);
Sum[empty] = () => Sum('');
Sum.prototype[of] = Sum[of];
Sum.prototype[empty] = Sum[empty];
Copy link
Member

Choose a reason for hiding this comment

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

I don't get this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

At some point I thought Sum('').constructor did not evaluate to Sum. I was mistaken. I've reverted to the daggy version.

@SimonRichardson
Copy link
Member

Right, I'm happy with this change, we should bump to v2.0.0 I guess?

@rpominov
Copy link
Member

+1 from me. I would rather didn't have empty() -> empty change though, just because it's not very important but would require major version bump in static-land, for instance, because I want it to be as close to fantasy-land as possible.

@davidchambers
Copy link
Member Author

I'm happy with this change

Fantastic!

we should bump to v2.0.0 I guess?

Yes, we should.

I would rather didn't have empty() -> empty change though, just because it's not very important but would require major version bump in static-land

Could you elaborate, @rpominov? Is Static Land not affected by the other breaking changes in this pull request?

@rpominov
Copy link
Member

Could you elaborate, @rpominov? Is Static Land not affected by the other breaking changes in this pull request?

Yeah, I think so. Not affected by other changes.

@rpominov
Copy link
Member

rpominov commented Oct 20, 2016

One more minor point in favor of method over a value. In the spec we often need to say something about methods. For example we have a title "Prefixed method names". Strictly speaking we would now have to say something like "methods and values" in all such places which will make it more confusing. Or we could keep saying only "methods" but that also will be confusing because one will think "does this include empty?".

@davidchambers
Copy link
Member Author

One more minor point in favor of method over a value. In the spec we often need to say something about methods. For example we have a title "Prefixed method names". Strictly speaking we would now have to say something like "methods and values" in all such places which will make it more confusing. Or we could keep saying only "methods" but that also will be confusing because one will think "does this include empty?".

Good point, @rpominov! I believe we should replace the word "method" regardless, as it means different things to different people. In JavaScript I distinguish between "methods" and "functions": the former expect this to refer to a value of a particular type; the latter do not. According to this definition, fantasy-land/empty is not a method even if we stick with Monoid m => () -> m.

We could continue to use "method" in the Java sense of "instance" methods and "static" methods. Regardless of whether we stick with "method" or choose another term, we should define the term.

@davidchambers
Copy link
Member Author

@rpominov, I've pushed a commit to rephrase potentially confusing references to "methods". How does it look?

Although I prefer Monoid m => m to Monoid m => () -> m, specifying a single location for empty, of, and chain is far more important to me. If after weighing the costs of updating Static Land and Static Land libraries against the benefit of a simpler type you feel strongly that we should stick with a nullary function, I'm happy to make that change. Let me know. :)

@rpominov
Copy link
Member

rpominov commented Oct 22, 2016

@rpominov, I've pushed a commit to rephrase potentially confusing references to "methods". How does it look?

"properties" sounds great 👍

If after weighing the costs of updating Static Land and Static Land libraries against the benefit of a simpler type you feel strongly that we should stick with a nullary function, I'm happy to make that change. Let me know. :)

Can't say that I feel strongly, but still feel like we'd better don't make this change. This is not only about static-land and static-land compatible libs. Any fantasy-land@1.0.x compatible lib that didn't delegated Monoid functionality to wrapped items also won't be affected by the rest of the changes, right?

@davidchambers
Copy link
Member Author

I've updated the pull request: empty is once again a nullary function. It's now acceptable to refer to "methods", so none of the headings needs to change, so the <div id="empty-method"></div> cruft is no longer necessary. :)

If you're happy with the changes, @rpominov, we'll ask @SimonRichardson to sign off and merge. :)

@rpominov
Copy link
Member

Thank you! Looks great 👍

@davidchambers
Copy link
Member Author

Okay. Do you agree, @SimonRichardson? If so, let's merge this and release v2.0.0.

Copy link
Member

@SimonRichardson SimonRichardson left a comment

Choose a reason for hiding this comment

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

Some nice consistency changes as well 👍

@davidchambers davidchambers merged commit af9126a into fantasyland:master Oct 23, 2016
@davidchambers davidchambers deleted the type-representatives branch October 23, 2016 09:15
@davidchambers
Copy link
Member Author

sanctuary-js/sanctuary-type-classes#6 shows how much simpler this makes interacting with empty, of, and chainRec in a compliant manner. :)

rjmk added a commit to rjmk/fantasy-land that referenced this pull request Nov 12, 2016
* 'master' of github.com:fantasyland/fantasy-land: (29 commits)
  Version 2.1.0
  Add Alt, Plus and Alternative specs (fantasyland#197)
  Use uppercase letters for Type representatives in laws (fantasyland#196)
  Fix id_test and argument order in laws (fantasyland#193)
  Version 2.0.0
  Another go at updating dependencies (fantasyland#192)
  release: integrate xyz (fantasyland#191)
  test: remove unnecessary lambdas (fantasyland#190)
  require static methods to be defined on type representatives (fantasyland#180)
  lint: integrate ESLint (fantasyland#189)
  Enforce parametricity (fantasyland#184)
  readme: tweak signatures to indicate that methods are not curried (fantasyland#183)
  Fix reduce signature to not use currying (fantasyland#182)
  Link to dependent specifications (fantasyland#178)
  Add Fluture to the list of implementations (fantasyland#175)
  laws/functor: fix composition (fantasyland#173)
  laws/monad: fix leftIdentity (fantasyland#171)
  Minor version bump
  bower: add bower.json (fantasyland#159)
  Fix chainRec signature to not use currying (fantasyland#167)
  ...
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.

5 participants