我们如何在保持类型安全的同时通用地处理关联类型

Posted

技术标签:

【中文标题】我们如何在保持类型安全的同时通用地处理关联类型【英文标题】:How can we handle associated types generically while keeping type-safety 【发布时间】:2021-10-15 05:51:21 【问题描述】:

这是我的问题:

假设我有各种类型的对象要处理,但它们共享相同的形式:我们有Items,它由一个字符串(比如一个 id)和一个可以是任何东西的 Content 组成。 所以分解的问题可以总结如下: 我希望能够从 content 生成 item 以通用方式将其关联到 id 但我希望类型系统来约束接口,以便我知道我会 取回传递的contentItem

这是一个尝试使用 FunctionalDependencies 的示例:

-# LANGUAGE MultiParamTypeClasses #-
-# LANGUAGE FunctionalDependencies #-

main :: IO ()
main = do
  putStrLn $ show $ handleContent TitiAssociation $ read "TitiContent"
  putStrLn $ show $ handleContent TotoAssociation $ read "TotoContent"
-- Expected
-- "TitiItem"
-- "TotoItem"

-- definition of the domain
data TitiContent = TitiContent String deriving (Read, Show)
data TotoContent = TotoContent String deriving (Read, Show)

data TitiItem = TitiItem String TitiContent deriving (Read, Show)
data TotoItem = TotoItem String TotoContent deriving (Read, Show)

--
class (Read a, Show a) => Content a where
class (Read a, Show a) => Item a where

instance Content TitiContent where
instance Content TotoContent where

instance Item TitiItem where
instance Item TotoItem where

-- Association of types
class (Content contentType, Item itemType) => ItemContentAssociation association contentType itemType | association -> contentType, association -> itemType, itemType -> association where

-- tokens to identify the types which will be handled
data TitiAssociation = TitiAssociation
data TotoAssociation = TotoAssociation

instance ItemContentAssociation TitiAssociation TitiContent TitiItem where
instance ItemContentAssociation TotoAssociation TotoContent TotoItem where

-- generic function for handling
handleContent :: (ItemContentAssociation association contentType itemType) => association -> contentType -> itemType
handleContent TitiAssociation TitiContent = TitiItem
handleContent TotoAssociation TotoContent = TotoItem

但是编译器会抱怨:

tmp.hs:41:15: error:
    * Couldn't match expected type `ass' with actual type `TitiAss'
      `ass' is a rigid type variable bound by
        the type signature for:
          handleContent :: forall ass contentType itemType.
                           ItemContentAss ass contentType itemType =>
                           ass -> contentType -> itemType
        at tmp.hs:40:1-92
    * In the pattern: TitiAss
      In an equation for `handleContent':
          handleContent TitiAss TitiContent = TitiItem
    * Relevant bindings include
        handleContent :: ass -> contentType -> itemType
          (bound at tmp.hs:41:1)

我也尝试了各种类型的 TypeFamilies 扩展,但编译器总是抱怨(如果需要,可以提供更多示例,但最初旨在保持初始帖子的大小合理)。

我是函数式编程领域的新手,所以请不要犹豫,提出新方法,因为我确信我忽略了问题的许多方面。

提前非常感谢, 干杯!

【问题讨论】:

使handleContent成为ItemContentAssociation的方法。 @DanielWagner 它实际上可能确实是解决方案......几天来解决这个问题。显然不能再看清楚了...... ^^'我会尝试让它在现实世界的例子中工作并澄清问题,如果不是这样的话。关于如何使用 TypeFamilies(尤其是“关联类型”)的任何想法?我发现它更具可读性,并且从我得到的功能来看,它们有很多重叠。 【参考方案1】:

几乎可以肯定,在 MPTC/FunDeps 世界和 TF 世界中,正确的答案是使 handleContent 成为 ItemContentAssociation 的方法。这就是类型族的具体情况,因为您在 cmets 中询问了这一点。

-# LANGUAGE FlexibleContexts #-
-# LANGUAGE TypeFamilies #-
-# LANGUAGE TypeFamilyDependencies #-

main :: IO ()
main = do
  putStrLn $ show $ handleContent TitiAssociation $ read "TitiContent \"titi\""
  putStrLn $ show $ handleContent TotoAssociation $ read "TotoContent \"toto\""
-- Expected
-- "TitiItem"
-- "TotoItem"

-- definition of the domain
data TitiContent = TitiContent String deriving (Read, Show)
data TotoContent = TotoContent String deriving (Read, Show)

data TitiItem = TitiItem String TitiContent deriving (Read, Show)
data TotoItem = TotoItem String TotoContent deriving (Read, Show)

--
class (Read a, Show a) => Content a where
class (Read a, Show a) => Item a where

instance Content TitiContent where
instance Content TotoContent where

instance Item TitiItem where
instance Item TotoItem where

-- Association of types
class (Content (ContentType association), Item (ItemType association)) =>
    ItemContentAssociation association
    where
    type ContentType association = content | content -> association
    type ItemType association
    handleContent :: association -> ContentType association -> ItemType association

-- tokens to identify the types which will be handled
data TitiAssociation = TitiAssociation
data TotoAssociation = TotoAssociation

instance ItemContentAssociation TitiAssociation where
    type ContentType TitiAssociation = TitiContent
    type ItemType TitiAssociation = TitiItem
    handleContent TitiAssociation c@(TitiContent s) = TitiItem s - what string should be used here? if s, why also have c? - c

instance ItemContentAssociation TotoAssociation where
    type ContentType TotoAssociation = TotoContent
    type ItemType TotoAssociation = TotoItem
    handleContent TotoAssociation c@(TotoContent s) = TotoItem s - what string? - c

也就是说,这里的气味很不对劲。重复代码的数量让我怀疑您对如何从您喜欢的其他语言进行设置提出了错误的想法。不过,如果不进一步了解这些定义的动机,就很难说更多关于如何解决它的问题。

【讨论】:

太棒了!是的,很好发现。我有专业的 Java 背景,但对 Haskell(纯函数式编程)很感兴趣。 所以基本上我想要实现的是一个通用的 CRUD 服务器。基本上,我希望在单独的模块(例如库)中包含核心逻辑(即,通过向项目添加 ID 来发布内容并返回项目)和各种域(即内容为标题和正文的注释)或清单(其内容是标签列表)但是核心逻辑不应该关心它处理的类型,因为它只是添加 ID(比如序列化的哈希)。 @amaille 为什么不是像data Identified a = Identified identifier :: UUID, payload :: a 这样的单一参数化数据类型(也没有类或关联类型)就足够了? 其实,你让我退后了一步。我对这个问题思考得越多,我就越认为我没有把正确的责任放在应该承担的地方……我得好好想想。接受你的答案,因为它可能不是惯用的,但它很漂亮:) Identified a 绝对足够了,但是在同构应用程序的情况下,我不能将客户端的 a 类型限制为来自域(这里有点猜测)@DanielWagner

以上是关于我们如何在保持类型安全的同时通用地处理关联类型的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在 Swift 中创建具有 Self 或关联类型要求的通用计算属性,如果可以,如何?

如何使用泛型缩小 TypeScript 联合类型

我们应该在转换通用 json 响应时使用“any”吗

编码类型,如何确定[关闭]

CTSCLSCLR

如何从另一个组件调用一个函数,同时保持另一个组件在 Angular 中的通用性?