函数作为类型类的实例?
{-# LANGUAGE LambdaCase #-}
我有很多以各种方式编码失败的函数。 例如:
f :: A -> Bool
在失败时返回False
g :: B -> Maybe B'
在失败时返回Nothing
h :: C -> Either Error C'
在失败时返回Left ...
我想以与Maybe
monad相同的方式链接这些操作,因此链接函数需要知道每个函数在进入下一个函数之前是否失败。 为此我写了这个类:
class Fail a where
isFail :: a -> Bool
instance Fail () where
isFail () = False
instance Fail Bool where -- a
isFail = not
instance Fail (Maybe a) where -- b
isFail = not . isJust
instance Fail (Either a b) where -- c
isFail (Left _) = True
isFail _ = False
但是,不符合的函数可能存在:
f' :: A -> Bool
在失败时返回True
g' :: B -> Maybe Error
Just Error
在失败时返回Just Error
(成功时Nothing
) h' :: C -> Either C' Error
在失败时返回Right ...
这些可以通过简单地使用转换它们的函数来包装它们来解决,例如:
f'' = not . f'
f'' = not . f'
。 g'' = (case Nothing -> Right (); Just e -> Left e) . g'
h'' = (case Left c -> Right c; Right e -> Left e) . h'
然而,链接函数的用户希望能够将f
, g
, h
, f'
, g'
和h'
并让它们正常工作。 他不会知道函数的返回类型需要被转换,除非他查看他正在合并的每个函数的语义,并检查它们是否与范围内的任何Fail
实例相匹配。 对于普通用户来说,这是单调乏味而且太微妙的,尤其是对于绕过用户不得不选择正确实例的类型推断而言。
这些功能不是在知道如何使用它们的情况下创建的。 所以我可以创建一个类型data Result ab = Fail a | Success b
data Result ab = Fail a | Success b
并在每个函数周围制作包装。 例如:
fR = (case True -> Sucess (); False -> Fail ()) . f
f'R = (case False -> Sucess (); True -> Fail ()) . f'
gR = (case Just a -> Sucess a; Nothing -> Fail ()) . g
g'R = (case Nothing -> Sucess (); Just e -> Fail e) . g'
hR = (case Left e -> Fail e; Right a -> Sucess a) . h
h'R = (case Right e -> Fail e; Left a -> Sucess a) . h'
但是,这感觉很脏。 我们所做的只是验证/解释在组合函数的上下文中如何使用f
, g
, h
, f'
, g'
和h'
中的每一个。 是否有更直接的方式来做到这一点? 我想要的是一种方式来说明哪个实例的Fail
类型应该用于每个函数,即(使用上面给出的类型类实例的名称), f
→ a
, g
→ b
, h
→ c
和f'
→ a'
, g'
→ b'
, h'
→ c'
用于“无效”函数,其中a'
, b'
和c'
定义为以下实例(与前面的实例重叠,所以你会需要能够以某种方式按名称选择它们):
instance Fail Bool where -- a'
isFail = id
instance Fail (Maybe a) where -- b'
isFail = isJust
instance Fail (Either a b) where -- c'
isFail (Right _) = True
isFail _ = False
它不一定要通过类特性来完成。 也许有一些方法可以做到这一点,而不是类型类?
不要这样做。 Haskell的静态类型系统和参照透明性为您提供了极大的有用保证:您可以确定某个特定的值意味着相同的事物1,而不管它是如何生成的。 对于这种干扰既没有可变性,也没有对表达式的动态样式“运行时重新解释”,因为您需要完成您所期望的任务。
如果你在那里的这些功能没有相应地遵守这样的规范,那么这很糟糕。 更好地摆脱它们(至少,隐藏它们并只导出具有统一行为的重新定义的版本)。 或者告诉用户他们将不得不一起查找每个用户的规格。 但不要试图绕开这个破碎定义的特定症状。
一个简单的变化,你可以申请只是“标志”失败意味着相反的功能,否则会让他们返回这样一个包装的结果:
newtype Anti a = Anti { profail :: a }
instance (Anti a) => Fail (Anti a) where
isFail (Anti a) = not $ isFail a
1Mind:可能非常抽象意义上的“同一件事”。 没有必要让Left
成为普遍的“失败构造函数”,显然它是与第一个类型参数关联的变体构造函数就足够了,这不是函数/ monad实例的操作 - 从它自动遵循这个将“意味着”在一次性应用程序中失败。
也就是说,当你选择了正确的类型时,东西应该是非常自动的; 显然,当你只是围绕布尔变换时,情况正好相反,所以也许你应该完全摆脱那些......