LiquidHaskell 在类型 "Data.String" 上运行良好但在类型 "Data.Text" 上运行良好的简单情况

A simple case where LiquidHaskell works well on the type "Data.String" but not on the type "Data.Text"

问题

我玩 LiquidHaskell 时非常兴奋,但是,我不知道我需要在多大程度上修改我的原始 Haskell 代码以满足 LiquidHaskell 的要求。

这是一个简单的例子,说明 Liquid 的规范如何适用于 String 类型但不适用于 Text 类型。

对于字符串类型,效果很好

例子

我定义了一个 Liquid 类型,我们说元组的值不能相同:

{-@ type NoRouteToHimself = {v:(_, _) | (fst v) /= (snd v)} @-}

然后,对于 String 类型规范,它运行良好,如下所示:

{-@ strOk :: NoRouteToHimself @-}
strOk :: (String, String)
strOk = ("AA", "AB")

LiquidHaskel 输出 >> 结果:安全

{-@ strBad :: NoRouteToHimself @-}
strBad :: (String, String)
strBad = ("AA", "AA")

LiquidHaskel 输出 >> 结果:不安全

到目前为止一切顺利,让我们为 Text 类型定义相同的函数。

Text类型出错

例子

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as Tx

{-@ foo :: NoRouteToHimself @-}
foo :: (Tx.Text, Tx.Text)
foo = ("AA", "AB")

预期结果:结果:安全

液体Haskell 输出:结果:不安全

 ..Example.hs:102:3-5: Error: Liquid Type Mismatch
  
 102 |   foo = ("AA", "AB")
         ^^^
  
   Inferred type
     VV : {v : (Data.Text.Internal.Text, Data.Text.Internal.Text) | x_Tuple22 v == ?a
                                                                    && x_Tuple21 v == ?b
                                                                    && snd v == ?a
                                                                    && fst v == ?b}
  
   not a subtype of Required type
     VV : {VV : (Data.Text.Internal.Text, Data.Text.Internal.Text) | fst VV /= snd VV}
  
   In Context
     ?b : Data.Text.Internal.Text
      
     ?a : Data.Text.Internal.Text

显然 LiquidHaskell 无法评估这种情况下元组的值。有什么建议吗?

Liquid Haskell 通过利用原始 Haskell 构造函数来工作。 String 代码是

的糖
{-@ strOk :: NoRouteToHimself @-}
strOk :: (String, String)
strOk = (,) ('A':'A':[]) ('A':'B':[])

和 Liquid Haskell 知道如何分解/递归那些构造函数。但是 Data.Text 不是根据 Haskell 构造函数定义的,而是使用不透明的转换函数 – -XOverloadedStrings 扩展插入它:

{-@ foo :: NoRouteToHimself @-}
foo :: (Tx.Text, Tx.Text)
foo = (Tx.pack "AA", Tx.pack "AB")

在这里,Liquid Haskell 不知道 Tx.pack 是如何工作的,它是否在其输出中产生任何可解构的东西。它也没有成功的一个更简单的例子是 (without -XOverloadedStrings)

{-@ foo :: NoRouteToHimself @-}
foo' :: (String, String)
foo' = (reverse "AA", reverse "AB")

为了使这项工作有效,LH 至少需要知道 Tx.packreverse 是单射的。我对 LH 了解不够,无法判断是否有可能实现这一目标。也许强迫它内联转换函数就可以了。除此之外,唯一的选择是 NF 值并在其上调用实际的 == 运算符——这在这种特殊情况下可以正常工作,但对于 LH 实际上是的非平凡用例来说是不可能的应该做的。

经过一番尝试,我找到了一种方法可以做到这一点。我不知道有什么方法可以保留 NoRouteToHimself 的多态性,但至少有一种方法可以谈论 Data.Text 对象的相等性。

技术是引入外延度量。也就是说,Text 实际上只是 表示 一个 String 的奇特方式,所以原则上我们应该能够使用 String 推理Text 个对象。所以我们引入一个度量来得到 Text 代表什么:

{-@ measure denot :: Tx.Text -> String @-}

当我们从 String 构造 Text 时,我们需要说 Text 的外延是我们传入的 String (这编码单射性, denot 起到了相反的作用).

{-@ assume fromStringL :: s:String -> { t:Tx.Text | denot t == s } @-}
fromStringL = Tx.pack

现在,当我们想要比较 LH 中不同 Text 的相等性时,我们改为比较它们的外延。

{-@ type NoRouteToHimself = v:(_,_) : denot (fst v) /= denot (snd v) @-}

现在我们可以让例子通过了:

{-@ foo :: NoRouteToHimself @-}
foo :: (Tx.Text, Tx.Text)
foo = (fromStringL "AA", fromStringL "AB")

要在 LH 中使用 Data.Text 的其他功能,需要对这些功能进行指称说明。这是一些工作,但我认为这是一件值得做的事情。

我很好奇是否有办法使这种处理方式更具多态性和可重用性。我还想知道我们是否可以重载 LH 的平等概念,这样我们就不必经历 denot。有很多东西要学。