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

static ‘empty’ value #164

Closed
wants to merge 1 commit into from

Conversation

davidchambers
Copy link
Member

This pull request introduces two changes:

As @scott-christopher pointed out in #162 (comment) there are situations in which one has a type value and wants the type's identity element, but can't determine it because it's only available via the empty method of values of the type, and one doesn't know how to construct a value!

As currently specified, the Monoid type class is not very useful. We're trying to have our 🍰 and eat it too, but it's not working. In order to accommodate the generalized Monoid m => functions such as Scott's Const.of we need to know that T.empty will be available. The spec must therefore require this. If it's to do so, the Identity type and several others can no longer satisfy Monoid. At that point the instance method serves no purpose (beyond saving one from typing .constructor occasionally).

If we're to have a static method only we can no longer rely on run-time values, so empty needn't be a method. Rather than a nullary function which returns the identity element, it can simply be the identity element. Scott pointed out that one could define empty as a getter if one felt so inclined. 😱

leftIdentity: test((x) => monoid.leftIdentity(Id[of](Sum[empty]()))(equality)(Sum[of](x))),
rightIdentity: test((x) => monoid.rightIdentity(Id[of](Sum[empty]()))(equality)(Sum[of](x)))
leftIdentity: test((x) => monoid.leftIdentity(Sum)(equality)(x)),
rightIdentity: test((x) => monoid.rightIdentity(Sum)(equality)(x))
};
Copy link
Member Author

Choose a reason for hiding this comment

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

Could someone review this change?

It's a bit strange for code in id_test.js to test the Sum type, but the Id type can no longer satisfy Monoid.

@CrossEye
Copy link

To my mind, this still doesn't solve the issue raised in #88. It helps, but it falls short of fixing the basic problem.

I said this:

It seems to me that the spec is confused on what it wants to define. It knows that defining types is paramount, and it would love to do so by specifying only the algebraic laws affecting the properties of instances of those types. But because of the issues discussed here, it needs also to be able to define a few meta-properties (of, empty) that can't so easily be associated with instances. Rather than accepting this fact, and just saying that the type definition also includes such a dictionary, the spec tries to have it both ways, saying you can define these on the instances or on such a dictionary. But it seems a form of schizophrenia.

While this PR does explicitly say that the dictionaries have to exist, it also lists the canonical path to the dictionary as starting with an instance:

A value which has a Monoid must provide an `empty` value on its `constructor` object:

    m.constructor.empty

My concern is that we simply may not have an instance available. The example we kept mentioning in #88 was foldMap:

foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m

What happens if that second parameter contains no instances?

The trouble, of course, is that any fix to this problem would probably mean quite a radical change to FL. I haven't yet had the time to investigate StaticLand, but it's possible that it might hold a solution to this; that doesn't address the FantasyLand problem, though.

So, in other words, my take is that this moves in the right direction, but not far enough. I don't know what would move us that way though.

@davidchambers
Copy link
Member Author

My concern is that we simply may not have an instance available. The example we kept mentioning in #88 was foldMap:

foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m

What happens if that second parameter contains no instances?

It seems to me one of the following types would work:

foldMap :: (Foldable t, Monoid m) => m -> (a -> m) -> t a -> m
foldMap :: (Foldable t, Monoid m) => { fantasy-land/empty :: m } -> (a -> m) -> t a -> m

@safareli
Copy link
Member

safareli commented Sep 23, 2016

Identity a can't be a Monoid but identity m can, when m is Monoid. So it's sort of correct to not have static method/value empty on Identity type but have on it's values as only way for Identity to be a Monoid is to have Monoid as it's value. For that reason I think it's better to favor instance method/value #163 then have only static method/value.

@CrossEye
Copy link

@davidchambers:

Sorry, I missed your response.

It seems to me one of the following types would work:

foldMap :: (Foldable t, Monoid m) => m -> (a -> m) -> t a -> m
foldMap :: (Foldable t, Monoid m) => { fantasy-land/empty :: m } -> (a -> m) -> t a -> m

Yes, that's what I'm getting at. If you're going to need signatures like that, why not use the dictionary/type-metadata as the first parameter? (I know that the spec calls this "constructor" but that's mostly a pun from the Javascript side.)

If you have access to the empty instance of the type, of course that would be fine, but the spec doesn't declare the mathematical instance, only a function to retrieve it. Supplying an arbitrary instance in order to derive that empty value does not seem doable anywhere that one could not also supply the "constructor".

A radically different solution for Monoids would be simply to require the dedicated value, not a function. But this doesn't help with of.

@CrossEye
Copy link

@safareli:

I think the difference (at least in my mind) is that Identity is not a type. Identity String is a type, and if String is considered with the monoid {empty: () => '', concat: (a, b) => a + b}, then Identity String can be seen as a monoid in an obvious manner. For any Monoid m, we have a separate type, Identity m which also forms a monoid with its own empty element.

@davidchambers
Copy link
Member Author

I'm confused by your comment, @CrossEye. 😕

If you're going to need signatures like that, why not use the dictionary/type-metadata as the first parameter?

Does { fantasy-land/empty :: m } not represent a type dictionary?

A radically different solution for Monoids would be simply to require the dedicated value, not a function.

Is this not the very change effected by this pull request?

@CrossEye
Copy link

CrossEye commented Sep 24, 2016

Does { fantasy-land/empty :: m } not represent a type dictionary?

Not unambiguously, to me. But if that's what you intend with it, I'd be happy with this example, although not your other one.

A radically different solution for Monoids would be simply to require the dedicated value, not a function.

Is this not the very change effected by this pull request?

Yes, sorry. I got so caught up in the location of empty, I forgot that you were redefining what it is.

@davidchambers
Copy link
Member Author

I'd like to know why you're okay with { fantasy-land/empty :: m } but not with m, @CrossEye. Is it because you want the caller to be able to provide T or t rather than T['fantasy-land/empty'] or t['fantasy-land/empty']? On a related note, what do you think of this pull request?

@scott-christopher
Copy link
Contributor

If we were to remove the empty property attached to the instance, I'm not sure I'd want the only remaining reference to be attached to the constructor property, as this is still only defined against the instance. I realise constructor becomes a reference to the type when instantiated via new FooType, but I quite often declare FL constructors as:

const Additive = (x) => ({
  concat: (y) => Additive(x + y),
})
Identity.empty = Additive(0)

For this to be a valid FL monoid, I would need to modify that to include the constructor property:

const Additive = (x) => ({
  constructor: Additive,
  concat: (y) => Additive(x + y),
})
Additive.empty = Additive(0)

Even after doing this, we're still effectively saying in the spec that you can't guarantee Additive.empty will exist, as we've only specified Monoid m => m.constructor.empty :: m should exist, so generic FL libraries would still need some instance to obtain the empty value while following the spec.

I'm starting to wonder whether we do away with specifying what the names and locations of empty and of are and just specify that for something to be considered a Monoid it must provide a way to produce a static empty value such that empty.concat(m) == m && m.concat(empty) == m, and similarly for Applicative. At which point, we'd just make use of foldMap :: (Foldable t, Monoid m) => m -> (a -> m) -> t a -> m and give up the ability to say that the first argument must be empty (at least in the type signature).

@safareli
Copy link
Member

What if we somehow represent not Monid value but Monid type
foldMap :: (Foldable t, Monoid m) => m _ -> (a -> m) -> t a -> m
m _ would state that we dont care what is inside m as long as it's Monoid and if we dont care about it's value we should not use methods from monoid which need an instance (concat) but we can use methods which don't necessarily need instance value empty. so in usage of this function we could pass actual Type as it has empty method/value or any value as it's value would have empty method/value and m _ would be correct for both of them.

p.s. this might be a stupid idea as well :d

@davidchambers
Copy link
Member Author

Superseded by #180

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.

None yet

4 participants