static-land icon indicating copy to clipboard operation
static-land copied to clipboard

Does empty need to be a function?

Open polytypic opened this issue 7 years ago • 6 comments

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.

polytypic avatar Nov 30 '16 20:11 polytypic

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

  • It's consistent with fantasy-land, where it's also a function. Although there are plans to make it value there too.
  • It's a bit simpler for the spec when all we have are functions. Just less kinds of things to think about, not sure how to better say it...
  • @gcanti mentioned in https://github.com/rpominov/static-land/issues/32#issuecomment-258691247 that there will be some issue with Flow if it will be just a value.

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.

rpominov avatar Nov 30 '16 20:11 rpominov

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()?

polytypic avatar Nov 30 '16 23:11 polytypic

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.

rpominov avatar Dec 01 '16 06:12 rpominov

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

gcanti avatar Dec 06 '16 12:12 gcanti

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.

masaeedu avatar May 10 '18 22:05 masaeedu

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.

adit-hotstar avatar Aug 16 '19 06:08 adit-hotstar