fantasyland / static-land

Specification for common algebraic structures in JavaScript based on Fantasy Land
MIT License
772 stars 41 forks source link

Does empty need to be a function? #37

Open polytypic opened 7 years ago

polytypic commented 7 years ago

Currently the empty of Monoid is specified as a nullary function rather than a plain value. Is there a plausible use case that benefits from empty being a function or requires it to be a function?

At the moment, I'm tempted to specify empty as a plain value in a use case of monoid.

rpominov commented 7 years ago

There are some minor reasons for it to be a function:

And there's no much harm in it being a function. I mean I don't see there a big conceptual issue.

Is there a plausible use case that benefits from empty being a function or requires it to be a function?

I'd say I'm not sure there isn't, although I can't present any.

All that said maybe we make that change in the future along with other breaking changes if there will be any.

polytypic commented 7 years ago

Ok. I'll just write down here a couple arguments in favour of using a plain value.

One downside of empty being a function is that it can (and does) have a negative effect on performance. How large the effect is depends on the case. In some cases you can mitigate that by e.g. manually caching the results of calling empty().

In a simple benchmark of foldMapOf with Sum I saw approximately 5% (manually optimized to cache empty()) to 15% (naïve switch from values to nullary functions) performance hit. IOW, I didn't yet find a way to reduce the performance hit to 0 (with Node 6.9.1). empty of Sum returns 0, which is an easy case for a compiler to optimize. In some cases, empty, unless manually optimized, can imply allocating a new object, which is more difficult for a compiler to eliminate. Having to manually optimize is arguably a nuisance. So, use of a function for empty in the spec seems to lead to inefficiency and complexity in every implementation.

Regarding simplicity, a dictionary is a mapping of names to values. In the current Static Land spec the values are just (artificially) limited to functions. I don't see how allowing non-functions would complicate the spec. On the contrary, one could give an arguably simpler specification for Monoid:

Constants

  1. empty :: Monoid m => Type m ~> m

Laws

  1. Right identity: M.concat(a, M.empty) ≡ a
  2. Left identity: M.concat(M.empty, a) ≡ a

(I just roughly deleted characters from the current Monoid spec.)

Regarding Flow Static Land, related experimental libraries in various ML style languages (e.g. exhibit A and exhibit B) don't require empty to be a function. I'm not intimately familiar with Flow. Perhaps @gcanti could elaborate. What would be lost if the parentheses here would be removed? Couldn't this line just be changed to empty: empty()?

rpominov commented 7 years ago

Regarding performance, I think in most cases implementation of a monoid should look like this:

const empty = []

const List = {
  empty() {return empty},
  concat(a, b) {return a.concat(b)},
}

But yea, this is a bit of complication and probably still causes a perf hit, so argument stands.

gcanti commented 7 years ago

Hi @polytypic, sorry for the delay. Besides my comment here (which is admittedly a contrived example) I can't think of critical downsides. If I find some cons I'll make sure to inform you and write here

masaeedu commented 6 years ago

I've been using plain values for empty in this semi-static land compatible library and it works out pretty well, especially when destructuring monoid instances for use in other things.

adit-hotstar commented 5 years ago

I'm in favor of using plain values too. Note that you could always use lazy getters if you need a nullary function.

const List = {
    get empty() {
        return Object.defineProperty(List, "empty", { value: [] }).empty;
    },
    concat: (a, b) => a.concat(b)
};

This is a contrived example, but it shows that nullary functions are equivalent to plain values when used as lazy getters. Hence, you don't sacrifice anything by using plain values.