Specifically, the documentation states “This is to avoid the need for Rank2Types in the definition of Application.”
I do vaguely remember why the Application type cannot be simply Request -> IO Response, the explanation being in a talk about wai, but first thing that comes to my mind is changing the type into this:
type Application = forall r. Request -> (Response -> IO r) -> IO r
Without enabling any extensions, this works on GHC 9.10.3. But seemingly using NoRankNTypes makes it not work, so is that what is being suggested? From the documentation of that extension, Rank2Types used to mean a different restriction, and I also can’t see why this type isn’t a rank-1 type.
when writing this post I did not realise RankNTypes was on by default so my question changed a bit
Would or could a newer version of wai use this? Is there a reason to avoid the extension with it being default? I want to design a generic API for Discord gateway bots, so wai has come to my mind with it
If designing from scratch in 2026, I imagine it would be less controversial to use the higher-rank type. type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived was first defined in wai-3.0.0, released in 2014. Strangely, Network.Wai enables {-# LANGUAGE Rank2Types #-} but the authors appear to have decided on exposing a simpler interface, so it could not have been chosen to broaden support across Haskell compilers. Even the ancient GHC 6.8 (2007) supports arbitrary-rank polymorphism. So the feature was mature and known to wai’s authors. The only reason I can think of is that a simpler interface was chosen to make it easier for users to follow.
You’re technically right and the type forall r. Request -> (Response -> IO r) -> IO r is itself not a higher rank type. However, there are a few nuances:
GHC used to only allow the keyword forall if RankNTypes (or Rank2Types) was enabled. Nowadays you can use ExplicitForAll and write a function signature like this:
foo :: forall r. Request -> (Response -> IO r) -> IO r
Even so, using forall in a type synonym is still syntactically disallowed by GHC even with ExplicitForAll; it requires RankNTypes.
Even if GHC would allow defining the type synonym, most uses of it would be a higher rank type. For example, Application -> Foo would then expand to (forall r. Request -> (Response -> IO r) -> IO r) -> Foo which is a higher rank type. Technically, types like Foo -> Application are not really higher rank because the quantifier appears only to the right of the function arrow, but GHC rejects these anyway probably because of historical reasons mentioned in point 1.
Type inference for higher rank types is undecidable. GHC is pretty good nowadays with ImpredicativeTypes but it is still complicated, and you will probably run into unexpected limitations.
For example the DeepSubsumption extension is a slight change to the type system which had bigger real world implications than anticipated. See the simplify subsumption proposal for examples.
And a clear limitation is that you can’t do things like [Application] (putting applications in a list) if Application is a type synonym with a forall in it, unless you also enable the ImpredicativeTypes extension.
And a clear limitation is that you can’t do things like [Application] (putting applications in a list) if Application is a newtype with a forall in it, unless you also enable the ImpredicativeTypes extension.
Thank, that was a kind of Freudian slip because I think newtypes are the best solution to deal with these cases. You can define a newtype with a forall in it:
newtype Application = MkApplication { serve :: forall a. Request -> (Response -> IO a) -> IO a }
This has much fewer issues with type inference and type checking in my experience. The cost is that you now need to wrap and unwrap this newtype, but ideally you’d hide that under a nice clean API.