Closed masaeedu closed 5 years ago
I'm afraid I'm gonna have to give this a š simply because it contains the word "functor". We don't have constraints (let alone constraint kinds) in JS, and I think this opens us up to a lot of misinterpretation. For example, Promise
isn't a functor, and it's something I have to explain fairly regularly - I think saying "it's an rfunctor" implies that rfunctor is somehow related to functor, which (in JS) it isn't.
I see where you're coming from, but I'm already worried when we start talking about promises or sets as "lawful functors/applicatives/monads", because they're simply not - as soon as you introduce constraints, you un-introduce "functor" :(
@i-am-tom It depends on what you're talking about when you refer to the overloaded term "functor". If you're talking about the category theoretic definition of a functor, it is indeed a valid functor over a restricted subcategory of all possible types. When Haskell users say "functor", they are using a colloquial shortening of "functor over the category of all Haskell types". This is not the only conceivable functor in the world if you're familiar with the underlying mathematical definition.
I'm afraid I'm gonna have to give this a š simply because it contains the word "functor"
All that said, if the objection here is only to the naming, I'd be happy with any synonym you can think of to express the concept of functor over a subcategory of all types.
Just a brief comment for now - I remember trying to write an RFunctor in rmonad, and deciding it was too complicated/ugly. I can't remember the details but I think the constraints got quite hard to manage in practice.
@i-am-tom Ok, so here's a hypothetical RFunctor
instance for Promise
s:
const Fn = (() => {
const id = x => x
const compose = f => g => a => f(g(a))
const pipe = fs => fs.reduce((g, f) => a => f(g(a)))
return { id, compose, pipe }
})()
const Prm = (() => {
// Misc
const equals = p1 => p2 => p1.then(x => p2.then(y => Promise.resolve(x === y)))
const isThenable = a => typeof a.then === "function"
// RFunctor
// Promise is a functor over non-thenable types
const suitable = a => !isThenable(a)
const rmap = f => p => p.then(f)
return { equals, isThenable, suitable, rmap }
})()
const Err = (() => {
// fail :: Error -> ā„
const fail = e => { throw e }
// ensure :: Error -> (a -> Boolean) -> a -> a | ā„
const ensure = e => f => a => f(a) ? a : fail(e)
return { fail, ensure }
})()
const asFunctor = ({ suitable, rmap }) => {
const err = new TypeError("Unsuitable value")
const ensureSuitability = Err.ensure(err)(suitable)
const map = f => rmap(Fn.pipe([ensureSuitability, f, ensureSuitability]))
return { map }
}
// The instance whose lawfulness is in question
const { map } = asFunctor(Prm)
const test = async () => {
// Identity law
// fmap id = id
{
const p = ... // for any promise
await Prm.equals(p)(map(Fn.id)(p)) || Err.fail("Identity law violated")
}
// Composition law
// fmap (g . f) = fmap g . fmap f
{
const { f, g } = ... // for any two morphisms
const p = ... // and any promise (extensional equality)
const p1 = map(Fn.compose(f)(g))(p)
const p2 = Fn.compose(map(f))(map(g))(p)
await Prm.equals(p1)(p2) || Err.fail("Composition law violated")
}
}
test().then(_ => console.log("success!"))
I can't really formally prove that the map
in the code above isn't illegal, but showing that it is illegal, as you say, should be fairly simple. If for some choice of p
, f
, and g
, we get the error "Identity law violated" or "Composition law violated", map
does not form a valid functor. Here's an example with some selected values of p
, f
, and g
https://jsfiddle.net/z22z8rLc/.
If some choices of p
, f
and g
instead result in an "Unsuitable value" TypeError
, this says nothing about the legality of the functor, since it is the same class of error as passing a -> b
to traverse
when b
is not Applicative
; a type error. This sort of thing is unavoidable in the absence of a type checker, and our options are to either pretend to be a type checker ourselves (throwing the TypeError
), or to simply call it "unspecified behavior" and leave it at that. In both cases, the programmer needs to understand that their code is illegal and fix it; the spec isn't responsible for producing correct results from incorrect usage.
In case you're wondering, with a type checker (e.g. TypeScript) you can easily assert the non-thenability constraint statically, and thereby entirely prevent code that passes illegal morphisms from compiling.
/shrug I've said my bit: I worry about misinterpretation. Ultimately, though, this is up to the collaborators within this repository, so I'm the wrong person to be talking to :sweat_smile:
Functor is currently described as:
The fact that there's no constraints on
a
orb
means this is a functor over the category of all types in JS: it maps a morphism between any two typesa -> b
to a morphismf a -> f b
(and implicitly, any typea
to the typef a
).In some cases, a type may exhibit the characteristics of being a legal functor over some universe of types, but not behave as a legal functor over every conceivable type.
For example,
Promise#then
can be used to form a legal functor over non-"thenable" types, and those types only. Consider the following implementation:While
Promise
does not form a legal functor over morphisms between all types, as shown above, it certainly does form a functor over types that satisfy a constraint (in this case,x => !isThenable(x)
).If the Fantasy Land spec allowed formalization of "functor over the category of some types", many more types naturally occurring in JS could be instantiated as lawful functors/applicatives/monads and benefit from code that works with these abstract classes.
RFunctor
could hypothetically look something like this:The existing
Functor
class would simply beRFunctor
for a choice of constraint that accepts all types.The specification has a choice between simply making the behavior of
rmap
with respect to non-suitable morphisms undefined, or requiring implementers to provide asuitable :: a -> Boolean
implementation which will be used to produce type errors for invalid morphisms.Prior art:
Not sure if @hsenag is active on here, but maybe he has some thoughts.