As I read through some sections in History of Haskell, I came across:
However, higher-kinded polymorphism has independent utility: it is entirely possible, and occasionally very useful, to declare data types parameterised over higher kinds, such as:
data ListFunctor f a = Nil | Cons a (f a)
Knowing "basic" ADTs I was a bit puzzled here, my "guess" was that the part in parens suggests a "parametric"/"dynamic" unary data constructor f
? So any data constructor of kind * -> *
that "can accept" type a
? Is my thinking correct or am I misinterpreting the syntax? I know I'm "just guessing" but I'm hopeful to gain a "lay-programmer" intuition on this capability here, some sample scenario needing (or benefiting immensively from) this ;) mostly I can imagine (just not in what exact manner) this allowing more flexibility in those "small embedded versatile recursable config language"-ADTs that Haskell makes such a pleasure to formulate and write evals
for.. close?
In GHCi, :i ListFunctor
on the above gives:
type role ListFunctor representational nominal
data ListFunctor (f :: * -> *) a = Nil | Cons a (f a)
So this seems to be what's "inferred" from the crisper data
declaration.
Solving problems using polymorphism makes your code clearer and easy to maintain. Also, makes us use some of the principles of the object-oriented programming paradigm. When implementing the method intro in each class, we are giving responsibility to each type of class.
That’s the essence of parametric polymorphism. Parametric polymorphism allows for “functions” from types to values. In the example above, chooseValue is a function which (implicitly) takes a type T, and then two arguments of type T, and returns a T.
Here we just used polymorphism. The method intro will be executed according to the type of the object. That is the power of using inheritance and polymorphism to solve problems. If the type is FamilyCustomer, the method intro that will be executed is the one implemented in the class FamilyCustomer.
You may know that in most modern programming languages, functions are first-class values—they can be stored in variables, passed to functions, etc. In most statically-typed programming languages, however, polymorphic functions are not first-class.
Yes, f
can be any unary type constructor.
For instance ListFunctor [] Int
or ListFunctor Maybe Char
are well-kinded.
f
can also be any n-ary type constructor with (n-1) arguments partially applied.
For instance ListFunctor ((->) Bool) Int
or ListFunctor (Either ()) Char
are well-kinded.
The basic kinding system is quite simple. If F :: * -> * -> ... -> *
, then F
expects type arguments. If G :: (* -> *) -> *
, then G
expects any thing of kind * -> *
including unary type constructor and partial applications as the ones shown above. And so on.
A problem which is nicely solved by higher kinds is configuration options. Assume we have a record
data Opt = Opt
{ opt1 :: Bool
, opt2 :: String
-- many other fields here
}
Now, configuration settings can be found in a file and/or passed through the command line and/or in environment variables. During the parsing of all these settings sources, we need to cope with the fact that not all sources define all options. Hence, we need a more lax type to represent subsets of configuration settings:
data TempOpt = TempOpt
{ tempOpt1 :: Maybe Bool
, tempOpt2 :: Maybe String
-- many other fields here
}
-- merge all options in one single configuration, or fail
finalize :: [TempOpt] -> Maybe Opt
...
This is horrible, since it duplicates all the options! We would be tempted to remove the Opt
type, and only use the weaker TempOpt
, to reduce clutter. However, by doing this we will need to use some partial accessor like fromJust
every time we need to access the value of an option in our program, even after the initial configuration handling part.
We can instead resort to higher kinds:
data FOpt f = FOpt
{ opt1 :: f Bool
, opt2 :: f String
-- many other fields here
}
type Opt = FOpt Identity
type TempOpt = FOpt Maybe
-- as before: merge all options in one single configuration, or fail
finalize :: [TempOpt] -> Maybe Opt
...
No more duplication. After we finalize
the configuration settings, we get the static guarantee that settings are always present. We can now use the total accessor runIdentity
to get them, instead of the dangerous fromJust
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With