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

Why can of be defined on the constructor? #88

Closed
CrossEye opened this issue Feb 11, 2015 · 55 comments
Closed

Why can of be defined on the constructor? #88

CrossEye opened this issue Feb 11, 2015 · 55 comments

Comments

@CrossEye
Copy link

It's quite possible that I'm simply missing something simple, but I'm disturbed by this sentence (emphasis added):

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

(All the same points will apply to Monoid's empty method, but it's Applicatives which are worrying me now.)

For a specification which is usually so prescriptive, this is surprisingly lax. But that's not the real problem. It seems to me that this makes it tremendously more difficult to write code that works across all Applicative Types; in fact, it probably makes it impossible.

A few points:

  • First of all, the Applicative laws, and Monad's left identity are written only in terms of the instance version of of. Should the specification be taken to read, for instance, "The appropriate one of m.of(a).chain(f) or m.constructor.of(a).chain(f) is equivalent to f(a) (left identity)" or some more precise version of the same?
  • It makes unwarranted assumptions about how I create types. In these days of Object.create, why should the specification assume that I use constructor functions to define my types? It's quite possible to do without them, and it's growing ever more popular to work that way.
  • It is possibly incoherent. Since nothing specifies which of the two I must implement, I can choose to implement the same of on both. This would probably not cause an issue. But nothing in the specification would prevent me from creating a conforming of on the instances and an unrelated of on the constructor or vice versa:
    Maybe.of = function(val) {return new Just(val);};
    Maybe.prototype.of = function(val) {throw "Unknown Preposition";};

Does that conform to the specification as written so long as Maybe.of upholds the required laws?

Does this one so long as Maybe.prototype.of upholds the laws?:

    Maybe.of = function(val) {throw "Unknown Preposition";};
    Maybe.prototype.of = function(val) {return new Just(val);};

If so, can anyone suggest a way that I can generically apply of to algebraic types without knowing for each specific type which version is being used?

Or... am I just missing something simple again?

This was all brought to mind by a recent Ramda issue.

@michaelficarra
Copy link
Contributor

We ran into this problem when creating an interface in shift-reducer-js that accepts a Fantasy Land Monoid. We opted to prefer the function on the prototype over the function on the constructor itself, but the decision was mostly arbitrary. I don't know if I agree with you about attaching the function to the constructor being nonsensical. We need a way to pass around the Monoid representation as a value, and without a type system, it makes sense to attach each function to a constructor.

@robotlolita
Copy link
Contributor

I believe this is mostly to accommodate a common idiom in JavaScript that is using constructor functions to initialise instances. In this way, it's assumed that ADTs will be defined as either:

class Maybe;
class Just extends Maybe;
class Nothing extends Maybe;

Or by using unrelated instances and leaving dynamic dispatch to decide all the behaviour. In truth, there isn't much of a difference between the two in JS, since there are no types, but we need to be able to accommodate for calling of without having an existing instance of maybe, since of isn't a method related to a particular Maybe value, but rather the "type" Maybe itself.

(edit: sorry for closing this issue earlier and the typos, I sent this from my phone :x)

@bergus
Copy link
Contributor

bergus commented Feb 11, 2015

I would propose that all type functions need to (only) be available as methods, on the instances. That's how the laws are specified, and that's how libraries will use them - as without instances, there is no type information in js.
Methods that do not need to inspect a value (like of and empty) must not rely on the this value, so that they can be passed around freely. This will be required for functions that want to create values with only knowing a "type".
Such methods should also be available as static functions (e.g. on the constructor of a class), so that they can be accessed directly (without an instance) when the expected type is known.

This way, we also won't get any problems with name collisions. Functions implement monads? Even when all constructor functions inherit an of method, that doesn't make their class instances applicatives.

@CrossEye
Copy link
Author

@michaelficarra: It's not that I think that attaching to a constructor function is nonsensical. It's the requirement to do so that seems nonsensical. There are good ways in Javascript to create types that do not depend on constructor functions. I believe this would be a legitimate version of Id with no constructor at all:

var makeId = (function() {
    var baseId = {
        equals: function(b) { // Setoid
            return typeof this.value.equals === "function" ? this.value.equals(b.value) : this.value === b.value;
        },
        concat: function(b) { // Semigroup (value must also be a Semigroup)
            return makeId(this.value.concat(b.value));
        },
        empty: function() { // Monoid (value must also be a Monoid)
            return makeId(this.value.empty ? this.value.empty() : this.value.constructor.empty());
        },
        map: function(f) { // Functor
            return makeId(f(this.value));
        },
        ap: function(b) { // Applicative
            return makeId(this.value(b.value));
        },
        chain: function(f) { // Chain
            return f(this.value);
        },
        extend: function(f) { // Extend
            return makeId(f(this));
        },
        of: function(a) { //Monad
            return makeId(a);
        },
        from: function() { // Comonad
            return this.value;
        }
    };

    function makeId(a) {
        var id = Object.create(baseId);
        id.value = a;
        return id;
    }
    return makeId;
}());

The prototypes of makeId('a') and makeId('b') are identical, which is really what is meant in Javascript for two things to be of the same type. But there is no useful constructor function around. (In this version makeId('a').constructor == Object, but we could write a version where it's null.) There are many in the JS community insisting that this is the only proper way to do OO in JS. While I disagree with that extreme, it is certainly one reasonable way to do so.

It simply seems to me that if we insist that instances have the of, that does not preclude implementations that provide other mechanisms that seem useful, but then the laws can be simple and the implementations don't need to make these choices.

@CrossEye
Copy link
Author

@robotlolita:

In truth, there isn't much of a difference between the two in JS, since there are no types.

I'd dispute that. I'd say having a common prototype means being of the same type. But that's not central here. That constructor functions, as I noted in my previous comment, are not the only way --or possibly even the best way -- to work with such types is pretty important.

[W]e need to be able to accommodate for calling of without having an existing instance of maybe, since of isn't a method related to a particular Maybe value, but rather the "type" Maybe itself.

Unless I've missed it, there's nothing in the laws that require us to be able to do that. In fact, as written the laws call of on existing instances. As I said in my first post, perhaps this is supposed to be some awkward shorthand for "on the instance or on the constructor function", but even that presupposes an instance whose constructor function we're checking.

I understand that this will often be a useful idiom, but as a requirement of the laws it bothers me. It makes working with abstract Applicatives or abstract Monoids significantly more difficult. Think of writing some tree-traversal concatenation algorithm using an abstract Monoid, It's relatively easy if I know where to find the concat and empty methods. But if emtpy could be in either of two places, and if there's nothing to tell me which one to use if it's in both, do I simply make an arbitrary choice? The laws seem to fail me in this case.

sorry for closing this issue earlier and the typos, I sent this from my phone

I do that all the time. And I do it even from a desktop or laptop, so I have no excuses! 😄

@CrossEye
Copy link
Author

@bergus:

I would propose that all type functions need to (only) be available as methods, on the instances.

That's what I would like to see as well.

@puffnfresh
Copy link
Member

The goal is to make Applicatives and Monoids significantly more useful to abstract over.

Look at how broken the following function is:

function sequence(list) {
  return list.reduce(
    function(a, b) {
      return a.of(function(c) {
        return c.concat([b]);
      }).ap(a);
    },
    list[0].of([])
  );
}

sequence([]);

What happens when we pass in an empty array? We break. Now imagine if we had another function pass us an arbitrary list, we do not know where to get the methods from, but this should be a totally valid use of the function.

Let's demand they also show us where to get them from:

function sequence(ac, list) {
  return list.reduce(
    function(a, b) {
      return ac.of(function(c) {
        return c.concat([b]);
      }).ap(a);
    },
    ac.of([])
  );
}

sequence(Maybe, []);

Now things work!

But now we have to pass dictionaries around. That sucks.

The specification tries to meet both: you can pass an object with the instances on it, or pass a dictionary (by using the constructor property) instead.

This is not fixing you to how to instantiate the object, you can always add your own constructor property. There is nothing special about it other than it's automatically created when using functions as constructors.

I originally required the constructor property because it's the only one which will handle both cases (i.e. functions requiring explicit dictionaries and not) - but lots of people complained about it. Maybe it was just the name people did not like (cause they think it's magic) - would the problem be fixed if we rename the property to instances instead?

Again, the whole point of this project is to enable forms of abstraction which we otherwise do not have.

What should we do?

@SimonRichardson
Copy link
Member

I vote for keeping it the same, if you want to use the magic then you get it for free, else you can make your own dictionary (as said). constructor is just a name at the end of things.

@CrossEye
Copy link
Author

@puffnfresh:

All right, I did get confused by the use of 'constructor', and I think it might confuse others. But that is not the fundamental point. If myAppl.of is a function and myAppl.constructor.of is a function, which one does the spec say I should use to create a value of the same Applicative? To say I can just choose arbitrarily between them doesn't seem to work, because as far as I can tell, the spec does not require that if they both exist they have the same behavior.

The problem does not really manifest itself when working with an Applicative or a Monoid of known type. Presumably then you also know how to construct a new one. But we should be able to write generic functions such as treefold against ADTs and, as I wrote earlier, I don't really see how to do this without knowing how to call the relevant functions.

@SimonRichardson
Copy link
Member

because as far as I can tell, the spec does not require that if they both exist they have the same behavior.

Why does that matter, if the both conform to the laws and pass, who cares?

Fundamentally they should be the same, they should return the same type and value. If one uses a constructor and another one doesn't, it shouldn't matter. As long as it quaks like a duck...

@CrossEye
Copy link
Author

because as far as I can tell, the spec does not require that if they both exist they have the same behavior.

Why does that matter, if the both conform to the laws and pass, who cares?

But that's the point. What if they don't? The spec says that one of them must exist. Let's imagine an implementation of some Monoid has a conforming implementation on the constructor. If my treefold implementation looks for the instance version first, and the instance I use has an empty method that has nothing to do with Monoids, then am I just sunk? How could I know? It's following the laws by having a constructor version, so it's not the implementation that's to blame. My code is doing the best it can. But the spec seems ambiguous here. It really feels as though I can't reliably write algorithms against these ADTs because of this. What am I missing?

@SimonRichardson
Copy link
Member

instance I use has an empty method that has nothing to do with Monoids, then am I just sunk?

Seems so... but then you should know what you're folding. If it's a library you're implementing and how to tell someone it has to conform to said specifications, then they also need to grasp the idea of monoids.

It's following the laws by having a constructor version...

It's not though, it's following the specification, by having a function with the same name, but it's obviously not following the laws. That's the point, the specification has certain names for functions and clashes are going to happen in the real world, you can't control that, but you can say that in order to work with this treefold implementation your monoid must follow the laws for it to work.

@CrossEye
Copy link
Author

I think I'm still confused.

As far as I can tell, if someone were to code this:

function Monoid(empty, concat) {
    function _Monoid(a) {
        if (!(this instanceof _Monoid)) {
            return new _Monoid(a);
        }
        this.value = (arguments.length) ? a : empty;
        return this;
    }
    _Monoid.prototype.concat = function(a) {
        return new _Monoid(concat(this.value, a.value));
    };
    _Monoid1.prototype.empty = function() {
        return new _Monoid();
    };
    return _Monoid;
}

var Add = new Monoid(0, function(a, b) {return a + b;});

Add(3).concat(Add(4)); //=> Add(7)

she would have a perfectly legitimate Monoid. But if she were to then do this:

Add.empty = function() {
    this.value = 0;
}

has she suddenly broken it? If I were to code a treefold in a way that checked the constructor property before the instance one, obviously it would not work. Nonetheless, it still seems quite strange to me that by adding a function to this perfectly good Monoid, one can turn it bad.

But my big concern is in writing code that would work with various implementations. One of the goals of Ramda is to work with various FP libraries in a consistent manner. At the moment, for instance, R.map does not work only on lists, as do similarly-named functions in most libraries. If the object you pass is a list, it will just use its internal list implementation. But if you pass something else that has a map function, it will call that function directly. So Ramda's map function works properly with any FantasyLand-compliant Functor. We would like to extend this notion in various directions, but those functions that are dual on the instance and the constructor are going to make this much more difficult. That's my fundamental concern.

@SimonRichardson
Copy link
Member

Add.empty = function() {
    this.value = 0;
}

You should only work with immutable data. Doing the following mutates the object and causes unspecified results. Maybe that should be explicitly said in the spec?

@CrossEye
Copy link
Author

I do realizethat. That wasn't the point. I was just trying to find something that made sense to me for the name 'empty'. Imagine this instead:

Add.empty = function() {
    throw new TypeError ('Sorry, Charlie');
};

@SimonRichardson
Copy link
Member

No, but you did highlight a very valid point :)

@SimonRichardson
Copy link
Member

One of the goals of Ramda is to work with various FP libraries in a consistent manner.

That would mean that all FP libraries would have to follow the spec and that's a big ask!

If the object you pass is a list, it will just use its internal list implementation. But if you pass something else that has a map function, it will call that function directly.

How do you know that map is working correctly, why do you care? You've provided the implementations for this to happen, it's up to the end user to be happy with the results as you've outlined through Ramda how to get the results.

Functor.prototype.map(f) {
    throw new TypeError('???');
}

How do you solve that one, it's the same situation.

@buzzdecafe
Copy link
Contributor

How do you solve that one, it's the same situation.

not quite the same. In that case ramda calls the only function it knows about, viz. the "instance method", and it throws. Oh well! The difference in the case of the spec is that a compliant implementation may be in another location.

@robotlolita
Copy link
Contributor

she would have a perfectly legitimate Monoid. But if she were to then do this:

Add.empty = function() {
    this.value = 0;
}

has she suddenly broken it?

Yes, that wouldn't be a valid Monoid instance anymore.

As a clarification, try to think about these objects in JS as if they were type classes. We're just relying on the object's dynamic dispatch here because it blends well with the language. But, in truth, we have this:

class Monoid m where
  m empty -> m
  m concat: m -> m 
end

instance Monoid for M where
  a empty = 0
  a concat: b = a + b 
end

Given this, if someone where to do:

a = new M
instanceFor(a).empty = function(){ return 1 }

Then it's obvious that the Monoid laws stop working, beause you've changed the type class.

@CrossEye
Copy link
Author

It's still not clear to me if I'm not getting my point across or if I'm missing something fundamental.

How do you solve that one, it's the same situation.

Well not really. I don't really care about solving that one. If the implementation is not compliant, all bets are off. Ramda might corrupt your data, crash your car, steal your girlfriend, whatever. The point is that because Add has a nullary empty method "on itself or its constructor object" that returns "a value of the same Monoid" as well as following the laws for Semigroup, it is compliant. But I still can't tell how to use it because the spec doesn't say something to the effect of, "if it has such a method on itself then any empty method on its constructor must also follow the same rules and vice versa." Knowing only that this claims to be a Monoid, which one do I call?

One of the goals of Ramda is to work with various FP libraries in a consistent manner.

That would mean that all FP libraries would have to follow the spec and that's a big ask!

Well, we'd only work with ones that were compatible. But FantasyLand compatibility is only part of it. We'd like to be able to integrate with that various immutable data libraries, libraries like Highland, ones like Bacon and RxJs, and even possibly the various Promise libraries, although that might be more of a stretch. We don't expect complete integration. But if the library has objects containing a fairly standard filter function, for instance, then we'd like for you to be able to change from

libObject.filter(fn);

to

R.filter(fn, libObject)

and possibly take advantage of currying and easy composition of such functions.

For these purposes, though, I'm focused on FantasyLand. My trouble is that knowing something complies with the spec doesn't seem to give me enough information about how to use it generically. And that seems a shame.

@joneshf
Copy link
Member

joneshf commented Feb 12, 2015

Do you have a proposed solution? That might help guide the conversation.

@CrossEye
Copy link
Author

@robotlolita:

But it still follows the Semigroup laws and it still "provides an empty method on itself or its constructor object" that "takes no arguments" and returns "a value of the same Monoid,", a method which adheres to the right identity and the left identity. Why is it not a Monoid?

@buzzdecafe
Copy link
Contributor

Do you have a proposed solution? That might help guide the conversation.

I like @bergus 's suggestion:

I would propose that all type functions need to (only) be available as methods, on the instances.

Then it may appear statically on the constructor, or somewhere else, but it must appear on the instance.

@CrossEye
Copy link
Author

@joneshf: I like @bergus' suggestion (#88 (comment)) that "all type functions need to (only) be available as methods, on the instances". But the comment from @puffnfresh (#88 (comment)) makes me realize that there are at least some problems with it. My next best suggestion would be to require them on the instance but note that they may also appear elsewhere. But failing all that, I think the best bet would be to require that if these functions appeared in either place that they have the correct behavior.

@joneshf
Copy link
Member

joneshf commented Feb 12, 2015

I would propose that all type functions need to (only) be available as methods, on the instances.
Then it may appear statically on the constructor, or somewhere else, but it must appear on the instance.

If of or empty is only available on values, you can't have an abstraction like sequence or foldMap though.

@CrossEye
Copy link
Author

If of or empty is only available on values, you can't have an abstraction like sequence or foldMap though.

Something needs to exist, but does it have to be a property of the type or simply a function passed to sequence or foldMap?

I'm still a newbie in Haskell, but if I read this right:

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

It expects the construction of a Monoid from a value to be done from a function that's supplied and not from some intrinsic property of the Monoid. Am I reading it wrong? If not, is there some overriding reason the FantasyLand spec can't make the same sort of assumptions?

@joneshf
Copy link
Member

joneshf commented Feb 12, 2015

If you attempt to implement that for any sum type where one of the choices doesn't have the last type variable (e.g. the Nothing of Maybe a, the [] of [a], or the Left a of Either a b) you can't have a total function without having some sort of dictionary/vtable/constructor/static thing passed in. Precisely because you don't have a value to apply the function to. Haskell takes care of this implicitly, but js has no compiler to do this. You can imagine after some desugaring that function might have the type:

foldMap :: FoldableDict t -> MonoidDict m -> (a -> m) -> t a -> m

If you were to implement that function by hand, the most sensible implementation for Maybe a is:

foldMap f (Just x) = f x
foldMap f Nothing  = mempty

or after desugaring, it might look something like this:

foldMap _ _  f (Just x) = f x
foldMap _ md f Nothing  = mempty md

If the MonoidDict were implemented similar to:

data MonoidDict m = MonoidDict {mempty :: m, mappend :: m -> m -> m}

Here the mempty is being resolved by the compiler based on the implicit Monoid m.

This doesn't even work in js unless you explicitly pass a constructor to the function (like the haskell compiler does after desugaring) for reason similar to what @puffnfresh gave above for sequence.

Of course, if you want to make it more general and work for any Foldable t it gets even worse, since you need two instances.

foldMap f = foldr (\a acc -> f a `mappend` acc) mempty

Again, after desugaring, it might look like:

foldMap fd md f = foldr fd (\a acc -> mappend md (f a) acc) (mempty md)

So the compiler would resolve the foldr function from the implicit Foldable t instance and the mappend and mempty functions from the implicit Monoid m instance.

If you're fine with partial functions and the runtime errors they bring about (hopefully you're not), then it's fine to only require the values to have the functions like of and empty.

@bergus
Copy link
Contributor

bergus commented Feb 14, 2015

@CrossEye: I don't think this should be closed, I think this is a serious issue.
I don't like the idea of "if they are both implemented, they must be the same". I can too easily imagine a Function.prototype.of so any constructor function might inherit an of property that does not adhere to the laws. I would like it much better to use the prototype object for these "type method maps" - that's what they are meant to be in JS, even if it doesn't make sense for nullary functions (that don't use this then).

@robotlolita: Thanks, that read on return-type polymorphism was enlightening. I like your last two code examples (as you say they're more idiomatic), but I think that generic function should be

function empty(t) {
    return t.prototype && t.prototype.empty ? t.prototype.empty() : t.empty();
}

so that you can pass Array to it instead of Array.prototype (or the instance that you don't have).

@puffnfresh
Copy link
Member

I finally remembered the real reason we should be disliking the properties being on the constructor.

Imagine we want to have Monoid for Tuple2. We have to define empty:

function Tuple2(a, b) {
  this.a = a;
  this.b = b;
}

Tuple2.empty = function() {
  // What do we do here?
  return new Tuple2(undefined, undefined);
};

Urgh, empty requires access to the constructors of whatever you want a Tuple2 of!

Really we need something like:

function Tuple2Monoid(a, b) {
  return {
    empty: function() {
      return new Tuple2(a.empty(), b.empty());
    },
    append: function(t1, t2) {
      return new Tuple2(a.append(t1.a, t2.a), b.append(t1.b, t2.b));
    }
  };
}

And then we have to pass that dictionary around. I think at this point we may as well admit to being defeated by JavaScript.

@buzzdecafe
Copy link
Contributor

@CrossEye
Copy link
Author

I can never figure out if I'm missing something or if I'm in some way ahead.

@puffnfresh: Is a type like that really useful? I can imagine instead something like this:

function Monoid(empty, concat) {
    function _Monoid(a) {
        if (!(this instanceof _Monoid)) {
            return new _Monoid(a);
        }
        this.value = (arguments.length) ? a : empty;
    }
    _Monoid.prototype.concat = function(a) {
        return new _Monoid(concat(this.value, a.value));
    };
    _Monoid.empty = function() {
        return new _Monoid();
    };
    return _Monoid;
}

var Tuple2Monoid = function(M1, M2) {
    function _Tuple2Monoid(a, b) {
        if (!(this instanceof _Tuple2Monoid)) {
            return new _Tuple2Monoid(a, b);
        }
        // possibly errors over bad types...
        this.value = (arguments.length > 1) ? [a, b] : empty;
    }
    _Tuple2Monoid.prototype.concat = function(tuple) {
        return new _Tuple2Monoid(
            this.value[0].concat(tuple.value[0]), 
            this.value[1].concat(tuple.value[1])
        );
    };
    _Tuple2Monoid.empty = function() {
        return new _Tuple2Monoid(M1.empty(), M2.empty());
    };
    return _Tuple2Monoid;
}

var Add = new Monoid(0, function(a, b) {return a + b;});
var Multiply = new Monoid(1, function(a, b) {return a * b;});

var AddMultTuple = Tuple2Monoid(Add, Multiply);

var amt = AddMultTuple(Add(3), Multiply(5));
var amt2 = AddMultTuple(Add(4), Multiply(2));

amt.concat(amt2); //=> AddMultTuple(Add(7), Multiply(10))

Such a tuple now has an appropriate type, and it's built around the constructor-style. Of course it's still dynamically typed, and nothing will stop you from trying to do AddMultTuple(Add(3), Add(4)), but if used correctly, such a type seems easy enough to write, and easy enough to use.

@joneshf
Copy link
Member

joneshf commented Feb 15, 2015

@CrossEye I'm confused, what is different there?

@CrossEye
Copy link
Author

@robotlolita:

Thank you for a very informative response. If nothing else, I'm learning a lot here!

Your final example is this version:

Array.prototype.foldMap = function(f, resultMonoid) {
  return this.length === 0? empty(resultMonoid)
  : /* else */ f(this[0]).concat(this.slice(1).foldMap(f, resultMonoid))
}

I'm not quite clear what is meant by resultMonoid as opposed to just monoid, but I'm not sure that's really important.

More importantly, I still don't see how the dictionary works here in the case that the monoid's empty is defined on the instance. Or, more precisely, I don't see the point of it, especially for something like foldMap. Obviously, as with any fold, if we never had to worry about non-empty foldables, then there would not be a reason for the dictionary, since its only purpose here is to provide the empty to start us up. That dictionary in this case would be an instance, right? To my mind, having to have an actual instance on hand pretty well defeats the purpose.

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.

Although I said something different earlier, my take is that it would be better to specify only the dictionary for these. I wish the spec weren't using the word "constructor" for this, as, although it is the most common case, it's still just a pun from the Javascript side. But regardless of how the dictionary is defined, the type is, for better or worse, not just a collection of instances, but also a dictionary containing certain metadata. It would be clearer if the spec said so explicitly.

@CrossEye
Copy link
Author

@joneshf: Am I caught up in the same confusion over naming discussed in #90? When I read @puffnfresh's example, a and b were to my mind instances. My implementation has the types created by Tuple2Monoid being generated out of two Monoid constructor functions / dictionaries. Is that not enough of a difference to matter?

@CrossEye
Copy link
Author

Ok, I think I'm coming to terms with at least one reason why this is bothering me from a mathematical point of view. As far as I can tell, the FantasyLand laws allow for the empty set to be treated as a Monoid!

This is of course a problem for something like foldMap, which is expected to return an actual value of the Monoid in question.

A normal mathematical definition of a Monoid, and similarly the one from Haskell, has to do with a set having an associative binary operation and an identity element. The operation (actually in Semigroup) is no issue. But empty is a real problem.

Definitions from other fields, which actually insist on an identity element don't have this issue. But FantasyLand tries to derive everything from values, or at least to allow you to define your Monoid in that manner.

Here's my reasoning:

My type = E = {}; All these hold trivially since E is empty:

  • ∀a ∈ E, a.equals(a) === true
  • ∀a, b ∈ E, a.equals(b) === b.equals(a)
  • ∀a, b, c ∈ E, If a.equals(b) and b.equals(c), then a.equals(c)

Hence E is a Setoid. No surprise.

And E is a Semigroup, again trivially since E is Empty:

  • ∀a, b, c ∈ E, a.concat(b).concat(c) is equivalent to a.concat(b.concat(c))

But now I have a choice. I can choose to say that this type's empty is defined on my instances, as the spec allows. Then these become trivially true, again since E is empty:

  • ∀m ∈ E, m.concat(m.empty()) is equivalent to m
  • ∀m ∈ E, m.empty().concat(m) is equivalent to m

Hence E is a Monoid.

Am I misinterpreting "A value which has a Monoid must provide an empty method on itself or its constructor object" in some way to allow this?

@CrossEye
Copy link
Author

CrossEye commented Mar 1, 2015

ping

Did I uncover a hole in the spec here? Or is my reasoning off somehow?

Should I try to figure out a solution myself for a PR or does it need additional discussion?

@rpominov
Copy link
Member

Will it be a solution if "or" be simply replaced with "and" in "itself or its constructor"?

@davidchambers
Copy link
Member

I'd say "and" is better than "or". Note that this would require us to bump the spec's version number.

@rpominov
Copy link
Member

Yeah. Btw, changes like that should became easier to make after libraries start depending on fantasy-lang package. NPM will warn if two incompatible versions are used.

@SimonRichardson
Copy link
Member

I think empty in it's current implementation is a hindrance to the spec, without having a type to make empty it's impossible to use empty correctly (see as a perfect example #88 (comment)).

Is it time for this (original contex #97 (comment)):

I'd like to see a rewrite of the spec using explicit dictionaries for everything. My theory is that'd be more useful than the current spec but much less convenient. Definitely happy to do that as Fantasy Land 2 if it actually works, though.

@CrossEye
Copy link
Author

Will it be a solution if "or" be simply replaced with "and" in "itself or its constructor"?

It will plug the hole that allows things that (mathematically) shouldn't be considered Monoids to pass the definition. This might have to have some additional text to insist that the two versions were the same.

But I'm not at certain it's the right thing to do. I think the instance empty (and similarly the instance of) are themselves problematic, trying to invite ill-conceived short-cuts. I think requiring these to be on the constructors/type dictionaries would be plenty.

@SimonRichardson
Copy link
Member

I think requiring these to be on the constructors/type dictionaries would be plenty.

I use to think like this, but I've run into so many situations now, where empty is completely useless.

Consider the following code (more context), I've got no way of knowing what the ? should be, so the spec doesn't even help us out here, in fact it prevents this from happening at all.

Writer.of = function(x) {
  return Writer(() => Tuple2(x, ?.empty()));
};

I'm coming to the conclusion the only way to do this correctly is to pass explicit dictionaries for everything. I actually want to re-write the spec to incorporate this idea.

@SimonRichardson
Copy link
Member

I've been playing around with the idea suggested by @puffnfresh here. It does make it very explicit about the creation and I've only run into one issue and that's the verbosity to set everything up.

// Tuple related types.
const M = nested.Monoid(λ);
const F = nested.Functor(λ);
const S = nested.Setoid(λ);

// Sum Monoid
const SM = Monoid(λ);

exports.nested = {
    'testing': function(t) {
        const tuple = M(SM, SM).empty();
        const mapped = F.map(tuple, (x) => x + 1);
        t.ok(S.equals(Tuple(Sum(0), Sum(1)), mapped));
        t.done();
    }
};

@dead-claudia
Copy link
Contributor

Related: #158

@CrossEye
Copy link
Author

I believe this has been adequately addressed 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

No branches or pull requests