errors/InvalidInstanceHead.md
InvalidInstanceHead ErrorAs of 0.11.6, the InvalidInstanceHead error has been added to the
purescript compiler. The error can be produced using the following:
module InvalidInstanceHead where
class ClassWithInvalidInstanceHead (x :: # Type)
instance FailingExample :: ClassWithInvalidInstanceHead ()
...
Normally, when you see this error, it's because you're trying to write an
instance of a given class that pattern matches on some row type. The
issue here is that there isn't really a sensible way to match on row. There
are, however, ways to get around this limitation.
With the arrival of purescript-typelevel-prelude
2.3.0 came the introduction of RowToList and its related classes. The
basic idea is that you can convert a row type to a type-level cons list of its
entries. For example:
-- ROW LIST
RowToList () Nil
RowToList (a :: A) (Cons "a" A Nil)
RowToList (a :: A, b :: B) (Cons "a" A (Cons "b" B Nil))
The RowToList constraint allows us to derive, from a row type, the RowList
that complements it. The keys of the RowList are alphabetically ordered, which
allows us to manipulate row types in a deterministic way. If your instance heads
pattern match on RowList instead of # Type, this error goes away.
Let's imagine a class that we use to assert that a record is homogenous. Or, in simpler terms, all the fields in the record have the same type.
class IsHomogenousRowType (rowType :: # Type)
Now, as we've seen earlier, we can't just pattern-match on this in its current
state; we need to convert it to a RowList. How do we do that?
instance isHomogenousImpl
:: ( RowToList rowType rowList
, IsHomogenousRowList rowList a
)
=> IsHomogenousRecord rowType
Here, we say that, "if you can find me a RowList representation for my record
that satisfies IsHomogenousRow, then this record is good to go". Notice how
we're "pattern matching" on the left side of RowToList - we don't know what
la is, but we trust the compiler to iron out that little detail for us.
Now we implement IsHomogenousRowList. The type parameter list is the RowList
we want to check for homogeneously-typed values, which all have type a.
class IsHomogenousRowList (list :: RowList) a | list -> a
Because we're now dealing with a RowList, we can pattern match and avoid those
pesky errors about instance heads. Our functional dependency here says that, "if
you can give me the type of list, I can always give you the type of a". The
first example of this is a special case:
instance isHomogenousRowListImplEmpty
:: IsHomogenousRowList Nil Unit
We just need to pick a type to use when we're dealing with an empty row; unit is inoffensive enough, so we'll go with that. Next up, what happens if we only have one entry?
instance isHomogenousRowListImplNonEmpty
:: IsHomogenousRowList (Cons k a Nil) a
When we have a singleton object, the instance is trivial; the record is always homogenous. All we're doing here is saying that the row is homogenous if we can find a type that matches the type of the single key. Easy!
Finally, we have to deal with the other cases. This can be generalised:
instance isHomogenousRowListImplReallyNonEmpty
:: IsHomogenousRowList la a
=> IsHomogenousRowList (Cons k a la) a
Here, we match the row as Cons k a la. If la is a homogenous row of type a
and a is the type of our row head, then we're all good! Success! Notice we end
up with two base cases here: an empty record and a singleton. Why? Well, if we
had only the empty one, it would only work for records where all fields were of
type Unit. Think about why this is.
At this point, everything would look as though it should work, right? We can write a little helper function to that class stuff so we can use it in pratice:
validateHomogeneity
:: forall ra
. IsHomogenousRecord ra
=> Record ra
-> Record ra
validateHomogeneity = id
Now, if we call validateHomogeneity on our record, it will be checked at
compile time. With RowToList, we recover all the ability to pattern match
on row types that we would otherwise have lost.
If you'd like to play around with the examples above, you can use TryPureScript to play with it in an interactive Gist.