A simple case where LiquidHaskell works well on the type "Data.String" but not on the type "Data.Text"
After some playing around, I have found a way you can do this. I don't know of a way that preserves the polymorphism of NoRouteToHimself
, but there is, at least, a way to speak about equality of Data.Text
objects.
The technique is to introduce a denotation measure. That is, a Text
is really just a fancy way of representing a String
, so we should in principle be able to use String
reasoning on Text
objects. So we introduce a measure to get what the Text
represents:
{-@ measure denot :: Tx.Text -> String @-}
When we construct a Text
from a String
, we need to say that the Text
's denotation is the String
we passed in (this encodes injectivity, with denot
playing the role of the inverse).
{-@ assume fromStringL :: s:String -> { t:Tx.Text | denot t == s } @-}
fromStringL = Tx.pack
Now when we want to compare equality of different Text
s in LH, we instead compare their denotations.
{-@ type NoRouteToHimself = v:(_,_) : denot (fst v) /= denot (snd v) @-}
And now we can get the example to pass:
{-@ foo :: NoRouteToHimself @-}
foo :: (Tx.Text, Tx.Text)
foo = (fromStringL "AA", fromStringL "AB")
To use other functions of Data.Text
in LH, one would need to give denotational specifications to those functions. It is some work, but I think it would be a worthwhile thing to do.
I'm curious if there are ways to make this treatment more polymorphic and reusable. I also wonder if we can overload LH's notion of equality so that we don't have to go through denot
. There is much to learn.
Liquid Haskell works by exploiting primitive Haskell constructors. The String
code is sugar for
{-@ strOk :: NoRouteToHimself @-}
strOk :: (String, String)
strOk = (,) ('A':'A':[]) ('A':'B':[])
and Liquid Haskell knows how to unravel / recurse those constructors away. But Data.Text
is not defined in terms of Haskell constructors, rather it uses an opaque conversion function – the -XOverloadedStrings
extension inserts it:
{-@ foo :: NoRouteToHimself @-}
foo :: (Tx.Text, Tx.Text)
foo = (Tx.pack "AA", Tx.pack "AB")
Here, Liquid Haskell doesn't know how Tx.pack
works, whether it yields anything deconstructible in its output. A simpler example where it also doesn't succeed is (without -XOverloadedStrings
)
{-@ foo :: NoRouteToHimself @-}
foo' :: (String, String)
foo' = (reverse "AA", reverse "AB")
To make this work, LH would need to know at least that Tx.pack
and reverse
are injective. I don't know enough about LH to tell if it's possible to achive that. Perhaps forcing it to inline the conversion function would do the trick. Short of that, the only option would be to NF the value and call the actual ==
operator on it – which would work fine in this particular case, but would be impossible for the non-trivial use cases that LH is actually supposed to do.