为什么不能将类的实例用作值?

时间:2015-03-17 05:59:32

标签: haskell

为什么我不能这样写:

data Color = R | G | B deriving Show

showColor :: Show Color
showColor = Show Color

main = do
    putStrLn (showColor.show R)
    putStrLn (showColor.show G)

为什么类的实例不是Haskell中的一等公民?

4 个答案:

答案 0 :(得分:5)

很难讨论为什么语言的某个特征是这样的,因为只有该语言的设计者才能真正回答这个问题。然而,他们确定每个类型每个类最多只能有一个实例。这很可能是因为实例是隐含使用的。考虑一个班级

class Foo a where foo :: a -> String

和这个模块

import A  -- defines an instance Foo Int
bar :: Int -> String
bar n = "The number is " ++ foo n

bar函数隐式是指模块A的实例。现在假设我们添加另一个导入

import A  -- defines an instance Foo Int
import B  -- defines another instance Foo Int
bar :: Int -> String
bar n = "The number is " ++ foo n

现在这是模棱两可的。 Haskell可能提供了一种语法来消除歧义,但却选择了禁止它。一个优点是,无论谁读取代码都可以更容易地找到使用过的实例,因为只能有一个。

但是可以部分模拟多个实例。 GHC扩展允许人们定义隐式参数,允许人们指定不同的"实例"对于每个函数调用的参数:

{-# LANGUAGE ImplicitParams #-}

data Color = R | G | B

showColor :: (?showC :: Color -> String)  => Color -> String
showColor c = "The color is: " ++ ?showC c

main :: IO ()
main = do
   let ?showC = \c -> case c of R -> "Red" ; _ -> "Not Red"
     in putStrLn (showColor B)
   let ?showC = \c -> case c of G -> "Green" ; _ -> "Not Green"
     in putStrLn (showColor B)

以上的输出是:

The color is: Not Red
The color is: Not Green

Agda编程语言没有类型类,但implicit instances具有类似的作用。在那里,隐式传递一个实例(如在Haskell中),但是如果需要,您可以手动覆盖隐式参数并使用特殊语法来指定不同的实例。

答案 1 :(得分:0)

在Haskell中,类型类非常隐含。 (例如,您无法显式导出或不导出类实例 - 这有时会令人讨厌。)

类型类的整个是它是一堆函数,其中正确的一个被自动选择。如果您希望能够为每种类型传递不同的功能 - 那么,只需执行此操作!

例如,有一个类

class Monoid m where
  mempty :: m
  mappend :: m -> m -> m

所以我们可以做类似

的事情
fold :: Monoid m => [m] -> m
fold (  []) = mempty
fold (x:xs) = x `mappend` fold xs

然而,没有人这样做。相反,他们明确传递参数:

foldr :: (x -> y -> y) -> y -> [x] -> y
foldr f y0 (  []) = y0
foldr f y0 (x:xs) = f x (foldr f y0 xs)

因为这样,使用多个不同的函数/起始值折叠相同的数据类型要容易得多。

如果您希望能够以多种不同的方式“显示”一个值,只需将show函数作为参数传递。如果您希望函数自动为数据类型选择正确的show函数,请使用Show类 - 但在这种情况下,只能有一个实例(因为那是要点)。

答案 2 :(得分:0)

类型类背后的思想在于名称:类型类是类(或集)类型(它们都支持给定的接口)。

例如Eq是支持相等的类型类。

类型为Eq a => [a] -> Maybe a的函数指定类型变量a的范围超出Eq成员的类型;您需要证明您的类型是Eq的成员才能调用此函数。

然后,实例通过演示类型如何支持接口,只是一个特定类型是类成员的建设性证据。

从这个角度来看,不仅仅是Haskell不支持多个实例;多个实例的想法没有意义。一个类型属于一个类或它不是; class / set的概念允许您查询成员资格,但不能存储多个不同的实现。

最重要的是,功能不应该能够为同一输入提供不同的返回值,因为您使用了不同的会员证明!现有的Haskell代码确实依赖于这个属性:经典的例子是Data.Map中的函数,它们(几乎)都有一个Ord约束,以便在{{{}}上运行1}}作为内部树。如果有可能(和正常)为一个类型设置许多不同的Map实例并在每次调用时传递您选择的一个,那么这将是一个非常不安全的界面!

为有效实例必须符合的类定义法则的做法也取决于此属性。如果我编写一组相互关联的函数,假设Ord的所有实例都支持monad定律,那么如果可以为运行在相同数据上的不同调用提供不同的实例,则很可能会破坏我的代码。

请注意,Scala 具有类型类。它有什么隐含参数;有一个设计模式来自己使用隐式参数来实现类型类。但这是一个漏洞的抽象;你可以使用隐式参数来做一些不符合"类型集"的概念框架的事情。

Haskell基本上使用隐式参数传递实现类型类,但它强制执行抽象; Haskell不是要求对所有类型类的隐式参数的使用进行连贯选择,而是拒绝实例可以不连贯的任何情况。隐含参数是内部实现细节;问他们为什么不是一流的,有点像问为什么vtable不是OO语言的第一堂课。

这些天Haskell明确支持隐式参数传递(带扩展),因此您可能使用相同的设计模式来实现"多个实例"在您编写的代码中。但是使用该工具编写的代码期望隐式参数是参数,并且应该在任意传递不同值时起作用。使用类型类编写的代码期望类型类抽象是正在使用的。

答案 3 :(得分:-1)

你的问题很难理解主要是因为

showColour :: Show Color
showColour = Show Color

没有意义。 Show Color如何属于Show Color类型? Show可以被视为一种约束,因此最终Show ColorBool,在我们的情况下将评估为True。

无论如何,使用模块可以实现静态范围。您可以轻松地执行类似

的操作
module Color where

import qualified Prelude

data Color = ...

show :: Color -> String
show c = Prelude.show c

然后使用print (Color.show R)