To improve terminology a little bit, I propose to call these functors "rigid" instead of "bindable". The motivation for saying "rigid" will be explained below.
Definition
A functor f
is called rigid if it has the inject
method as shown. Note that every functor has the eject
method.
class (Functor f) => Rigid f where
inject :: (a -> f b) -> f(a -> b)
eject :: f(a -> b) -> a -> f b
eject fab x = fmap (\ab -> ab x) fab
The law of "nondegeneracy" must hold:
eject . inject = id
Properties
A rigid functor is always pointed:
instance (Rigid f) => Pointed f where
point :: t -> f t
point x = fmap (const x) (inject id)
If a rigid functor is applicative then it is automatically monadic:
instance (Rigid f, Applicative f) => Monad f where
bind :: f a -> (a -> f b) -> f b
bind fa afb = (inject afb) <*> fa
The property of being rigid is not comparable (neither weaker nor stronger) than the property of being monadic: If a functor is rigid, it does not seem to follow that it is automatically monadic (although I don't know specific counterexamples for this case). If a functor is monadic, it does not follow that it is rigid (there are counterexamples).
Basic counterexamples of monadic functors that are not rigid are Maybe
and List
. These are functors that have more than one constructor: such functors cannot be rigid.
The problem with implementing inject
for Maybe
is that inject
must transform a function of type a -> Maybe b
into Maybe(a -> b)
while Maybe
has two constructors. A function of type a -> Maybe b
could return different constructors for different values of a
. However, we are supposed to construct a value of type Maybe(a -> b)
. If for some a
the given function produces Nothing
, we don't have a b
so we can't produce a total function a->b
. Thus we cannot return Just(a->b)
; we are forced to return Nothing
as long as the given function produces Nothing
even for one value of a
. But we cannot check that a given function of type a -> Maybe b
produces Just(...)
for all values of a
. Therefore we are forced to return Nothing
in all cases. This will not satisfy the law of nondegeneracy.
So, we can implement inject
if f t
is a container of "fixed shape" (having only one constructor). Hence the name "rigid".
Another explanation as to why rigidity is more restrictive than monadicity is to consider the naturally defined expression
(inject id) :: f(f a -> a)
where id :: f a -> f a
. This shows that we can have an f-algebra f a -> a
for any type a
, as long as it is wrapped inside f
. It is not true that any monad has an algebra; for example, the various "future" monads as well as the IO
monad describe computations of type f a
that do not allow us to extract values of type a
- we shouldn't be able to have a method of type f a -> a
even if wrapped inside an f
-container. This shows that the "future" monads and the IO
monad are not rigid.
A property that is strictly stronger than rigidity is distributivity from one of E. Kmett's packages. A functor f
is distributive if we can interchange the order as in p (f t) -> f (p t)
for any functor p
. Rigidity is the same as being able to interchange the order only with respect to the "reader" functor r t = a -> t
. So, all distributive functors are rigid.
All distributive functors are necessarily representable, which means they are equivalent to the "reader" functor c -> t
with some fixed type c
. However, not all rigid functors are representable. An example is the functor g
defined by
type g t = (t -> r) -> t
The functor g
are not equivalent to c -> t
with a fixed type c
.
Constructions and examples
Further examples of rigid functors that are not representable (i.e. not "distributive") are functors of the form a t -> f t
where a
is any contrafunctor and f
is a rigid functor. Also, the Cartesian product and the composition of two rigid functors is again rigid. In this way, we can produce many examples of rigid functors within the exponential-polynomial class of functors.
My answer to What is the general case of QuickCheck's promote function? also lists the constructions of rigid functors:
f = Identity
- if
f
and g
are both rigid then the functor product h t = (f t, g t)
is also rigid
- if
f
and g
are both rigid then the composition h t = f (g t)
is also rigid
- if
f
is rigid and g
is any contravariant functor then the functor h t = g t -> f t
is rigid
One other property of rigid functors is that the type r ()
is equivalent to ()
, i.e. there is only one distinct value of the type r ()
. This value is point ()
, where point
is defined above for any rigid functor r
. (I have a proof but I will not write it here, because I could not find an easy one-line proof.) A consequence is that a rigid functor must have only one constructor. This immediately shows that Maybe
, Either
, List
etc. cannot be rigid.
Connection with monads
If f
is a monad that has a monad transformer of the "composed-outside" kind, t m a = f (m a)
, then f
is a rigid functor.
The "rigid monads" are possibly a subset of rigid functors because construction 4 only yields a rigid monad if f
is also a rigid monad rather than an arbitrary rigid functor (but the contravariant functor g
can still be arbitrary). However, I do not have any examples of a rigid functor that is not also a monad.
The simplest example of a rigid monad is type r a = (a -> p) -> a
, the "search monad". (Here p
is a fixed type.)
To prove that a monad f
with the "composed-outside" transformer t m a = f (m a)
also has an inject
method, we consider the transformer t m a
with the foreign monad m
chosen as the reader monad, m a = r -> a
. Then the function inject
with the correct type signature is defined as
inject = join @t . return @r . (fmap @m (fmap @f return @m))
with appropriate choices of type parameters.
The non-degeneracy law follows from the monadic naturality of t
: the monadic morphism m -> Identity
(substituting a value of type r
into the reader) is lifted to the monadic morphism t m a -> t Id a
. I omit the details of this proof.
Use cases
Finally, I found two use cases for rigid functors.
The first use case was the original motivation for considering rigid functors: we would like to return several monadic results at once. If m
is a monad and we want to have fbind
as shown in the question, we need f
to be rigid. Then we can implement fbind
as
fbind :: m a -> (a -> f (m b)) -> f (m b)
fbind ma afmb = fmap (bind ma) (inject afmb)
We can use fbind
to have monadic operations that return more than one monadic result (or, more generally, a rigid functor-ful of monadic results), for any monad m
.
The second use case grows out of the following consideration. Suppose we have a program p :: a
that internally uses a function f :: b -> c
. Now, we notice that the function f
is very slow, and we would like to refactor the program by replacing f
with a monadic "future" or "task", or generally with a Kleisli arrow f' :: b -> m c
for some monad m
. We, of course, expect that the program p
will become monadic as well: p' :: m a
. Our task is to refactor p
into p'
.
The refactoring proceeds in two steps: First, we refactor the program p
so that the function f
is explicitly an argument of p
. Assume that this has been done, so that now we have p = q f
where
q :: (b -> c) -> a
Second, we replace f
by f'
. We now assume that q
and f'
are given. We would like to construct the new program q'
of the type
q' :: (b -> m c) -> m a
so that p' = q' f'
. The question is whether we can define a general combinator that will refactor q
into q'
,
refactor :: ((b -> c) -> a) -> (b -> m c) -> m a
It turns out that refactor
can be constructed only if m
is a rigid functor. In trying to implement refactor
, we find essentially the same problem as when we tried to implement inject
for Maybe
: we are given a function f' :: b -> m c
that could return different monadic effects m c
for different b
, but we are required to construct m a
, which must represent the same monadic effect for all b
. This cannot work, for instance, if m
is a monad with more than one constructor.
If m
is rigid (and we do not need to require that m
be a monad), we can implement refactor
:
refactor bca bmc = fmap bca (inject bmc)
If m
is not rigid, we cannot refactor arbitrary programs. So far we have seen that the continuation monad is rigid, but the "future"-like monads and the IO
monad are not rigid. This again shows that rigidity is, in a sense, a stronger property than monadicity.
Traversable
, but can't check right now – IndemonstrableTraversable
(e.g. you can easily traverseConst
), but it does seem related. – AromaTraversable
in some sense.sequenceA
would be able to yank->
to the outside of aTraversable
(using(->) r
'sApplicative
instance) but not the other way round. IOWsequenceA :: Traversable t => t (a -> b) -> a -> t b
– Apocalypseinject
impliespoint
, first we set a = F t ininject
and getinject :: (F t -> F t) -> F(F t -> t)
. We then apply this to the identity morphismid :: F t -> F t
. We obtain a (naturally defined) value ofF(F t -> t)
. Now, we apply the "strength" of the functor:x -> F y -> F (x,y)
and project this to obtain the morhpismx -> F y -> F x
. Now we set x = t and y = F t -> t in this morphism. We obtaint -> F(F t -> t) -> F t
. Since we already have a natural value ofF (F t -> t)
, we thus obtain a natural transformationt -> F t
. – ExistenceF (a->b) -> a -> F b
holds for any functor F, not only for traversable. – Existencepoint x = fmap (const x) (inject id)
? – Gallenzinject
exists. – ExistenceMonad
and in fact any such functorF
is equivalent to the function functor(Log F ->)
, whereLog
basically counts the number of times thatt
appears inF t
, which must be fixed and >0. But I don't have a proof for this, and in fact I doubt thatLog
would be actually computable, at least not in Haskell types. It might be possible mathematically, but I wouldn't be surprised if it required the axiom of choice or something. – Aromainject id :: IO (IO a -> a)
. That would be very dangerous, effectively providing access tounsafePerformIO
in pure code with the mild requirement that eventually this is being called from IO (which always happens, since we start frommain
). Basically we only have to domain = do upIO <- inject id ; print (pureF upIO 12) ; ...
to allow the purely typedpureF
to run side-effects. Scary. – HomogeneousIdentity
and(->) a
. So I suspect it's much stronger thanMonad
. – Griegopromote
and notinject
. I found a wider class of examples within exponential-polynomial functors, as I detailed in my question. You can construct these functors from Identity, by taking pairs, and by mapping from a contrafunctor. – ExistenceF = Maybe
,inject _ = Nothing
, or any other functorF
withF Void
nonempty? Surely you want some kind of condition oninject
. – Alidisinject
instances for nontrivial functors. Perhapseject . inject = id
or vice versa, whereeject :: F (a->b) -> a -> F b
. – ExistenceDistributive f
gets youdistribute :: Functor g => g (f a) -> f (g a)
, which from a Haskell type standpoint is more powerful than your function. I don't know if it satisfies your category-theoretic laws. EveryRepresentable
functor is distributive, and the documentation says the other way round holds mathematically. – UncircumcisedF t = (t -> r) -> t
example,eject . inject
is the identity butinject . eject
isn't the identity. – Alidiseject . inject
andinject . eject
are identity. The non-identity appears only asinject . eject
and only for "bindable" functors F of the form F t = A t -> G t where A is a contrafunctor and G is bindable. The reason is that the chain of morphismsA (a->t) -> A t -> A (a->t)
does not yield identity, whileA t -> A (a->t) -> A t
does. – Existenceeject . inject = id
1) "bindable" is strictly stronger than monad, and strictly weaker than "distributive". 2) specific examples of bindable and non-bindable functors, as well as bindable that is not distributive. – ExistenceLog f = forall a. f a -> a
? – PeekabooLog
will not always work as a "logarithm". It's a weird type function that does not have any of the properties of the usual logarithm except for a single example: iff a = r -> a
thenLog f = r
. Ordinary logarithm has properties such aslog(a*b) = log(a)+log(b)
, which thisLog
does not have. – ExistenceLog F ->
is isomorphic toF
(for the class ofF
under consideration). Secondly, it's not clear to me that it doesn't have the property you claim (where we useProduct f g a = Product (f a) (g a)
andSum f g a = Sum (Either (f a) (g a))
, of course). – PeekabooLog
show properties very different from what you would expect. 1) We haveLog (Sum F G) = Product (Log F) (Log G)
instead oflog(a*b) = log(a)+log(b)
. 2) Defining a functorg
astype g a = (Int, a, a)
, what would you expectLog g
to be? Calculations showLog g = Int -> Bool
. 3) What would you expectLog Maybe
to be? Calculations show thatLog Maybe
is a recursive type equivalent to natural numbers,data Nat = One | Succ Nat
. I'd say all of these examples show quite unexpected results, showing thatLog
is not at all similar to a logarithm. – ExistenceLog Maybe
is void. I mixed it up withLog h
whereh
is defined bytype h a = (Maybe a) -> a
. For that type constructor,Log h
is equivalent to the natural numbers typeNat
defined bydata Nat = One | Succ Nat
. – Existence(a -> (b, b)) -> (a, a) -> (b, b)
? Does it throw away some data? Maybe all the instances are obvious to the folks in the discussion, but it would be helpful for the slowpokes among us if you could just explicitly write out the instances you're suggesting in your question/answer @winitzki. – CreosolWhatever f => (f a, f a)
is not an instance I probably know what this is, but it's hard to say without knowing what the actual instance should look like. – Creosol