8
votes

My knowledge of category theory isn’t very good. So please bear with me.

I’ve been reading Monads Made Difficult

and saw the following definition.

class (Category c, Category d) => Functor c d t where
  fmap :: c a b -> d (t a) (t b)

from that site I read that the Functor type in Haskell’s prelude is really an Endofunctor. (where both category c and d in the above class are Hask)

After reading through the page, I was wondering. Would Haskell be better suited for meta-programming if it used real Functors and not just Endofunctors?

say we have the following (the Js stands for Javascript)

data Js a where
  litInt :: Integer -> Js Integer
  litString :: String -> Js String
  var :: String -> Js a
  lam :: String -> Js b -> Js (a -> b)
  let :: String -> Js a -> Js b -> Js b
  ap :: JsFunc a b -> Js a -> Js b

type JsFunc a b = Js (a -> b)

now back to the definition of Functor above

class (Category c, Category d) => Functor c d t where
  fmap :: c a b -> d (t a) (t b)

we can have c be JsFunc, d be Hask and have t be Js

this will give us a Functor for Js. That I could not do using Haskell’s Endofunctor.

I could not find anything for the Apply or Applicative type-classes defined in the same way as this Functor. Could the same be done with those too?

And correct me if I’m wrong. But the same can not be done for Monad, because it really an instance of an Endofunctor?

3

3 Answers

3
votes

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).

3
votes

According to this mailing list post, applicative functors are known in the math literature as "weakly symmetric lax monoidal functors". A (brief, not careful) look at the nlab page for lax monoidal suggests that this concept is also parameterized by two categories, so you can probably extend this parametric functor class to a parametric applicative class with some work.

As you say, monads are endofunctors, so they can't be parameterized by two categories. But the one category they have as a parameter need not be Hask; so one could give a parameterized monad as well, like

class Functor c c f => Monad c f where
    return :: c a (f a)
    join :: c (f (f a)) (f a)

Then we can use a sort of standard trick to define bind:

(=<<) :: Monad c m => c a (m b) -> c (m a) (m b)
(=<<) f = join . fmap f

The (.) operation here is the composition from the Category class, not function composition from Prelude.

2
votes

we can have c be JsFunc, d be Hask and have t be Js

Well, no we can't exactly since JsFunc is an unsaturated type synonym, so we can't instantiate a variable (c) to it.

You could create a newtype

newtype JsCat a b = JsCat (Js (a -> b))

and then create a Functor JsCat (->) Js like you say.

However, this doesn't give you anything new since if we expand what you wanted the type of fmap to be, it becomes

fmap :: JsFunc a b -> (->) (Js a) (Js b) -- or
fmap :: Js (a -> b) -> Js a -> Js b

which is none other than an Apply instance for Js. So, I'm not sure why you want to make this into a functor when it already fits into an existing abstraction.