Well, first –
... if it used real Functors and not just Endofunctors ...
Haskell functors are real functors. Only, the Functor
class doesn't allow any kind of general functors, but only the specific ones that are endofunctors on Hask.
Indeed, non-endo–functors are interesting; mathematicians use them all the time. And while as you say, there can't be non-endofunctor monads, an analogue to Applicative
is possible: that class is really just a nice Haskell interface for monoidal functors, which can be defined between different categories. Looks about like this:
class (Functor r t f, Category r, Category t) => Monoidal r t f where
pureUnit :: () `t` f ()
fzipWith :: (a, b) `r` c -> (f a, f b) `t` f c
To actually use this anywhere as conveniently as the standard Applicative
class though, you'll need a bit more than just the Monoidal
class. This has been tryied in a couple of libraries. I also wrote one. But, well... it doesn't exactly look beautiful...
class (Functor f r t, Cartesian r, Cartesian t) => Monoidal f r t where
pureUnit :: UnitObject t `t` f (UnitObject r)
fzipWith :: (ObjectPair r a b, Object r c, ObjectPair t (f a) (f b), Object t (f c))
=> r (a, b) c -> t (f a, f b) (f c)
class (Monoidal f r t, Curry r, Curry t) => Applicative f r t where
-- ^ Note that this tends to make little sense for non-endofunctors.
-- Consider using 'constPure' instead.
pure :: (Object r a, Object t (f a)) => a `t` f a
(<*>) :: ( ObjectMorphism r a b
, ObjectMorphism t (f a) (f b), Object t (t (f a) (f b))
, ObjectPair r (r a b) a, ObjectPair t (f (r a b)) (f a)
, Object r a, Object r b )
=> f (r a b) `t` t (f a) (f b)
(<*>) = curry (fzipWith $ uncurry id)
I don't know if any of these frameworks would allow you to write the applicative JS code you want in a practical way. Possibly, but actually I rather suspect it would get very messy. Also note that, while you now have applicatives floating around, this doesn't mean you can actually do what's normally done with Hask applicatives in Js code – on the contrary, you'd actually need to wrap those applicatives as transformers around the Js
type.
You might want to consider abandoning “JS values” as such completely, and only write in a category/arrow paradigm instead. I.e., turn the definition around:
data JsFunc a b where
litInt :: Integer -> JsFunc () Integer
litString :: String -> JsFunc () String
var :: String -> JsFunc () a
lam :: String -> JsFunc () b -> JsFunc a b
let :: String -> JsFunc () a -> JsFunc () b -> JsFunc () b
ap :: JsFunc a b -> JsFunc () a -> JsFunc () b
type Js = JsFunc ()
here, we use ()
as the terminal object of the JsFunc
category – which makes the Js a ≡ JsFunc () a
arrows equivalent to what we normally call values (at least if we don't consider strictness semantics). If you go that route, pretty much everything needs to be written point-free, but there's syntax to pretend otherwise, and you can nicely incorporate functors and even monads (as Kleisli arrows).