【问题标题】:How to combine return type polymorphism with parametric polymorphism in Haskell?如何在 Haskell 中将返回类型多态性与参数多态性结合起来?
【发布时间】:2019-06-13 15:00:57
【问题描述】:

我有一组包装器类型FilePaths(由于我使用的库的限制,它根据提供的类型创建特定的存储)和我需要从这些文件路径中获取的几条记录。

newtype SourceFilepath = SourceFilepath String deriving (Show)
newtype HeaderFilepath = HeaderFilepath String deriving (Show)
-- ..many more wrappers

data Source =
  Source {..}

data Header = 
  Header {..}

data Metadata = 
  Metadata {..}

-- .. many more record types

我想创建通用函数loadSource,它接受某些类型(实际上只有文件路径包装器)并基于提供的类型产生另一个特定类型的值(SourceHeaderMetadata 等) .伪代码:

loadSource :: a -> Compiler b
loadSource (SourceFilepath path) = subload path
loadSource (HeaderFilepath path) = subload path
-- .. other cases for other types 
--
-- `a` can be filepath wrappers
-- different `a` can lead to the same `b` sometimes

此功能无法运行我收到多个 a’ is a rigid type variable bound by the type signaturerigid b.. 错误。

所以我没有这样的多个功能(代码正常工作):

subload :: FromJSON b => FilePath -> Compiler b
subload path = <already implemented operational logic>

loadHeader :: HeaderFilepath -> Comiler Header
loadHeader (HeaderPath path) = subload path

loadMetadata :: MetadataFilepath -> Comiler Metadata
loadMetadata (MetadataFilepath path) = subload path

-- .. many more similar functions

我怎样才能做到这一点?

【问题讨论】:

    标签: haskell polymorphism parametric-polymorphism


    【解决方案1】:

    有几种方法可以实现这一点,但正如 @DanielWagner 所说,如果没有关于您想要实现的目标的更多详细信息,很难判断哪种方法最适合您。

    最简单的可能是使用具有关联类型族的类型类(或具有功能依赖的多参数类型类)将文件路径包装器的类型映射到编译器子类型。类型族方法如下所示:

    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE TypeFamilies #-}
    
    class Loadable a where
      filepath :: a -> String
      type Load a
    

    带有样板实例,例如:

    instance Loadable SourceFilepath where
      filepath (SourceFilepath pth) = pth
      type Load SourceFilepath = Source
    instance Loadable HeaderFilepath where
      filepath (HeaderFilepath pth) = pth
      type Load HeaderFilepath = Header
    instance Loadable MetadataFilepath where
      filepath (MetadataFilepath pth) = pth
      type Load MetadataFilepath = Metadata
    

    请注意,这里将两个文件路径包装器映射到相同的编译器子类型没有问题(例如,type Load HeaderFilepath = Source 可以正常工作)。

    给定:

    subload :: FromJSON b => FilePath -> Compiler b
    subload = ...
    

    loadSource的定义是:

    loadSource :: (Loadable a, FromJSON (Load a)) => a -> Compiler (Load a)
    loadSource = subload . filepath
    

    之后:

    > :t loadSource (SourceFilepath "bob")
    loadSource (SourceFilepath "bob") :: Compiler Source
    > :t loadSource (MetadataFilepath "alice")
    loadSource (MetadataFilepath "alice") :: Compiler Metadata
    

    您可以通过参数化包装器来大大减少样板文件,并且 - 就像 @DanielWagner - 我不理解您关于编译器将它们视为相同类型的文件的评论,因此您需要向我们展示发生了什么当你尝试的时候错了。

    无论如何,我的原始类型族解决方案的完整来源:

    {-# LANGUAGE DeriveGeneric #-}
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# OPTIONS_GHC -Wall #-}
    
    import Data.Aeson
    import GHC.Generics
    
    newtype SourceFilepath = SourceFilepath String deriving (Show)
    newtype HeaderFilepath = HeaderFilepath String deriving (Show)
    newtype MetadataFilepath = MetadataFilepath String deriving (Show)
    
    data Source = Source deriving (Generic)
    data Header = Header deriving (Generic)
    data Metadata = Metadata deriving (Generic)
    
    instance FromJSON Source
    instance FromJSON Header
    instance FromJSON Metadata
    
    data Compiler b = Compiler
    
    subload :: FromJSON b => FilePath -> Compiler b
    subload = undefined
    
    class Loadable a where
      filepath :: a -> String
      type Load a
    instance Loadable SourceFilepath where
      filepath (SourceFilepath pth) = pth
      type Load SourceFilepath = Source
    instance Loadable HeaderFilepath where
      filepath (HeaderFilepath pth) = pth
      type Load HeaderFilepath = Header
    instance Loadable MetadataFilepath where
      filepath (MetadataFilepath pth) = pth
      type Load MetadataFilepath = Metadata
    
    loadSource :: (Loadable a, FromJSON (Load a)) => a -> Compiler (Load a)
    loadSource = subload . filepath
    

    以及标记解决方案的完整来源:

    {-# LANGUAGE DeriveGeneric #-}
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE TypeFamilies #-}
    {-# OPTIONS_GHC -Wall #-}
    
    import Data.Aeson
    import GHC.Generics
    
    newtype TypedFilePath a = TypedFilePath FilePath deriving (Show)
    
    data Source = Source deriving (Generic)
    data Header = Header deriving (Generic)
    data Metadata = Metadata deriving (Generic)
    
    instance FromJSON Source
    instance FromJSON Header
    instance FromJSON Metadata
    
    data Compiler b = Compiler
    
    subload :: FromJSON b => FilePath -> Compiler b
    subload = undefined
    
    type family Load a where
      Load Source = Source
      Load Header = Header
      Load Metadata = Metadata
    
    loadSource :: FromJSON (Load a) => TypedFilePath a -> Compiler (Load a)
    loadSource (TypedFilePath fn) = subload fn
    

    【讨论】:

    • 谢谢!这很有帮助,具有关联类型族的类型类涵盖了我的案例。
    【解决方案2】:

    只需让你的包装器也参数化:

    newtype WrappedFilePath a = WrappedFilePath FilePath
    
    loadSource :: FromJSON a => WrappedFilePath a -> Compiler a
    loadSource (WrappedFilePath p) = subload fp
    

    如果愿意,您可以重复使用 Tagged,而不是创建新的 WrappedFilePath

    【讨论】:

    • 我试过了,编译器子系统认为它是同一类型的文件并尝试应用不起作用的函数。这就是为什么我需要有单独的HeaderFilePathSourceFilePath
    • @FilipvanHoft 我不明白你的第一条评论。您的伪代码已经为loadSource 中的两种模式调用了相同的subload 函数;这不反映你真正想要发生的事情吗?如果没有,那么也许您可以详细说明您确实想要发生的事情?
    • loadSource 无法运行,这就是我想我可能会使用的方式。
    • @FilipvanHoft 好的。在您详细说明您想要什么行为之前,我认为没有人可以进一步帮助您。
    猜你喜欢
    • 2018-03-02
    • 1970-01-01
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-14
    相关资源
    最近更新 更多