Haskell类型约束+ forall

j8ag8udp  于 2023-08-06  发布在  其他
关注(0)|答案(1)|浏览(138)

我对Haskell相对较新,并试图解决如何使用forall将我的类型“封装”在非类型化数据中:
我不知道这是否法律的,但我在尝试:

data Value = Value (forall a b c. (Basis a, Basis b, Basis c) => Val a b c)
data Operation = Operation (forall a. (Basis a) => Op a a)

字符串
我想知道如何将操作应用于值,给定值的“a”类型必须匹配操作中的“a”:

opApply :: Operation -> Value -> IO()
opApply (Operation g) (Value q) = app1 g q


在这里,'app1'正确地警告类型错误。
我尝试使用约束和类型族,但无法克服。

xlpyo6sf

xlpyo6sf1#

最有可能的情况是,您并不想这样做。如果您将类型“封装”在非类型化数据中,因为您认为数据类型是一个实现细节,应该对类型的“公共Basis接口”隐藏起来,或者因为您认为在类型签名中编写Value而不是Val a b c会更方便,或者是因为您模糊地认为需要以统一的方式处理大量不同类型的ValOp,而正确的方法是构造统一的、非参数化的ValueOperator类型,那你就做错了。
在Haskell中表达opApply的通常正确方法是写如下:

data Val a b c = Val a b c
data Op a b = Op a b

opApply :: Op a a -> Val a b c -> IO ()
opApply = ...

字符串
可能会将Basis约束添加到opApply类型签名,如:

opApply :: (Basis a) => Op a a -> Val a b c -> IO ()


或者是:

opApply :: (Basis a, Basis b, Basis c) => Op a a -> Val a b c -> IO ()


视您需要在abc类型上执行Basis作业而定。
abc类型变量在类型签名中是可见的,并且是类型的“接口”的一部分,这是一个特性,而不是一个bug。这使您可以简洁地表达编译时约束,即Op a a中的两个类型参数是相同的类型a,并且该类型还必须与Val a b c中的a匹配,opApply操作才有效。如果有任何可能的方法可以使用这些简单的类型来编写程序,那么这就是您应该编写程序的方式。
另一方面,在一项研究中,如果您发现需要处理ValOp,而这些类型参数在运行时之前是未知的--通常只有在您重新从控制台或文件或某些其他输入源阅读输入,并将其解析为ValOp,其中解析器可能会为一个输入或一个Val String Int Double表示另一个输入--那么您可能希望使用“存在”类型,例如:

data SomeVal = forall a b c. (Basis a, Basis b, Basis c) => SomeVal (Val a b c)
data SomeOp = forall a b. (Basis a, Basis b) => SomeOp (Op a b)


请注意forall的位置和此处的限制:它们位于构造函数的外部,而不是构造函数的内部,这与您最初问题中的示例不同。
这些SomeValSomeOp存在类型可用于保存未指定类型的ValOp值,这些值(只能)通过Basis接口进行操作。存在主义者很难相处,所以除非迫不得已,否则你不会想这么做。对它们进行操作的代码变得非常复杂,非常快。假设TypeableBasis的一个超类(在完全通用的情况下进行这种类型匹配是必需的),尝试通过这些 Package 器将Op a aVal a b c“匹配”的opApply将采用以下形式:

{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

import Data.Typeable

class (Typeable t) => Basis t
data Val a b c = Val a b c
data Op a b = Op a b
data SomeVal = forall a b c. (Basis a, Basis b, Basis c) => SomeVal (Val a b c)
data SomeOp = forall a b. (Basis a, Basis b) => SomeOp (Op a b)

opApply :: SomeOp -> SomeVal -> IO ()
opApply (SomeOp (op :: Op a1 a2)) (SomeVal (val :: Val a3 b c))
  = case eqT @a1 @a2 of
      Just Refl -> case eqT @a1 @a3 of
        Just Refl -> print $ opApply' op val
        _ -> error "`Op a a` must match `Val a b c`"
      _ -> error "need an `Op a a`, not a general `Op a b`"
  where opApply' :: Op a a -> Val a b c -> Int
        opApply' _ _ = 42


真恶心。
您在问题中定义的类型:

data Value = Value (forall a b c. (Basis a, Basis b, Basis c) => Val a b c)
data Operation = Operation (forall a. (Basis a) => Op a a)


是秩2类型,而不是存在类型。它们可能是有用的,尽管它们可能并不意味着你所想的那样。
如果您希望类型表示抽象的ValueOperation值,而这些值可以用Basis的任何类型表示,以便单个特定值:

v :: Value


表示一个抽象的Val,它可以转换为Val Int Int IntVal Char Bool Bool或任何其他Basis示例,这取决于它在代码中的使用位置,与单个特定的数字文本的方式相同:

42


可以是IntDoubleWord8或任何其他具有Num示例的类型,具体取决于它在给定程序中出现的位置,* 则 * 这些等级为2的类型可能有意义。
在这种情况下,opApply会使用abc的呼叫端指定型别,将OperatorValue结合起来:

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

class Basis t
data Val a b c = Val a b c
data Op a b = Op a b
newtype Value = Value (forall a b c. (Basis a, Basis b, Basis c) => Val a b c)
newtype Operation = Operation (forall a. (Basis a) => Op a a)

opApply :: forall a b c. (Basis a, Basis b, Basis c) => Operation -> Value -> IO ()
opApply (Operation op) (Value val)
  = opApply' (op @a) (val @a @b @c)
  where opApply' :: Op a a -> Val a b c -> IO ()
        opApply' _ _ = putStrLn "so something"


在使用opApply的程式码中,您需要提供一组特定的Basis型别做为参数:

opApply @Int @Int @Int o v
opApply @Char @Bool @Bool o v


同样,给定的o :: Operatorv :: Value将是类型不可知的--opApply的调用者可以提供任何有效的目标Basis类型,而ov类型将不得不强制opApply生成所请求的Basis类型的值。
因此,这些类型(1)是可能的,(2)可能是有用的,尽管我不确定它们是否与您在这里尝试做的相匹配。

相关问题