Can a Functor / Applicative be tied to one specific type or structure?

Viewed 199

I’m trying to understand the applicative typeclass, and in particular the <*> function. Now I see its type signature is f (a -> b) -> f a -> f b and I take it that f is a functor, which I think of as some kind of structure wrapping some data. It appears to me that f must handle generic types, specifically it must be possible for f to have the parameterized types a, b, and in fact it must also support a -> b.

If my understanding is correct, then what we are doing is working with a typeclass f that is initially intended to wrap some data, like say a list of strings or a tree containing file buffers or whatever random thing we might want. But f must not be committed to any one type, because not only must it handle such a type but it is also required to handle functions from the data to the other data. Thus if we had an example implementation of <*> which contained

Just2 f <*> j = fmap f j

the way to read this is that j is some kind of data inside of a Just2. f is a function which maps data to data. And f is wrapped in a Just2.

Is all of that right? Fundamentally my question is: Must anything applicative be so generic that it can always simultaneously handle arbitrary data and also functions from data to data? Or is there some way that you could have an applicative such that the only data it allows inside is, say, lists?

3 Answers

Yes, your understanding is largely correct. In particular, any specific Applicative, say one named Foo, has an associated specialization of the function pure with type signature:

pure :: a -> Foo a

that must work for any type a selected by the caller, such as:

> pure 10 :: Foo Int
> pure length :: Foo (String -> Int)

So, whatever Foo is, it has to be able to "handle" any provided type without limitations because pure can technically be applied to any type without limitations.

One cautionary note, though. The idea that a functor f "wraps" data, so that f Int is somehow a "container" of Int values, can be a helpful intuition and is often literally correct (e.g., lists, trees, etc.), but it's not always strictly true. (Some counterexamples include the functors IO, (->) r, and Const b, which "contain" values in a very different sense than real containers.)

For "regular" Functors and Applicatives, you're right; they need to be able to handle values of any type. This is known as parametric polymorphism. If you have a type that you think is almost a Functor except that it can't do that, then consider the MonoFunctor typeclass from the mono-traversable package. It's the same idea as Functor, except with a single valid element type baked in. I'm not aware of any packages that have a monomorphic equivalent to Applicative. I think this is because <*> uses values of 3 different types inside the same container, so it doesn't have a good monomorphic analogue.

From your question, it sounds like there could be many different Applicative typeclasses. But there is only one; therefore it has to be generic.

The Applicative typeclass is defined by its functions (<*> and pure) and by the laws these functions need to adhere to. In any Haskell codebase, there can be exactly one definition only. But there can be many types that have an instance of the Applicative typeclass. You can define your own instances with the instance declaration and by defining the required functions. The compiler will not check that your definitions abide by the functor laws, though - this is up to you to ensure.

Typeclasses like Functor, Applicative and Monad do not specify a data type; they don't say that you need a "list-like" type, or a "box-like" type, event though lists and other containers like Either do have instances of these type classes. Any type that you can equip with the required functions in such a way that the Applicative laws hold becomes an Applicative.

It is often helpful to think of containers or boxes. But you need to stretch that intuition once you use Applicative instances of types like functions; e.g., a -> r.

Compared with interfaces in OO-languages, typeclasses are more powerful because you can define some data type to be an instance of a typeclass even if you do not have access to the source code of the type itself.

Related