我正在尝试在运行时连接函数(我必须这样做),这可能涉及函数输入和输出中的类型类约束。
在像Java这样的语言中,这将是微不足道的。 f1 :: Int -> Num
,f2 :: Num -> Num
我们现在可以致电f2 . f1
。如果Num是一个Java风格的界面,那将没有问题。但类型类的行为不像接口。
类型类允许您做的一件事是避免在数据类型之间进行转换。我们可以来回转换并摆脱类型类。但令人担忧的是,我们无缘无故地创造了所有物品。我们试着避免这种情况。
我开始尝试创建(a,Api a)的元组,其中Api是将在a上运行的函数的记录。我的猜测可能与类型类的工作方式非常相似?但是你遇到了混凝土类型问题,我认为存在。但后来我意识到(a,Api a)应该能够完全隐藏a,因为没有人关心,然后Api变成了数据类型的简单记录,而不是函数。
所以我很想知道......解决这个问题的懒惰是什么?
module Main where
--data Api a = Api { f1 :: a -> Int, f2 :: a -> String }
data Api = Api { f1 :: Int, f2 :: String }
data MyData = MyData Int String
myf1 (MyData x y) = x
myf2 (MyData x y) = y
myApi x = Api (myf1 x) (myf2 x)
from :: Int -> Api
from x = myApi $ MyData x "string"
to :: Api -> String
to api = f2 api
main = print $ to . from $ 5
那么,它是否足够智能(或可能)意识到它根本不需要创建Api值,因为我们需要的是在MyData值上调用myf2?
所以Java界面的“等价”不是某人告诉我的类型类,而是记录或数据类型?而懒惰提供了界面的“轻量级”?
答案 0 :(得分:12)
但后来我意识到(a,Api a)应该能够完全隐藏a,因为没有人关心,然后Api变成了数据类型的简单记录,而不是函数。
完全!使用像普通数据类型那样的存在性称为existential typeclass antipattern(另请参阅the FAQ)。然而,它与懒惰并没有直接关系;您可以使用严格的语言轻松地将每个字段表示为() -> Result
。当然,使用它不会那么好。
类型类的优点在于隐式,类型导向的解析:您可以直接使用操作,就像它们在您使用的特定类型上是单形的一样,并且它工作正常,无需为每个要实现操作的类型提供单独的名称。有关为什么类型类有价值的示例,请想象Num是否作为记录实现;你必须在每个地方传递每种类型的实施记录。更糟糕的是,没有地方可以放置fromInteger :: (Num a) => Integer -> a
,因为它在结果中是多态的,并且根本不接受a
类型的任何值!
另一个例子就像Ord。您不能将Ord表示为记录,因为Ord的任何实例都必须对此类型到记录转换所消除的值进行操作。将Ord作为类型类也可以让我们编写对可以排序的任何类型的值都是通用的代码,这确实非常有价值,并且在这方面,类型类的使用在某种程度上肯定类似于OOP接口。 / p>
然而,当没有真正的相关价值可言,或者它们都来自“外部”只是作为内部状态(如存在主义)时,类型类只会增加不必要的样板,而不是“首先 - 类”。函数和数据类型是Haskell中真正的抽象单元;类型类只是方便。
那么,它是否足够智能(或可能)意识到它根本不需要创建Api值,因为我们需要的是在MyData值上调用myf2?
GHC可能会创建中间数据类型,但您不必过于担心;存在主义也带有类型类实例字典,就像你的Api类型一样。这个记录界面的优点不是真正的性能,而是简单性,清晰度和可组合性(很容易转换记录的所有字段以产生“修改”的实现 - 但你不能拥有一个转换类型的函数 - 类实例)。