我不明白你为什么首先使用类型类。类型类比仅仅拥有普通功能有什么好处?
只需将二元运算符定义为 Haskell 二元运算符,它们只是普通函数:
f :: MyType -> MyType -> MyType
f = ...
由于您所有的 DSL 类型都在 MyType 中,因此没有理由使用类型类。
打包和拆包
当然,这仍然不能解决您的error 问题。我过去采用的一种方法是使用类型类来定义将原始类型“打包”和“提取”到 DSL 中的方法:
class Pack a where
pack :: a -> MyType
class Extract a where
extract :: MyType -> a
String 的实例如下所示:
instance Pack String where pack = A
instance Extract String where
extract (A str) = str
extract _ = error "Type error: expected string!"
Extract 类可以处理不兼容类型的错误处理。
这让您可以将功能统一“提升”到您的 DSL 中:
-- Lifts binary Haskell functions into your DSL
lift :: (Extract a, Extract b, Pack c) => (a -> b -> c)
-> MyType -> MyType -> MyType
lift f a b = pack $ f (extract a) (extract b)
如果您将MyType 设为Pack 和Extract 的实例,这将适用于纯Haskell 函数和 了解您的DSL 的函数。也就是说,有意识的函数只会得到某种MyType,并且必须手动处理它,如果他们的MyType参数不是他们所期望的,则调用error。
因此,对于您可以直接用 Haskell 编写的函数,这解决了您的 error 问题,但对于那些依赖于 MyType 的函数却不是真的。
错误处理
使用pack 也很好,因为切换到比error 更好的错误处理机制非常简单。您只需切换extract 的类型(或者甚至pack,如果合适的话)。也许你可以使用:
class Extract a where
extract :: MyType -> Either MyError a
然后以Left (TypeError expected got) 失败,这会让你写出漂亮的错误消息。
这还可以让您轻松地将多个原始函数组合成MyType 级别的“案例”。基本思想是我们将多个可提升函数组合成一个MyType -> MyType -> MyType,在内部我们只使用第一个不会给我们错误的函数。这也可以给我们一些漂亮的语法:)。
以下是相关代码:
type MyFun = MyType -> MyType -> Either MyError MyType
(|:) :: (Extract a, Extract b, Pack c) => MyFun -> (a -> b -> c) -> MyFun
(f |: option) a b = case f a b of
Right res -> return res
Left err -> (lift option) a b
match :: MyFun
match _ _ = Left EmptyFunction
test = match |: (\ a b -> a ++ b :: String)
|: (\ a b -> a || b)
不幸的是,我不得不添加一个:: String 类型签名,否则它会模棱两可。如果我使用+,也会发生同样的情况,因为它不知道要依赖什么样的数字。
现在test 是一个可以在两个As 或两个Bs 上正常工作的函数,否则会出错:
*Main> test (A "foo") (A "foo")
Right (A "foofoo")
*Main> test (C True) (C False)
Right (C True)
*Main> test (A "foo") (C False)
Left TypeError
另请注意,这对于不同类型的参数非常有效,例如可以组合 A 和 B 值的案例。
这意味着您现在可以方便地将f、g、h 等函数重铸为 Haskell 中的顶级名称。以下是您如何定义f:
f :: MyFun
f = match |: \ s1 s2 -> {- something with strings -}
|: \ s i -> {- something with a string and an int -}
|: \ i d -> {- something with an int and a double -}
|: {- ...and so on... -}
您有时必须使用类型签名来注释某些值,因为没有足够的信息来使类型推断正常工作。仅当您使用来自类型类的操作(即+)或使用具有更一般类型的操作(如++ 用于字符串)时才会出现这种情况(++ 可以在任何列表上工作)。 p>
您还必须更新 lift 才能正确处理错误。这涉及将其更改为返回 Either 并添加必要的管道。我的版本是这样的:
lift :: (Extract a, Extract b, Pack c) => (a -> b -> c) -> MyFun
lift f a b = fmap pack $ f <$> extract a <*> extract b
新类型
这主要通过让|: 为您构造检查错误来解决您的error 问题。这种方法的主要缺点是,如果您希望您的 DSL 具有多个具有相同 基础 Haskell 类型的类型,例如:
data MyType = A Double
| B Double
{- ... -}
您可以使用newtype 为Double 创建一个包装器来解决此问题。像这样的:
newtype BDouble = B Double
instance Pack Double where pack = A
instance Pack BDouble where pack = B
-- same for Extract