Generalising ($) like Control.Category generalises (.)
$
applies morphisms to values. The concept of a value seems trivial, but actually, general categories need to have no such notion. Morphisms are values (arrow-values... whatever), but objects (types) needn't actually contain any elements.
However, in many categories, there is a special object, the terminal object. In Hask, this is the ()
type. You'll notice that functions () -> a
are basically equivalent to a
values themselves. Categories in which this works are called well-pointed. So really, the fundamental thing you need for something like $
to make sense is
class Category c => WellPointed c where
type Terminal c :: *
point :: a -> Terminal c `c` a
unpoint :: Terminal c `c` a -> a
Then you can define the application operator by
($) :: WellPointed c => c a b -> a -> b
f $ p = unpoint $ f . point p
The obvious instance for WellPointed
is of course Hask itself:
instance WellPointed (->) where
type Terminal c = ()
--point :: a -> () -> a
point a () = a
--unpoint :: (() -> a) -> a
unpoint f = f ()
The other well-known category, Kleisli
, is not an instance of WellPointed
as I wrote it (it allows point
, but not unpoint
). But there are plenty of categories which would make for a good WellPointed
instance, if they could properly be implemented in Haskell at all. Basically, all the categories of mathematical functions with particular properties (LinK, Grp, {{•}, Top}...). The reason these aren't directly expressible as a Category
is that they can't have any Haskell type as an object; newer category libraries like categories or constrained-categories do allow this. For instance, I have implemented this:
instance (MetricScalar s) => WellPointed (Differentiable s) where
unit = Tagged Origin
globalElement x = Differentiable $ \Origin -> (x, zeroV, const zeroV)
const x = Differentiable $ \_ -> (x, zeroV, const zeroV)
As you see, the class interface is actually a bit different from what I wrote above. There isn't one universally accepted way of implementing such stuff in Haskell yet... in constrained-categories
, the $
operator actually works more like what Cirdec described.
There are two abstractions used for things like this in Haskell, one usings Arrow
s and the other Applicative
s. Both can be broken down into smaller parts than those used in base
.
If you go in the Arrow
direction and break down the capabilities of Arrow
s into component pieces, you'd have a separate class for those arrows that are able to lift arbitrary functions into the arrow.
class ArrowArr a where
arr :: (b -> c) -> a b c
This would be the opposite of ArrowArr
, arrows where any arbitrary arrow can be dropped to a function.
class ArrowFun a where
($) :: a b c -> (b -> c)
If you just split arr
off of Arrow
you are left with arrow like categories that can construct and deconstruct tuples.
class Category a => ArrowLike a where
fst :: a (b, d) b
snd :: a (d, b) b
(&&&) :: a b c -> a b c' -> a b (c,c')
If you go in the Applicative
direction this is a Copointed
"Applicative
without pure
" (which goes by the name Apply
).
class Copointed p where Source
copoint :: p a -> a
class Functor f => Apply f where
(<.>) :: f (a -> b) -> f a -> f b
When you go this way you typically drop the Category
for functions and instead have a type constructor C a
representing values (including function values) constructed according to a certain set of rules.