隐式包装新类型可以使生活简单得多。例如,考虑:
newtype Fun2 a1 a1 b = Fun2 { applyFun2 :: a1 -> a2 -> b }
instance Functor (Fun2 a1 a2) where
fmap f (Fun2 g) = Fun2 (\x y -> f (g x y))
instance Applicative (Fun2 a1 a2) where
Fun2 f <*> Fun2 g = Fun2 (\x y -> f x y (g x y))
pure a = Fun2 (\x y -> a)
sumAndProduct :: Num a => a -> a -> (a, a)
sumAndProduct = applyFun2 $ (,) <$> Fun2 (+) <*> Fun2 (*)
通过隐式包装新类型,我们可以简单地编写:
sumAndProduct :: Num a => Fun2 a a (a, a)
sumAndProduct = (,) <$> (+) <*> (*)
隐式地解开新类型将使我们能够像正常函数一样使用sumAndProduct
(因此也使GeneralizedNewtypeDeriving
变得不必要):
result :: (Int, Int)
result = sumAndProduct 10 20
当然,不应该在新类型之间使用隐式强制:
import Data.Monoid
x :: Sum Int
x = 10
y :: Product Int
y = x -- this is not allowed
y :: Product Int
y = getSum x -- this is allowed
y :: Product Int
y = (x :: Int) -- this is also allowed
无论如何,I'm not the only one希望使用扩展名来启用此功能。恕我直言,这似乎是一个声音扩展。如果类型不匹配,并且可以通过包装(如果期望的类型是新类型)或展开(如果实际的类型是新类型)将实际类型转换为期望的类型,则可以这样做。如果可以同时包装和拆包(这是不太可能的),则只需抛出一个错误,告诉用户明确。
但是,仅因为我认为这是一个好主意,并不意味着它实际上是。人们在上面链接的Reddit帖子上提出的一些担忧是:
您所描述的内容将非常具有干扰性。由于任何类型都可以包含在新类型中,这意味着所有类型为
t
的绑定都将泛化为Coercible a t => t
(本质上将表达式包装在coerce
中)。
我真的不知道这意味着什么,但是我假设它是指诸如newtype Fix f = Fix { unFix :: f (Fix f) }
之类的递归新类型。但是,这实际上似乎不是问题,因为您始终可以显式地包装和拆开新类型。也许有人可以阐明本段要传达的内容?
在任何情况下都将大大阻碍类型推断。
我看不出如何会阻碍类型推断。
当您使用newtype为同一个基础类型具有多个同类型类的实例时,您需要使用构造函数来消除要使用的实例的歧义,否则您需要显式的类型注释,从而破坏了隐式的目的newtype构造函数。
我看不到显式类型注释如何破坏隐式newtype构造函数的目的。例如,看一下我前面提到的sumAndProduct
函数定义:
sumAndProduct :: Num a => Fun2 a a (a, a)
sumAndProduct = (,) <$> (+) <*> (*)
比原始定义更具可读性和启发性:
sumAndProduct :: Num a => a -> a -> (a, a)
sumAndProduct = applyFun2 $ (,) <$> Fun2 (+) <*> Fun2 (*)
您甚至根本不需要编写任何类型签名:
sumAndProduct = applyFun2 $ (,) <$> (+) <*> (*)
如您所见,我对Reddit帖子提出的关注并不完全相信。我可能是错的。因此,我的问题是“您认为在Haskell中对新类型进行隐式包装和展开是一个好主意吗?”如果是,请解释为什么它确实是一个好主意。如果没有,请说明原因。