我试图在Haskell中实现一种动态类型的编程语言,它支持三种数据类型,我们将它们称为A、B和C,为了说明起见,我将允许A = Integer、B = [Integer]和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,则将另一种类型转换为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我对此有三个问题:
A实现toB这一事实似乎是不干净的。即使永远不应该调用它,我也希望完全避免实现它。instance ImplicitlyConvertible Value只是一堆样板代码,我想去掉.instance Op Value的实现是否合理。我一开始是不是走错路了?我如何更干净地实现所有这些呢?
发布于 2021-01-01 15:24:06
这实际上是最简单的工作,通过你的问题,所以我将从最后开始。
instance Op Value的实现是否合理。不,您的instance Op Value实现是不明智的。您试过在示例输入上评估它吗?你可能已经注意到它永远不会产生任何结果。问题的根源在于你用什么来称呼valueOp。看起来,您使用多态函数add (或mult)调用了add,但实际上并非如此。由于valueOp总是将Value上的函数作为其第一个参数,所以Op Value实例中的add定义总是使用由实例本身定义的函数add调用valueOp。这就产生了无限的递归。
如何将多态函数add传递给valueOp?请考虑这样一种类型:
valueOp :: (forall a. Op a => a -> a -> a) -> (Value -> Value -> Value)(请注意,您需要为此启用RankNTypes。)这种类型以a上的二进制函数作为输入,该函数适用于任何具有Op实例的a。因此,对于第一种情况,您可以写:
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只是一堆样板代码,我想去掉.您实际需要用于A、B和C的A实例吗?如果您从未独立使用它们,那么您可以将它们合并到Value实例中,这肯定会减少样板。此时,如果您只有一个实例( Value实例),您可以考虑完全摆脱class结构,只需定义toA :: Value -> Value和toB :: Value -> Value函数。
如果您需要保留所有这些实例,那么我看不出有什么方法可以绕过样板。
A实现toB这一事实似乎是不干净的。即使永远不应该调用它,我也希望完全避免实现它。这应该会向你提出一个关于你的总体战略的问题。在许多方面,你已经使事情令人愉快地通用,但还不清楚是什么买下了你。毕竟,如果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')这需要枚举B和C的所有可能选项作为valueOp的输入,这是令人沮丧的冗长,但您可以轻松地知道您的所有函数都是总计的。
https://stackoverflow.com/questions/65529511
复制相似问题