如何将带有标志的类型系统用于有时共享的函数,有时是独占的?

时间:2012-01-18 15:24:41

标签: haskell types

我正在研究绑定到Web服务,其中一些调用采用标志来返回其他信息。例如,通过ID获取艺术家可以使用“录音”标志返回艺术家的录音,并且通过ID获取发行也可以获取录音标记以获得该发行的所有音轨录音。但是,在获取艺术家时,您可以获得该艺术家的所有版本,但是当您实际获得特定版本时,此标志无效。

因此要将其编码到Haskell中,以下内容应该是有效的程序:

getArtistById 5678 [ WithRecordings ]
getReleaseById 37837 [ WithRecordings ]

以下应该是一个无效的程序,并且无法构建:

getReleaseById 739 [ WithReleases ]

我想到了一些解决方案,但我不确定应该采取哪些措施。想到的第一个想法是使用类型类ArtistFlagReleaseFlag,但由于一些原因,这没有意义。首先,ArtistFlag f => [f]表示相同标志的列表,这是有缺陷的。但是,类型类意味着将来添加额外标志的能力,这也没有多大意义 - 有一定数量的标志。

我的下一个选项是每个终点标志的单独数据声明:

data ArtistFlag = ArtistWithRecordings | ArtistWithReleases
data ReleaseFlag = ReleaseWithRecordings

这有点笨拙 - 理想情况下*WithRecordings应始终具有相同的名称,以简化程序员的API。

最后,由于缺乏知识,这是我没有探索过的唯一选项,这可能是HList可以解决的。 getArtistById应该采用一组异类的艺术家旗帜。我不知道如何在HList中表达这一点,或者即使它可以做到这一点。

很想知道打字大师要说些什么:)

我正在实施的API的实际部分位于http://musicbrainz.org/doc/XML_Web_Service/Version_2#Subqueries - 注意recordings标志等。

2 个答案:

答案 0 :(得分:6)

完全按照字面意思提出问题,这不能通过一个简单的列表来实现;列表中的每个元素都具有完全相同的类型,并且独立于所有其他元素,因此不可能像这样“远距离行动”。

然而,要使它在非常小的情况下工作所需的更改(实际上,需要更多它才能使它与HList一样工作)。这是一个让您的API保持基本相同但不需要任何前缀的想法:使用type-classes来表示多种类型支持的标志。

data ArtistFlag = ArtistWithRecordings | ArtistWithReleases
data ReleaseFlag = ReleaseWithRecordings

withReleases :: ArtistFlag
withReleases = ArtistWithReleases

class HasRecordingsFlag flag where
  withRecordings :: flag

instance HasRecordingsFlag ArtistFlag where
  withRecordings = ArtistWithRecordings

instance HasRecordingsFlag ReleaseFlag where
  withRecordings = ReleaseWithRecordings

getArtistById :: Int -> [ArtistFlag] -> IO (Maybe Artist)
getReleaseById :: Int -> [ReleaseFlag] -> IO (Maybe Release)

用户代码的唯一变化是With变小了。这是一个简单的解决方案,可能是解决问题的最简单,最惯用的方法,特别是考虑到您正在连接的外部API的限制。

然而,在我看来,在某些情况下,这可能是重新调整API的好机会;例如,您展示的getArtistByIdgetReleaseById函数让我担心。如果getArtistById 42 [withReleases]getArtistById 42 []具有相同的类型,则无法保证您实际从前一个调用中获取了版本;在结果中可能有一个Maybe [Release]字段或类似字段,并且要求发布的艺术家的程序必须不安全地展开此Maybe(例如,使用fromJust)因为有一个不变量在未在类型系统中编码的API中。

对此最好的解决方案可能取决于各种因素过于局部化和详细,但需要考虑。对我来说另外一件事是,据推测,“艺术家的所有录音”与“艺术家所有发行的所有录音”都是一样的。因此,实质上,与艺术家一起检索版本是优化;如果我们能够表达更“原始”的形式,并以最有效的方式自动检索,那将是最好的。

当然,这可能并不总是可以实现,如果您正在尝试创建直接API,它甚至可能都不可取。但这是理想的,并向我建议,如果你愿意使用类型系统来尽可能地获得最好的API,那么可能值得考虑改变焦点:)

答案 1 :(得分:1)

我会建议另一种解决方案,基于假设(可能是错误的!),“flags”参数将始终是一个静态常量。当这个假设成立时,你可以通过在类型级别传递它来获得很多杠杆。这是一个非常简单的例子:

class GetArtist  a where getArtistById  :: Int -> a
class GetRelease a where getReleaseById :: Int -> a

data Artist = Artist {- whatever data you store about an artist goes here -}
data Recording = Recording {- data about recordings -}
data Release = Release {- data about releases -}

然后,您可以简单地枚举有效的返回类型(当然还有实现):

instance GetArtist Artist where -- ...
instance GetArtist [Recording] where -- ...
instance GetArtist [Release] where -- ...
instance (GetArtist a, GetArtist b) => GetArtist (a, b) where
    getArtistById n = (getArtistById n, getArtistById n)
-- maybe a similar instance for triples
instance GetRelease Release where -- ...
instance GetRelease [Recording] where -- ...
-- no instance GetRelease [Release]
instance (GetRelease a, GetRelease b) => GetRelease (a, b) where
    getReleaseById n = (getReleaseById n, getReleaseById n)

例如,要使用getArtistById,您只需将其称为getArtistById 5678,并将结果用于您需要的任何类型 - Artist,或(Artist, [Recording]),或其他任何类型

相关问题