The ability to treat something as a first-class construct means you can abstract it out. You can abstract 5 out of 5 * 5 * 5
using a (first-order) function f 5
.
f :: Int -> Int
f x = x * x * x
Higher-order functions can abstract out multiplication f (*)
:
f :: (Int -> Int -> Int) -> Int
f (·) = (x · x) · x
This does not perfectly abstract the multiplication out because we are passing in a monomorphic multiplication function f ((*) :: Int -> Int -> Int)
rather than the constrained polytype:
(*) :: forall a. Num a => a -> a -> a
To fully abstract the multiplication function, we need to be able to pass a polymorphic function as an argument: Without Rank-N types, we could not properly abstract out polymorphic functions! The type of the argument is copy-pasted from the type of (*)
.
f :: (forall a. Num a => a -> a -> a) -> Int
f mul = mul @Int (mul @Int 5 5) 5
Passing a polymorphic function as an argument enables us to instantiate it at different types. run print
abstracts out the polymorphic print
function, used at print :: Int -> IO ()
and print :: Bool -> IO ()
.
run :: (forall a. Show a => a -> IO ()) -> IO ()
run pr = do
pr @Int 10
pr @Bool False
And you can abstract the sequencing as well, such that run print
equals runOf (*>) print
.
runOf :: (forall a b. IO a -> IO b -> IO b)
-> (forall a. Show a => a -> IO ())
-> IO ()
runOf (·>) pr =
sequ @() @()
(pr @Int 10)
(pr @Bool False)
This ability is important because polymorphic functions are one of the most common features in Haskell. To perform a dictionary translation of any type class with quantification in the methods, you need rank-2 types. You could say that Functor abstracts a over the polymorphic function fmap
.
type Functor :: (Type -> Type) -> Constraint
class Functor f where
fmap :: forall a a'. (a -> a') -> (f a -> f a')
type DFunctor :: (Type -> Type) -> Type
data DFunctor f = DFunctor
{ d_fmap :: forall a a'. (a -> a') -> (f a -> f a') }
As such you can replace a Functor f
constraint with a polymorphic function argument.
ffmapNot :: (forall a a'. (a -> a') -> (f a -> f a'))
-> (f (f Bool) -> f (f Bool))
ffmapNot fmap = fmap (fmap not)
This is the idea behind the lens library. To parameterise methods from type classes, and passing them as a compositional
type Traversal :: Type -> Type -> Type -> Type -> Type
type Traversal as bs a b = forall f. Applicative f => (a -> f b) -> (as -> f bs)
Now we can define toList = toListOf traverse
(more accurately, toListof foldMap
).
-- in actuality, `toListOf` has a more general type
toListOf :: forall as a. Traversal as as a a -> as -> [a]
toListOf trav as = trav' (:) as [] where
trav' :: (a -> [a] -> [a]) -> (as -> [a] -> [a])
trav' = coerce do trav @(Const (Endo [a]))
toListOf traverse
:: Traversable t
=> t a -> [a]
toListOf (traverse . traverse)
:: Traversable t1
=> Traversable t2
=> t1 (t2 a) -> [a]
runST :: (forall s. ST s a) -> a
keeps two separateST
computations from having the sames
. This prevents you from leakingSTRef
s outside theST
monad. – Kingwooddata Ext where Ext :: a -> Ext
). Probably also other constructs that could be expressed with Rank 2 Types? – Bucketdata (:~:) a b where Refl :: a :~: a
) using rank 2 types -newtype (:~:) a b = Refl (forall p . p a -> p b)
- and a similar such encoding is used inlens
. – Tortoise