For example in the following code can I fill the hole to always compute the Nat value corresponding to the type level n parameter?
{-# LANGUAGE DataKinds, KindSignatures, ExplicitForAll #-}
data Nat
= Zero
| Succ Nat
lower :: forall (n :: Nat) . Nat
lower = _
test0 = lower @Zero == Zero
test1 = lower @(Succ Zero) == Succ Zero
I tried using a type class:
class Lower (n :: Nat) where
lower :: Nat
instance Lower Zero where
lower = Zero
instance Lower n => Lower (Succ n) where
lower = Succ (lower @n)
But then every time I want to use lower I need to explicitly require Lower n:
lower' :: forall (n :: Nat) . Lower n => Nat
lower' = lower @n
The short answer is "no".
As noted in the comments, in Haskell, type parameters are erased at runtime. What this actually means is that the runtime behavior of a function depends only on the arguments supplied for its term-level parameters, never (directly) on the arguments supplied for its type-level parameters.
Put more directly, a function is a function of its term-level parameters only, and so the value of a function application is determined by the value of the term-level arguments, irrespective of the type-level arguments. If we call the same function twice with the same term-level arguments but different type-level arguments, the two application will produce the same term-level result.
In practice, this is usually a distinction without a difference. The function:
has runtime behavior that depends only on its term-level parameter
x. If I callid Truemultiple times, it always produces the resultTrue, irrespective of the type-level argumenta. BUT, obviously, if the term-level argument isTrue, then the type-level argument is necessarilyBool, so the question of whetherid Truewould produce the same result with different type-level arguments for parameterais moot.But for your example, the question is not moot. Your function:
must produce a value that depends only on its term-level parameters (of which there are none), and never on its type-level parameter
n. Multiple calls oflowerwith different type-level arguments will still have the same (nullary) term-level arguments, and so the calllowermust always produce the same term-level result. It is therefore impossible forlowerto produceZerofor some type-level argument andSucc Zerofor some other type-level argument.A constraint is a "magical" term-level parameter. Specifically, your
Lower nconstraint is syntactic sugar for a newtype parameter:that exhibits a "coherence" property such that for each possible value for the type-level parameter
n, there is only one valid term-level value of typeCLower n, and Haskell will automatically supply this (only possible) term-level value wherever it is needed.So your type-class version of
lower'is roughly equivalent to:with the appropriate
CLower nargument automatically supplied where needed.This provides a means for the type-level parameter to come pretty close to directly determining runtime behavior. In this situation, it's still the case that
lower'can only depend on its term-level parameterCLower x, but now this term-level parameter has a type (and so a value) that depends on the type-level parametern. If you calllower'with two different type-level arguments, Haskell automatically supplies two different term-level arguments, and the result of these twolower'calls can and will return two different term-level values.If you want to avoid a constraint and still get two different results for two different type-level arguments, you must introduce some other term-level parameter on which
lowercan depend. Trivially, you could write something like:Now,
lowerhas a term-level parameter (an integer), so it can produce different results for different term-level arguments:This is not a very smart approach, of course, because the term-level parameter
m :: Intis completely unrelated to the type-leveln :: Natparameter. If you want your term-level parameter to somehow relate to the type-level parametern, it must have a type that depends onn, like so:and you'll need to make
Somethinginto a GADT like:so you can write a body for
lowerthat acts on different term-level arguments that in turn have been determined by the type-level argument:This isn't very ergonomic for your use case because when it comes time to actually invoke
lowerwith a particular type-level argument, you need to supply the matching term-level argument explicitly:or, since Haskell can infer the type-level argument automatically, just:
and then you have to ask yourself why not just use the term-level
Zerodirectly, if you still need to pass a term-levelSZeroaround to calllower.Still, term-level parameters that take the form of either constraints or GADTs are really the only mechanism to have a type-level parameter affect the runtime behavior of a function in the way you want.