【问题标题】:Implementing dynamic datatype conversion in Haskell在 Haskell 中实现动态数据类型转换
【发布时间】:2021-01-01 12:04:51
【问题描述】:

我正在尝试在 Haskell 中实现一种动态类型的编程语言,它支持三种数据类型,我们称它们为 ABC,仅出于说明目的,我将让 A = Integer、@987654325 @ 和C = (Integer, Integer) (但你可以忽略这些类型的语义,这不是我关心的)。

为了在算术表达式中互换使用任何类型的值,我实现了一个代数数据类型Value

data Value = A A
           | B B
           | C C

因为我希望能够添加和乘以值,所以我实现了类型类OP

class Op a where
  add :: a -> a -> a
  mul :: a -> a -> a

现在,我还希望我的类型可以隐式转换(当两个不同的类型出现在算术表达式中时),根据以下规则:

  • 如果两种类型都是A,则不进行转换
  • 如果其中一种类型为A,则另一种转换为A
  • 否则,两种类型都转换为B

为了实现这一点,我实现了另一个类型类,ImplicitlyConvertible

class ImplicitlyConvertible a where
  toA :: a -> A
  toB :: a -> B

一个完整的例子如下所示:

{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}

module Value where

type A = Integer

type B = [Integer]

type C = (Integer,Integer)

data Value = A A
           | B B
           | C C

class ImplicitlyConvertible a where
  toA :: a -> A
  toB :: a -> B

instance ImplicitlyConvertible A where
  toA = id
  toB = error "can't convert A to B"

instance ImplicitlyConvertible B where
  toA = sum
  toB = id

instance ImplicitlyConvertible C where
  toA   = sum
  toB c = [fst c, snd c]

instance ImplicitlyConvertible Value where
  toA v = case v of
    A a -> toA a
    B b -> toA b
    C c -> toA c
  toB v = case v of
    A a -> toB a
    B b -> toB b
    C c -> toB c

class Op a where
  add :: a -> a -> a
  mul :: a -> a -> a

instance Op A where
  add = (+)
  mul = (*)

instance Op B where
  add = zipWith (+)
  mul = zipWith (*)

valueOp :: (Value -> Value -> Value) -> (Value -> Value -> Value)
valueOp op (A v) v' = op (A v) (A $ toA v')
valueOp op v (A v') = op (A $ toA v) (A v')
valueOp op v v'     = op (B $ toB v) (B $ toB v')

instance Op Value where
  add = valueOp add
  mul = valueOp mul

我有三个问题:

  • toB 实际上并没有为A 实现这一事实似乎是不干净的。即使它永远不应该被调用,我也想完全避免实现它。

  • instance ImplicitlyConvertible Value 只是一堆我想去掉的样板代码。

  • 我不确定我对instance Op Value 的实现是否合理。

我是不是一开始就走错了路?我怎样才能更干净地实现所有这些?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    实际上,倒推你的问题是最容易的,所以我会从最后开始。

    • 我不确定我对instance Op Value 的实现是否合理。

    不,您对instance Op Value 的实现是不明智的。您是否尝试过在示例输入上对其进行评估?您可能已经注意到它永远不会产生结果。问题源于您所称的valueOp看起来您正在使用多态函数add(或mult)调用valueOp,但实际上并非如此。由于valueOp 始终将Values 上的函数作为其第一个参数,因此add 在您的Op Value 实例中的定义将始终使用函数valueOp 调用valueOp,该函数由定义/em> 实例本身。这会创建一个无限递归。

    如何将多态函数add 传递给valueOp?考虑这样的类型:

    valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)
    

    (请注意,您需要为此启用RankNTypes。)此类型将a 上的二进制函数作为输入,该函数适用于任何 a,它具有@987654339 @ 实例。所以,对于第一种情况,你可以这样写:

    valueOp op (A v) v' = A $ op v (toA v')
    

    输入和输出仍然是Value 类型,但是当我们调用op 时,我们使用的是A 类型的值,这正是我们想要的。其他两种情况自然如下:

    valueOp op v (A v') = A $ op (toA v) v'
    valueOp op v v'     = B $ op (toB v) (toB v')
    

    • instance ImplicitlyConvertible Value 只是一堆我想去掉的样板代码。

    您真的需要ABCImplicitlyConvertible 实例吗?如果您从不独立使用它们,那么您可以将它们合并到 Value 实例中,这肯定会减少样板文件。此时,如果您只有一个实例(Value 实例),您可以考虑完全摆脱class 结构并简单地定义函数toA :: Value -> ValuetoB :: Value -> Value

    如果您需要保留所有这些实例,那么我真的没有办法绕过样板。


    • toB 实际上并没有为A 实现这一事实似乎不干净。即使它永远不应该被调用,我也想完全避免实现它。

    这应该会向您提出一个关于您的整体策略的问题。在许多方面,你让事情变得令人愉快,但不清楚是什么给了你。毕竟,如果ImplicitlyConvertible 的唯一用例在valueOp 中,那么您真的需要一个新类来实现一个功能吗?如果没有,也许您应该将实例折叠到valueOp 本身的定义中?您可能仍然在定义中有 error "can't convert A to B",但实际上您将能够证明它从未被调用过,这与在您当前的代码中任何人都可以通过 A 值调用 toB 不同。

    valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)
    valueOp op x y = case (x,y) of
      (A v, v') -> A $ op v (toA v')
      (v, A v') -> A $ op (toA v) v'
      (v, v')   -> B $ op (toB v) (toB v')
      where
        toA (A a) = a
        toA (B b) = sum b
        toA (C c) = sum c
        toB (A a) = error "can't convert A to B"
        toB (B b) = b
        toB (C c) = [fst c, snd c]
    

    或者,如果这种转换是必要的,您可以仅在明确可能的情况下定义它:

    class Convert x y where
      convert :: x -> y
    
    instance Convert Value A where
      convert (A a) = convert a
      convert (B b) = convert b
      convert (C c) = convert c
    
    instance Convert A A where
      convert = id
    
    instance Convert B A where
      convert = sum
    
    instance Convert C A where
      convert = sum
    
    instance Convert B B where
      convert = id
    
    instance Convert C B where
      convert c = [fst c, snd c]
    
    
    valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)
    valueOp op (A v) v' = A $ op v (convert v')
    valueOp op v (A v') = A $ op (convert v) v'
    valueOp op (B v) (B v') = B $ op v v'
    valueOp op (B v) (C v') = B $ op v (convert v')
    valueOp op (C v) (B v') = B $ op (convert v) v'
    valueOp op (C v) (C v') = B $ op (convert v) (convert v')
    

    这需要枚举BC 的所有可能选项作为valueOp 的输入,这令人沮丧地冗长,但您可以轻松地知道所有函数都是完整的。

    【讨论】:

    • 这太棒了!我以前从未见过forall a. 构造,这正是我想不通的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-22
    • 2014-10-01
    • 1970-01-01
    相关资源
    最近更新 更多