【问题标题】:Programmatic type annotations in HaskellHaskell 中的编程类型注释
【发布时间】:2023-03-26 13:06:01
【问题描述】:

在元编程时,将有关您的程序已知但在 Hindley-Milner 中无法推断的类型的信息传递给 Haskell 的类型系统可能是有用的(或必要的)。在 Haskell 中是否有一个库(或语言扩展等)提供了执行此操作的工具(即编程类型注释)?

假设您正在使用异构列表(例如,使用 Data.Dynamic 库或存在量化实现)并且您希望将列表过滤为沼泽标准、同质类型的 Haskell 列表。你可以写一个函数像

import Data.Dynamic
import Data.Typeable

dynListToList :: (Typeable a) => [Dynamic] -> [a]
dynListToList = (map fromJust) . (filter isJust) . (map fromDynamic)

并使用手动类型注释调用它。例如,

foo :: [Int]
foo = dynListToList [ toDyn (1 :: Int)
                    , toDyn (2 :: Int)
                    , toDyn ("foo" :: String) ]

这里foo是列表[1, 2] :: [Int];这工作得很好,你又回到了 Haskell 的类型系统可以做它的事情的坚实基础上。

现在假设您想做很多相同的事情,但是 (a) 在您编写代码时,您不知道调用 dynListToList 生成的列表的类型需要是什么,但是 (b ) 你的程序确实包含了解决这个问题所需的信息,只是 (c) 它不是类型系统可以访问的形式。

例如,假设您从异构列表中随机选择了一个项目,并且您想按 那个 类型过滤列表。使用Data.Typeable 提供的类型检查工具,您的程序拥有执行此操作所需的所有信息,但据我所知——这是问题的本质——无法将其传递给类型系统。这里有一些伪 Haskell 说明了我的意思:

import Data.Dynamic
import Data.Typeable

randList :: (Typeable a) => [Dynamic] -> IO [a]
randList dl = do
    tr <- randItem $ map dynTypeRep dl
    return (dynListToList dl :: [<tr>])  -- This thing should have the type
                                         -- represented by `tr`

(假设 randItem 从列表中随机选择一个项目。)

如果return 的参数没有类型注释,编译器会告诉你它有一个“模糊类型”并要求你提供一个。但是您不能提供手动类型注释,因为类型在写入时未知(并且可能会有所不同);然而,类型 在运行时是已知的——尽管它是类型系统无法使用的形式(这里,所需的类型由值 tr 表示,TypeRep——见Data.Typeable了解详情)。

伪代码:: [&lt;tr&gt;] 是我想要发生的魔法。有没有办法以编程方式为类型系统提供类型信息?也就是说,你的程序的值中包含类型信息?

基本上,我正在寻找一个具有(伪)类型 ??? -&gt; TypeRep -&gt; a 的函数,该函数采用 Haskell 类型系统未知类型的值和 TypeRep 并说:“相信我,编译器,我知道我正在做。这东西有这个TypeRep所代表的价值。” (请注意,这不是unsafeCoerce 所做的。)

或者有什么完全不同的东西让我在同一个地方?例如,我可以想象一个允许分配给类型变量的语言扩展,例如启用范围类型变量的扩展的增强版本。

(如果这是不可能的或非常不切实际的——例如,它需要将一个完整的类似 GHCi 的解释器打包到可执行文件中——请尝试解释原因。)

【问题讨论】:

  • import Data.Maybe, dynListToList = mapMaybe fromDynamic.
  • @AntalS-Z—很好的简化。
  • 正如您所说,在您的示例中,类型信息仅在运行时可用,因此您无法在类型检查阶段(在编译时发生)获得它。没有办法实现你的伪代码示例,因为它没有任何意义。
  • 我很难跟踪这个问题中有多少建筑危险信号。如果您需要这样做,您可能会受益于在走廊里招呼一位经验丰富的 Haskeller,并与他们聊一两个小时以了解如何重新开始。
  • @DanielWagner:我希望走廊里有一位经验丰富的 Haskeller!无论如何,这是对 Haskell 类型系统局限性的探索。但确实有一些问题需要做类似这样的事情,或者(更糟糕的是,从架构的角度来看)围绕类型系统进行完整的最终运行。

标签: haskell types metaprogramming type-systems hindley-milner


【解决方案1】:

不,你不能这样做。总而言之,您正在尝试编写依赖类型的函数,而 Haskell 不是依赖类型的语言。您无法将 TypeRep 值提升为真实类型,因此无法写下所需函数的类型。为了更详细地解释这一点,我首先要说明为什么您对randList 类型的表述方式并不真正有意义。然后,我将解释为什么你不能做你想做的事。最后,我将简要提及一些关于实际操作的想法。

存在主义

randList 的类型签名不能代表您想要的意思。记住 Haskell 中的所有类型变量都是通用量化的,它读取

randList :: forall a. Typeable a => [Dynamic] -> IO [a]

因此,我有权将其称为randList dyns :: IO [Int] 任何我想要的地方;我必须能够为 all a 提供返回值,而不仅仅是为 some a。将其视为一个游戏,调用者可以选择a,而不是函数本身。您想要说的(这不是有效的 Haskell 语法,尽管您可以通过使用存在数据类型将其转换为有效的 Haskell1)更像是 p>

randList :: [Dynamic] -> (exists a. Typeable a => IO [a])

这保证列表的元素是some 类型a,它是Typeable 的一个实例,但不一定any 这种类型。但即使这样,你也会遇到两个问题。首先,即使你可以构建这样一个列表,你能用它做什么?其次,事实证明你一开始就无法构建它。

既然您对存在列表元素的所有了解都是Typeable 的实例,那么您可以用它们做什么呢? Looking at the documentation,我们看到只有两个函数2 采用Typeable 的实例:

因此,关于列表中元素的类型,您所知道的只是您可以在它们上调用typeOfcast。由于我们永远无法对它们做任何有用的事情,我们的存在主义也可能是(同样,不是有效的 Haskell)

randList :: [Dynamic] -> IO [(TypeRep, forall b. Typeable b => Maybe b)]

如果我们将typeOfcast 应用于列表的每个元素、存储结果并丢弃现在无用的存在类型的原始值,这就是我们得到的结果。显然,此列表中的TypeRep 部分没有用。列表的后半部分也不是。由于我们回到了一个普遍量化的类型,randList 的调用者再次有权要求他们获得一个Maybe Int、一个Maybe Bool 或一个Maybe b 对于任何(可输入的)b他们的选择。 (事实上​​,他们比以前稍微强大了一点,因为他们可以将列表的不同元素实例化为不同的类型。)但是他们无法弄清楚他们正在转换什么类型除非他们已经知道 - 您仍然丢失了您试图保留的类型信息。

即使抛开它们没有用的事实,您也无法在这里构造所需的存在类型。当您尝试返回存在类型列表 (return $ dynListToList dl) 时会出现错误。你打电话给dynListToList的具体类型是什么?回想一下dynListToList :: forall a. Typeable a =&gt; [Dynamic] -&gt; [a];因此,randList 负责选择 which a dynListToList 将使用。但它不知道选择哪个a;再次,这就是问题的根源!因此,您尝试返回的类型未指定,因此不明确。3

依赖类型

好的,那么使这种存在主义有用(并且可能)?嗯,我们实际上有更多的信息:我们不仅知道一些 a,我们还有它的TypeRep。所以也许我们可以把它打包:

randList :: [Dynamic] -> (exists a. Typeable a => IO (TypeRep,[a]))

不过,这还不够好; TypeRep[a] 根本没有链接。这正是您想要表达的:某种方式来链接TypeRepa

基本上,你的目标是写类似的东西

toType :: TypeRep -> *

这里,*是所有类型中的那种;如果您以前没有见过种类,那么它们就是类型,什么类型是值。 * 对类型进行分类,* -&gt; * 对单参数类型构造函数进行分类等(例如,Int :: *Maybe :: * -&gt; *Either :: * -&gt; * -&gt; *Maybe Int :: *。)

有了这个,你可以写(再一次,这段代码不是有效的 Haskell;事实上,它实际上与 Haskell 只是有一点相似之处,因为你无法在 Haskell 的类型系统中编写它或类似的东西):

randList :: [Dynamic] -> (exists (tr :: TypeRep).
                           Typeable (toType tr) => IO (tr, [toType tr]))
randList dl = do
  tr <- randItem $ map dynTypeRep dl
  return (tr, dynListToList dl :: [toType tr])
    -- In fact, in an ideal world, the `:: [toType tr]` signature would be
    -- inferable.

现在,您承诺的事情是正确的:不是存在某种对列表元素进行分类的类型,而是存在一些 TypeRep 以便其对应的类型对​​列表中的元素进行分类。如果你能做到这一点,你就会被设置。但是在 Haskell 中写 toType :: TypeRep -&gt; * 是完全不可能的:这样做需要依赖类型的语言,因为 toType tr 是一个依赖于值的类型。

这是什么意思?在 Haskell 中,值依赖于其他值是完全可以接受的。这就是函数。例如,值head "abc" 取决于值"abc"。同样,我们有类型构造函数,因此类型依赖于其他类型是可以接受的;考虑Maybe Int,以及它如何依赖于Int。我们甚至可以拥有取决于类型的值!考虑id :: a -&gt; a。这确实是一系列函数:id_Int :: Int -&gt; Intid_Bool :: Bool -&gt; Bool 等。我们拥有哪一个取决于a 的类型。 (真的,id = \(a :: *) (x :: a) -&gt; x;虽然我们不能用 Haskell 编写这个,但我们可以使用一些语言。)

然而,至关重要的是,我们永远不能拥有依赖于值的类型。我们可能想要这样的东西:想象Vec 7 Int,长度为7 的整数列表类型。这里,Vec :: Nat -&gt; * -&gt; *:一个类型,其第一个参数必须是 Nat 类型的值。但是我们不能在 Haskell 中编写这种东西。4 支持这一点的语言称为 dependently-typed(我们可以像上面那样写 id );示例包括 CoqAgda。 (此类语言通常兼作证明助手,通常用于研究工作而不是编写实际代码。依赖类型很难,使它们对日常编程有用是一个活跃的研究领域。)

因此,在 Haskell 中,我们可以先检查有关类型的所有内容,丢弃所有这些信息,然后编译仅引用值的内容。事实上,这正是 GHC 所做的;因为在 Haskell 中我们永远无法在运行时检查类型,所以 GHC 在编译时擦除所有类型而不改变程序的运行时行为。这就是为什么unsafeCoerce 易于实现(操作上)并且完全不安全的原因:在运行时,它是无操作的,但它取决于类型系统。因此,像toType 这样的东西完全不可能在 Haskell 类型系统中实现。

事实上,正如您所注意到的,您甚至不能写下所需的类型并使用unsafeCoerce。对于某些问题,您可以摆脱它;我们可以写下函数的类型,但只能通过作弊来实现。这正是fromDynamic 的工作原理。但正如我们在上面看到的,在 Haskell 中甚至没有一个很好的类型可以解决这个问题。想象中的toType函数可以让你给程序一个类型,但是你甚至不能写下toType的类型!

现在怎么办?

所以,你不能这样做。你应该做什么?我的猜测是您的整体架构对于 Haskell 来说并不理想,尽管我还没有看到它; TypeableDynamic 实际上并没有在 Haskell 程序中出现那么多。 (正如他们所说,也许您“说的是带有 Python 口音的 Haskell”。)如果您只有一组有限的数据类型要处理,您也许可以将事物捆绑到一个普通的旧代数数据类型中:

data MyType = MTInt Int | MTBool Bool | MTString String

然后你可以写isMTInt,然后使用filter isMTInt,或者filter (isSameMTAs randomMT)

虽然我不知道它是什么,但您可能有一种方法可以 unsafeCoerce 解决这个问题。但坦率地说,这不是一个好主意,除非你真的、真的、真的、真的、真的、真的知道你在做什么。即便如此,也可能不是。如果您需要unsafeCoerce,您就会知道,这不仅仅是为了方便。

我真的同意Daniel Wagner's comment:你可能会想从头开始重新考虑你的方法。不过,再一次,因为我还没有看到你的架构,我不能说这意味着什么。如果你能提炼出一个具体的困难,也许还有另一个 Stack Overflow 问题。


1 如下所示:

{-# LANGUAGE ExistentialQuantification #-}
data TypeableList = forall a. Typeable a => TypeableList [a]
randList :: [Dynamic] -> IO TypeableList

但是,由于这段代码无论如何都无法编译,我认为用exists 写出来更清晰。

2 从技术上讲,还有一些其他看起来相关的函数,例如toDyn :: Typeable a =&gt; a -&gt; DynamicfromDyn :: Typeable a =&gt; Dynamic -&gt; a -&gt; a。然而,Dynamic 或多或少是围绕Typeables 的存在包装,依赖typeOfTypeReps 知道何时到unsafeCoerce(GHC 使用一些特定于实现的类型和unsafeCoerce,但是你可以这样做,除了dynApply/dynApp)可能,所以toDyn 没有做任何新的事情。而fromDyn 并不真正期望它的类型为a 的参数;它只是cast 的包装。这些功能以及其他类似功能不提供任何仅typeOfcast 无法提供的额外功能。 (例如,返回Dynamic 对您的问题不是很有用!)

3 要查看实际的错误,您可以尝试编译以下完整的 Haskell 程序:

{-# LANGUAGE ExistentialQuantification #-}
import Data.Dynamic
import Data.Typeable
import Data.Maybe

randItem :: [a] -> IO a
randItem = return . head -- Good enough for a short and non-compiling example

dynListToList :: Typeable a => [Dynamic] -> [a]
dynListToList = mapMaybe fromDynamic

data TypeableList = forall a. Typeable a => TypeableList [a]

randList :: [Dynamic] -> IO TypeableList
randList dl = do
  tr <- randItem $ map dynTypeRep dl
  return . TypeableList $ dynListToList dl -- Error!  Ambiguous type variable.

果然,如果你尝试编译这个,你会得到错误:

SO12273982.hs:17:27:
    Ambiguous type variable `a0' in the constraint:
      (Typeable a0) arising from a use of `dynListToList'
    Probable fix: add a type signature that fixes these type variable(s)
    In the second argument of `($)', namely `dynListToList dl'
    In a stmt of a 'do' block: return . TypeableList $ dynListToList dl
    In the expression:
      do { tr <- randItem $ map dynTypeRep dl;
           return . TypeableList $ dynListToList dl }

但问题的重点是,您不能“添加修复这些类型变量的类型签名”,因为您不知道自己想要什么类型。

4 主要是。 GHC 7.4 支持将类型提升到种类和种类多态性;见section 7.8, "Kind polymorphism and promotion", in the GHC 7.4 user manual。这不会使 Haskell 依赖类型化——像 TypeRep -&gt; * 这样的例子仍然没有5——但是你可以通过使用非常富有表现力的类型来编写 Vec看起来 喜欢价值观。

5 从技术上讲,您现在可以写下 看起来 具有所需类型的内容:type family ToType :: TypeRep -&gt; *。但是,这需要提升 kind TypeReptype,而不是 type @ 的 value 987654435@;此外,您仍然无法实现它。 (至少我不这么认为,而且我看不出你会怎么做——但我不是这方面的专家。)但在这一点上,我们还很遥远。

【讨论】:

  • randList :: [Dynamic] -&gt; [Typeable] 你可能是指randList :: [Dynamic] -&gt; [TypeRep],但即使这样也有点虚伪,因为typeOf 不是你可以对Typeable 值做的唯一事情,你也可以@987654440 @ 他们。此外,如果你有randList :: [Dynamic] -&gt; (exists (tr :: TypeRep). Typeable (toType tr) =&gt; IO [toType tr]),你可能实际上也想要结果中的 typerep,不是吗?否则你只是说“结果是可表示的类型”,而不是“结果是可表示的类型,这就是它的样子”
  • Ben: 1. 是的,我的意思是[TypeRep],谢谢。我会尽快解决这个问题…… 2. 我想办法解决你关于cast 的精彩观点。 (愚蠢的unsafeCoerce 使我的论点无效:-)) 3. 我想过拥有exists … (tr, IO [toType tr]),但我认为exists 是一个依赖总和,然后可以对其进行模式匹配以提取信息。由于参数化,Haskell 无法对存在类型执行此操作,但我们显然已经将参数化抛在了脑后 :-) 不过,如果它更清楚,我可以更改它;这可能是个好主意。
  • 我不会在后一点上声称权威 - 如果您确信自己知道自己在做什么,那么您可以保持原样:)
  • @BenMillwood:固定,固定,再固定。 (同时进行了一些澄清/错字修复。)感谢您在我的回答中发现这些错误。 (包括与 tr 的存在对;清晰度比可能在技术上正确更重要,而且我并没有在依赖类型中声称拥有很大的权威!)
【解决方案2】:

您观察到的是 TypeRep 类型实际上并没有携带任何类型级别的信息。只有术语级别的信息。这是一种耻辱,但是当我们知道我们关心的所有类型构造函数时,我们可以做得更好。例如,假设我们只关心Ints、列表和函数类型。

{-# LANGUAGE GADTs, TypeOperators #-}

import Control.Monad

data a :=: b where Refl :: a :=: a
data Dynamic where Dynamic :: TypeRep a -> a -> Dynamic
data TypeRep a where
    Int   :: TypeRep Int
    List  :: TypeRep a -> TypeRep [a]
    Arrow :: TypeRep a -> TypeRep b -> TypeRep (a -> b)

class Typeable a where typeOf :: TypeRep a
instance Typeable Int where typeOf = Int
instance Typeable a => Typeable [a] where typeOf = List typeOf
instance (Typeable a, Typeable b) => Typeable (a -> b) where
    typeOf = Arrow typeOf typeOf

congArrow :: from :=: from' -> to :=: to' -> (from -> to) :=: (from' -> to')
congArrow Refl Refl = Refl

congList :: a :=: b -> [a] :=: [b]
congList Refl = Refl

eq :: TypeRep a -> TypeRep b -> Maybe (a :=: b)
eq Int Int = Just Refl
eq (Arrow from to) (Arrow from' to') = liftM2 congArrow (eq from from') (eq to to')
eq (List t) (List t') = liftM congList (eq t t')
eq _ _ = Nothing

eqTypeable :: (Typeable a, Typeable b) => Maybe (a :=: b)
eqTypeable = eq typeOf typeOf

toDynamic :: Typeable a => a -> Dynamic
toDynamic a = Dynamic typeOf a

-- look ma, no unsafeCoerce!
fromDynamic_ :: TypeRep a -> Dynamic -> Maybe a
fromDynamic_ rep (Dynamic rep' a) = case eq rep rep' of
    Just Refl -> Just a
    Nothing   -> Nothing

fromDynamic :: Typeable a => Dynamic -> Maybe a
fromDynamic = fromDynamic_ typeOf

以上所有内容都很标准。有关设计策略的更多信息,您需要阅读 GADT 和单例类型。现在,您要编写的函数如下;字体看起来有点愚蠢,但请耐心等待。

-- extract only the elements of the list whose type match the head
firstOnly :: [Dynamic] -> Dynamic
firstOnly [] = Dynamic (List Int) []
firstOnly (Dynamic rep v:xs) = Dynamic (List rep) (v:go xs) where
    go [] = []
    go (Dynamic rep' v:xs) = case eq rep rep' of
        Just Refl -> v : go xs
        Nothing   ->     go xs

在这里,我们选择了一个随机元素(我掷了一个骰子,结果为 1),并仅从动态值列表中提取具有匹配类型的元素。现在,我们可以对标准库中的普通无聊的旧Dynamic 做同样的事情;但是,我们无法以一种有意义的方式使用TypeRep。我现在证明我们可以这样做:我们将对TypeRep 进行模式匹配,然后使用TypeRep 告诉我们的特定类型的封闭值。

use :: Dynamic -> [Int]
use (Dynamic (List (Arrow Int Int)) fs) = zipWith ($) fs [1..]
use (Dynamic (List Int) vs) = vs
use (Dynamic Int v) = [v]
use (Dynamic (Arrow (List Int) (List (List Int))) f) = concat (f [0..5])
use _ = []

请注意,在这些等式的右侧,我们使用包装值在不同的具体类型TypeRep 上的模式匹配实际上是在引入类型级别的信息。

【讨论】:

  • 这是一个非常出色的解决您问题的方法,由一位出色的计算机科学家提供,所以 +1,但是......不要这样做!您真的需要混合这些异构类型吗?你真的不能处理你想放在一个列表中的几种类型的抽象数据类型吗?那不是更优雅吗?如果没有,您是否不能将这些类型实例化为某个类型类,然后您就不需要在异构列表中再次挖掘该类型?静态类型是你的朋友。静态类型带走了一个痛苦的世界。只有在必要时才放弃静态类型。
  • @AndrewC FWIW 我完全同意你的看法。
【解决方案3】:

您需要一个根据运行时数据选择不同类型的值返回的函数。好的,太好了。但是一个类型的整个目的是告诉你可以对一个值执行什么操作。当你不知道函数会返回什么类型时,你对它返回的值做什么?您可以对它们执行哪些操作?有两种选择:

  • 您想读取类型,并根据它的类型执行一些行为。在这种情况下,您只能满足事先已知的有限类型列表,主要是通过测试“是这种类型吗?然后我们执行此操作......”。这在当前的Dynamic 框架中很容易实现:只需返回Dynamic 对象,使用dynTypeRep 过滤它们,然后将fromDynamic 的应用程序留给任何想要使用您的结果的人。此外,如果您不介意在生产者代码而不是消费者代码中设置类型的有限列表,则很可能没有Dynamic:只需为每种类型使用带有构造函数的ADT,data Thing = Thing1 Int | Thing2 String | Thing3 (Thing,Thing)。如果可能的话,后一种选择是迄今为止最好的选择。
  • 您想要执行一些适用于一系列类型的操作,其中可能有一些您还不知道,例如通过使用类型类操作。这更棘手,在概念上也很棘手,因为您的程序不允许根据某些类型类实例是否存在来更改 行为 - 这是类型类系统的一个重要属性,引入新实例可以进行程序类型检查或停止类型检查,但它不能改变程序的行为。因此,如果您的输入列表包含不适当的类型,您就不会抛出错误,所以我真的不确定您能做的任何事情实际上并不涉及在某个时候回退到第一个解决方案。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多