[Note: this assumes familiarity with functors.]
Another approach is to use type-level composition as defined in Data.Functor.Compose.
>>> getCompose (fmap (+5) (Compose [Just 1, Just 2, Nothing, Just 3]))
[Just 6,Just 7,Nothing,Just 8]
You can abstract this into a defintion for your maybeX:
-- Wrap, map, and unwrap
maybeX :: (a -> b) -> [Maybe a] -> [Maybe b]
maybeX f = getCompose . fmap f . Compose
(In fact, nothing in the definition assumes Maybe or [], just the restricted type annotation. If you enable the FlexibleContexts extension, you can infer the type (Functor g, Functor f) => (a1 -> a) -> f (g a1) -> f (g a) and use it on arbitrary nested functors.
>>> maybeX (+1) (Just [1,2])
Just [2,3]
>>> maybeX (+1) [[1,2]]
[[2,3]]
>>> maybeX (+1) Nothing -- type (Num a, Functor g) => Maybe (g a), since `Nothing :: Maybe a`
)
The Compose constructor composes the [] and Maybe type constructors into a new type constructor:
>>> :k Compose
Compose :: (k1 -> *) -> (k -> k1) -> k -> *
Compare:
Compose :: (k1 -> *) -> (k -> k1) -> k -> *
(.) :: (b -> c) -> (a -> b) -> a -> c
(The main difference is that (.) can compose any two functions; it appears that Compose and its associated instances require the last higher-kinded value to be applied to a concrete type, one of kind *.)
Applying the data constructor Compose to a list-of-Maybes produces a wrapped value
>>> :t Compose [Nothing]
Compose [Nothing] :: Compose [] Maybe a
As long as the arguments to Compose are themselves instances of Functor (as [] and Maybe are), Compose f g is also a functor, so you can use fmap:
>>> fmap (+5) (Compose [Just 1,Nothing,Just 2,Just 3])
Compose [Just 6,Nothing,Just 7,Just 8]
The Compose value is just a wrapper around the original value:
>>> getCompose $ fmap (+5) (Compose [Just 1,Nothing,Just 2,Just 3])
[Just 6,Nothing,Just 7,Just 8]
In the end, this isn't much different than simply composing fmap directly:
instance (Functor f, Functor g) => Functor (Compose f g) where
fmap f (Compose x) = Compose (fmap (fmap f) x)
The difference is that you can define the type with a function and get its accompanying Functor instance for free, instead of having to hand-code both.