如何从支持某些API的函数返回非具体值?

时间:2011-11-16 17:08:31

标签: haskell

我正在两个模型之间构建API。我不在乎它是否返回[]或Seq或任何可折叠的东西都没问题。但如果我尝试这样做,我就会出错。

module Main where
import Prelude hiding (foldr)
import Data.Foldable
import Data.Sequence

data Struct = Struct

main = do
  print $ foldr (+) 0 $ list Struct
  print $ foldr (+) 0 $ listFree Struct


listFree :: Foldable f => a -> f Int
listFree s = singleton 10

class TestClass a where
  list :: Foldable f => a -> f Int

instance TestClass Struct where
  list s = singleton 10

listFree和列表定义都给出了相同的错误:

TestFoldable.hs:19:12:
Could not deduce (f ~ [])
from the context (Foldable f)
  bound by the type signature for
             list :: Foldable f => Struct -> f Int
  at TestFoldable.hs:19:3-15
  `f' is a rigid type variable bound by
      the type signature for list :: Foldable f => Struct -> f Int
      at TestFoldable.hs:19:3
In the expression: [10]
In an equation for `list': list s = [10]
In the instance declaration for `TestClass Struct'

为什么?什么是完成我在这里尝试做的“正确”方式?

我想要完成的是隐藏调用者的实现。实际的数据结构可能是Seq,IntMap或其他任何东西,很可能不是列表。

我收到的回复是“只返回一个列表”。但这意味着转换,不是吗?如果它是一个1,000,000元素结构怎么办?仅仅因为API的限制将其转换为中间数据结构似乎是一个糟糕的解决方案。

这是一个普遍的问题。如何具有符合某些API的返回值?隐藏具体实现,以便实现者可以自由选择最适合他们的结构,并且可以更改它而无需更改API的用户。

另一种方法是:如何返回界面而不是具体类型?

结束注意:

StackOverflow上的Haskell社区是(SuperlativeCompliment c => forall c.c)

存在量化似乎是这种情况的一般解决方案。

考虑的另一种可能性,即不是一般解决方案但可能适用于这种特定情况,可能会避免存在性解决方案所需的额外包装值,即为客户端返回折叠的闭包:

list :: a -> ((Int -> b -> b) -> b -> b)
list = \f a0 -> foldr f a0 (singleton 10)

5 个答案:

答案 0 :(得分:8)

  

为什么?

Foldable f => a -> f Int类型意味着该函数可能返回它想要的任何可折叠。这意味着该函数将返回用户想要的任何类型。即如果用户在需要列表的上下文中使用该函数,并且如果他在需要Seq的上下文中使用它也应该工作。由于您的定义显然不是这种情况,因此与其类型不匹配。

  

完成我想要做的事情的“正确”方法是什么?

最简单的方法是让你的函数返回一个列表。

但是,如果您确实需要隐藏使用用户列表这一事实,最简单的方法是在列表周围创建一个包装器类型,而不是导出该类型的构造函数。即类似的东西:

module Bla (ListResult(), list) where
data ListResult a = ListResult [a]

instance Foldable (ListResult a) where
    foldr op s (ListResult xs) = foldr op s xs

list s = ListResult [10]

现在,如果用户导入了您的模块,它可以折叠ListResult,因为它是可折叠的,但它无法解压缩以获取列表,因为构造函数未导出。因此,如果您稍后将ListResult的定义更改为data ListResult a = ListResult (Seq a)list也使用Seq而不是列表,则该更改对用户完全不可见。< / p>

答案 1 :(得分:5)

Foldable类仅提供用于销毁Foldable实例的方法,而不提供用于构造实例的方法。完整的课程方法列表如下:

class Foldable t where
    fold :: Monoid m => t m -> m
    foldMap :: Monoid m => (a -> m) -> t a -> m
    foldr :: (a -> b -> b) -> b -> t a -> b
    foldl :: (a -> b -> a) -> a -> t b -> a
    foldr1 :: (a -> a -> a) -> t a -> a
    foldl1 :: (a -> a -> a) -> t a -> a

您可以看到这些方法的返回类型从不具有“t foo”类型。因此,您无法构造一个多态的值,您可以选择Foldable个实例。

但是,是基于类型构造函数的类,其构造函数出现在至少一个方法的返回类型中。例如,有

class Pointed p where
    point :: a -> p a

pointed包提供。还有Monoid提供的base课程:

class Monoid m where
    mempty :: m
    mappend :: m -> m -> m
    mconcat :: [m] -> m

你可以像这样组合这两个类:

points :: (Pointed p, Monoid (p a)) => [a] -> p a
points = mconcat . map point

例如,在ghci:

> points [7,3,8] :: Set Int
fromList [3,7,8]
> points [7,3,8] :: First Int
First { getFirst = Just 7 }
> points [7,3,8] :: Last Int
Last { getLast = Just 8 }
> points [7,3,8] :: [Int]
[7,3,8]
> points [7,3,8] :: Seq Int
fromList [7,3,8]

答案 2 :(得分:5)

sepp2k已经提供了一个很好的答案,但允许我采取类似但略有不同的角度。你所做的是提供结果类型多态。你写道:

listFree :: Foldable f => a -> f Int

这样做可以保证您可以生成用户可能需要的任何可折叠文件。当然,你永远不会遵守这个承诺,因为Foldable不提供任何类似构造函数的函数。

所以你要做的就是处理泛型。你想做一个弱的承诺:函数listFree将产生一些 Foldable,但将来它可能会改变。您今天可以使用常规列表来实现它,但稍后,您可能会使用其他内容重新实现它。并且您希望此实现细节就是:实现细节。您希望该函数的契约(类型签名)保持不变。

听起来像另一个奇怪而令人困惑的Haskell扩展的工作!存在量化!

{-# LANGUAGE ExistentialQuantification #-}

import Prelude hiding (foldr, foldl, foldr1, foldl1)
import Data.Foldable

data SomeFoldable a = forall f. Foldable f => F (f a)

foo :: SomeFoldable Int
foo = F [1,2,3]

此处我提供了值foo,但其类型为SomeFoldable Int。我不是告诉你它是 Foldable,只是它某些可折叠。为方便起见,SomeFoldable可以很容易地成为Foldable的实例。

instance Foldable SomeFoldable where
  fold (F xs) = fold xs
  foldMap f (F xs) = foldMap f xs
  foldr step z (F xs) = foldr step z xs
  foldl step z (F xs) = foldl step z xs
  foldr1 step (F xs) = foldr1 step xs
  foldl1 step (F xs) = foldl1 step xs

现在,我们可以使用Foldable执行foo项内容,例如:

> Data.Foldable.sum foo
6

但除了Foldable曝光之外,我们不能对它做任何事情:

> print foo
No instance for (Show (SomeFoldable Int)) blah blah blah

根据需要调整代码很容易:

data Struct = Struct

main = do
  print $ foldr (+) 0 $ list Struct
  print $ foldr (+) 0 $ listFree Struct


listFree :: a -> SomeFoldable Int
listFree s = F [10]

class TestClass a where
  list :: a -> SomeFoldable Int

instance TestClass Struct where
  list s = F [10]

但请记住,存在量化有其缺点。无法打开SomeFoldable来获取下面的具体Foldable。原因与你的函数签名在开头是错误的原因相同:它承诺结果类型多态:它无法保留的承诺。

unwrap :: Foldable f => SomeFoldable a -> f a   -- impossible!
unwrap (F xs) = xs    -- Nope. Keep dreaming. This won't work.

答案 3 :(得分:0)

更新的问题有一个非常不同的答案。 (因为我的另一个答案是好的,但回答不同的问题,我将离开它。)

创建abstract data type。缺点是在模块中定义一个新类型

newtype Struct a = Struct [a]

这里我假设[a]是您要隐藏的具体实现。然后,添加Foldable实例。

deriving instance Foldable Struct
-- could do this on the newtype definition line above,
-- but it doesn't fit with the flow of the answer

在模块边界处,您可以通过导出Struct类型而不是Struct构造函数来隐藏实际实现。您的来电者可以使用Struct a进行的唯一操作就是调用Foldable方法。

答案 4 :(得分:0)

正如sepp2k在他的回答中所说, 问题是我们必须有一个单形返回类型。 就像他答案中的清单一样。

但是,我们仍然可以返回一个具有命名类型的包装类型, FoldableContainer,但它是Foldable。 为此,我们需要GADTts。

{-# LANGUAGE GADTs #-}
module Main where
import Prelude hiding (foldr)
import Data.Foldable

data Struct = Struct

data FoldableContainer a where
  F :: Foldable f => f a -> FoldableContainer a

instance Foldable FoldableContainer where
  foldMap g (F f) = foldMap g f

main = do
  print $ foldr (+) 0 $ list Struct
  print $ foldr (+) 0 $ listFree Struct


listFree :: a -> FoldableContainer Int
listFree s = F [10]

class TestClass a where
  list :: a -> FoldableContainer Int

instance TestClass Struct where
  list s = F [10]

但请注意,虽然这可行,但有些人说可能确实更好 只返回一个清单。为什么?首先,我们不创建额外的类型, 第二,每个构建的F都需要携带 Foldable字典,因为它在编译时是未知的。 我不知道这是多少性能损失,但不能忘记。 另一方面,我们不再需要列表作为中间类型, 在sum上使用Set Int的情况无需先转换为[Int]