Open polytypic opened 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.
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:
empty :: Monoid m => Type m ~> m
M.concat(a, M.empty) ≡ a
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()
?
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.
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
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.
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.
Currently the
empty
ofMonoid
is specified as a nullary function rather than a plain value. Is there a plausible use case that benefits fromempty
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.