【问题标题】:Check whether a type is an instance of Show in Haskell at runtime?在运行时检查一个类型是否是 Haskell 中 Show 的实例?
【发布时间】:2021-12-15 20:58:06
【问题描述】:

假设我在 Haskell 中有一个简单的数据类型来存储一个值:

data V a = V a

我想让 V 成为 Show 的实例,而不管 a 的类型。如果 a 是 Show 的实例,则 show (V a) 应返回 show a 否则应返回错误消息。或者在 Pseudo-Haskell 中:

instance Show (V a) where
    show (V a) = if a instanceof Show
                   then show a
                   else "Some Error."

如何在 Haskell 中实现这种行为?

【问题讨论】:

  • Haskell 是一种完全类型擦除的语言;在运行时,内存分配的结构不包含可用于恢复其类型或这些类型实现的类的标记。因此,没有像 Java 或类似语言提供的 instanceof 运算符。 (在某些情况下,还有一些更高级的技术可以用于类似的运行时类型反射,但如果你是初学者,你应该首先坚持基础!)
  • 这种行为无法实现,因为从哲学上讲,每个类型都在每个类中。当然,如果编译器无法find您尝试show 的某种类型的Show 实例,则会出现错误;但这在概念上被理解为“你忘记编写必要的实例”而不是“你试图显示一个不可显示的类型”。类型类是开放的,任何人以后都可以为某些库类定义实例。编译器在编译库时不可能证明这不会发生!
  • 也就是说:这种行为可以用overlapping instances 来模拟,这被认为有点丑陋(甚至不安全)的黑客行为。也许更符合这个想法的是closed type families,尽管它们不允许您实现该显示实例。
  • 你通过写instance Show a => Show (V a) where ...来实现这个行为。那么“Some Error”就变成了编译时类型错误。
  • 一些与@leftaroundabout的评论相关的链接okmij.org/ftp/Haskell/typeEQ.htmlwiki.haskell.org/GHC/AdvancedOverlap

标签: haskell


【解决方案1】:

正如我在评论中所说,分配在内存中的运行时对象在 Haskell 程序中没有类型标签。因此,没有像 Java 这样的通用 instanceof 操作。

考虑以下内容的含义也很重要。在 Haskell 中,第一个近似值(即忽略一些初学者不应该过早处理的花哨的东西),所有运行时函数调用都是单态的。即,编译器直接或间接地知道可执行程序中每个函数调用的单态(非泛型)类型。即使您的 V 类型的 show 函数具有泛型类型:

-- Specialized to `V a`
show :: V a -> String  -- generic; has variable `a`

...如果不直接或间接地告诉编译器每个调用中a 的确切类型,您实际上无法编写一个在运行时调用该函数的程序。比如:

-- Here you tell it directly that `a := Int`
example1 = show (V (1 :: Int)) 

-- Here you're not saying which type `a` is, but this just "puts off" 
-- the decision—for `example2` to be called, *something* in the call
-- graph will have to pick a monomorphic type for `a`.
example2 :: a -> String
example2 x = show (V x) ++ example1

从这个角度来看,希望你能发现你所问的问题:

instance Show (V a) where
    show (V a) = if a instanceof Show
                   then show a
                   else "Some Error."

基本上,由于 a 参数的类型在编译时对于任何对 show 函数的实际调用都是已知的,因此在运行时测试此类型没有意义——您可以在编译时对其进行测试!一旦你掌握了这一点,你就会被引导到 Will Sewell 的建议:

-- No call to `show (V x)` will compile unless `x` is of a `Show` type.
instance Show a => Show (V a) where ...

编辑: 更有建设性的答案可能是这样的:您的 V 类型需要是多个案例的标记联合。这确实需要使用GADTs 扩展:

{-# LANGUAGE GADTs #-}

-- This definition requires `GADTs`.  It has two constructors:
data V a where
  -- The `Showable` constructor can only be used with `Show` types.
  Showable   :: Show a => a -> V a
  -- The `Unshowable` constructor can be used with any type.
  Unshowable :: a -> V a

instance Show (V a) where
  show (Showable a) = show a
  show (Unshowable a) = "Some Error."

但这不是对类型是否为 Show 实例的运行时检查——您的代码负责在编译时了解 Showable 构造函数的使用位置。

【讨论】:

  • 如果出于某种原因,您确实想要为非 Show 类型返回错误消息怎么办?
  • @immibis:现在我想到了,您可以使用带有Show a 约束和the compiler option to defer type errors to runtime 的最后一个版本。但是编译器仍然会警告你你的程序在运行时可能会失败——它甚至会准确地告诉你哪些行会失败!
  • 返回一条错误信息,不会让程序崩溃。
  • 为什么你认为多态递归不适合初学者?
  • @dfeuer:其实我在想RankNTypes
【解决方案2】:

您可以使用此库:https://github.com/mikeizbicki/ifcxt。能够在可能有也可能没有 Show 实例的值上调用 show 是它给出的第一个示例。这就是你如何适应V a

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}

import IfCxt
import Data.Typeable

mkIfCxtInstances ''Show

data V a = V a

instance forall a. IfCxt (Show a) => Show (V a) where
    show (V a) = ifCxt (Proxy::Proxy (Show a))
        (show a)
        "<<unshowable>>"

这是这个库的精髓:

class IfCxt cxt where
    ifCxt :: proxy cxt -> (cxt => a) -> a -> a

instance {-# OVERLAPPABLE #-} IfCxt cxt where ifCxt _ t f = f

我不完全理解,但我认为它是这样工作的:

它并没有违反“开放世界”假设

instance {-# OVERLAPPABLE #-} Show a where
    show _ = "<<unshowable>>"

确实如此。该方法实际上与此非常相似:为范围内没有实例的所有类型添加默认情况以供参考。但是,它添加了一些间接性,以免弄乱现有实例(并允许不同的函数指定不同的默认值)。 IfCxt 用作“元类”,即约束类,指示这些实例是否存在,默认情况下指示“假”。

instance {-# OVERLAPPABLE #-} IfCxt cxt where ifCxt _ t f = f

它使用 TemplateHaskell 为该类生成一长串实例:

instance {-# OVERLAPS #-} IfCxt (Show Int) where ifCxt _ t f = t
instance {-# OVERLAPS #-} IfCxt (Show Char) where ifCxt _ t f = t

这也意味着调用mkIfCxtInstances 时不在范围内的任何实例都将被视为不存在。

proxy cxt 参数用于将Constraint 传递给函数,(cxt =&gt; a) 参数(我不知道 RankNTypes 允许这样做)是一个可以使用约束 @ 987654335@,但只要未使用该参数,就不需要解决约束。这类似于:

f :: (Show (a -> a) => a) -> a -> a
f _ x = x

proxy 参数提供约束,然后 IfCxt 约束被求解为 tf 参数,如果它是 t 则有一些 IfCxt 实例提供此约束这意味着它可以直接解决,如果它是 f 则永远不会要求约束,因此它会被丢弃。


这个解决方案是不完善的(因为新模块可以定义新的 Show 实例,除非它也调用 mkIfCxtInstances,否则它将无法工作),但是能够做到这一点违反开放世界假设。

【讨论】:

    【解决方案3】:

    即使你能做到这一点,这也是一个糟糕的设计。我建议将Show 约束添加到a

    instance Show a => Show (V a) where ...
    

    如果您想将成员存储在不是Show 实例的容器数据类型中,那么您应该为它们创建一个新的数据类型。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-25
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-21
      • 1970-01-01
      相关资源
      最近更新 更多