【问题标题】:How to map over a long tuple of instances of the same class?如何映射同一类实例的长元组?
【发布时间】:2017-03-09 10:08:33
【问题描述】:

假设我们在 Haskell 中有一个 C 类,它有一个函数 f,它返回 R 类型的结果。 如果给定一个包含 C 类实例的长元组 t,那么我们如何才能很好地获得 f 在 t 的成员上的结果列表(或至少一个元组)?

请注意,元组很长,因此包括为每个元组成员键入内容的解决方案并不完美。我们不想输入那么多。

-- GIVEN --

data R = R  -- more constructors here

class C a where
  f :: a -> R

data A = A
instance C A where
  f _ = R  -- some fancy f here

data B = B
instance C B where
  f _ = R  -- some fancy f here

-- some other instances of C here

t = (A, B, B, A, B, A, B) -- a long tuple of instances of C


-- QUESTION: How to obtain l as below, but in the nicest way? --

l :: [R]
l = let (t1, t2, t3, t4, t5, t6, t7) = t
    in [f t1, f t2, f t3, f t4, f t5, f t6, f t7]

【问题讨论】:

  • 你为什么有这样一个元组?大小 > 3 的元组通常表明您已经将它们用于应该更好地完成的事情,例如记录,或者可能是嵌套的 2 元组
  • 这不是我的决定。我们可以比元组中的坐标更容易地映射记录的字段吗?
  • 不是本质上的,但您也许可以使记录多态以自然支持类型更改映射操作的方式。

标签: list class haskell tuples


【解决方案1】:

这是一个非常标准的泛型编程用例。这是一个非常常见的用例,有些库不必编写通用实现。

one-liner

import Generics.OneLiner

l :: [R]
l = gfoldMap (For :: For C) (pure . f) t :: [R]

gfoldMap 是数据类型字段的折叠,假设它们都是给定类型类的所有实例,允许一般收集结果,这里是C

gfoldMap (For :: For C) :: (forall a. C a => a -> [R]) -> (A, B, B, A, B, A, B) -> [R]

请注意,这需要Generic 的实例,由基本包派生到最多7 个元组。 您可以定义自己的元组,并为其派生Generic

出于各种原因,您可能希望保留数据类型的结构,而不是在列表中收集结果。在撰写本文时,one-liner 在这方面仍然有点僵化,因为它不处理“类型更改遍历”(从 (A, B, B, A, B, A, B)(R, R, R, R, R, R, R))。


product-profunctors

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}

import Data.Profunctor
import Data.Profunctor.Product
import Data.Profunctor.Product.Default

我们定义了一种新类型,不会用孤儿污染环境。

newtype P a b = P { unP :: a -> b } deriving
  (Profunctor, ProductProfunctor)

我们将f 声明为Default 映射ABR 的方式。

instance Default P A R where def = P f
instance Default P B R where def = P f

库隐式将此扩展到元组,将AB 的任何元组映射到R 的对应元组。

-- Type signature required
t'_ :: (R, R, R, R, R, R, R)
t'_ = unP def t

您可能不想键入类型签名。由于类型类恶作剧,无法推断。但是,它可以根据输入的类型来计算。因此,您可以定义一个类型族(类型级函数),它在元组类型中用R 替换出现的AB。实际上,任何看起来像F a b c d e f(其中F 是类型构造函数)的类型都将转换为F R R R R R R

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE UndecidableInstances #-}

type family Rify (a :: k) where
  Rify (f a) = Rify f a
  Rify a = a

专门化def(它有一个类型推断不友好的类型)来使用这个家族:

defP :: Default P a (Rify a) => P a (Rify a)
defP = def

-- Type signature now optional
t' :: (R, R, R, R, R, R, R)
t' = unP defP t

产品推广者之二

你也可以通过选择正确的profunctor来获得一个列表。

顾名思义,product-profunctors 以一般方式与ProductProfunctor 一起工作。这个 profunctor 在列表中收集 R 值。它相当于Control.Applicative中的Const [R]

newtype Q a b = Q { unQ :: a -> [R] }

instance Profunctor Q where dimap f _ (Q q) = Q (q . f)

instance ProductProfunctor Q where
  purePP _ = Q (const [])
  Q x **** Q y = Q (\a -> x a ++ y a)

  -- Older versions of product-profunctor use these two instead.
  empty = Q (const [])
  Q x ***! Q y = Q (\(a, b) -> x a ++ y b)

-- or
--
-- newtype Q a b = Q (Star (Const [R]) a b)
--   deriving (Profunctor, ProductProfunctor)
--
-- unQ :: Q a b -> a -> [R]

定义默认操作。

instance Default Q A b where def = Q (pure . f)
instance Default Q B b where def = Q (pure . f)

再一次,这些被隐式组合成“遍历”元组。

-- The second parameter doesn't actually matter, but
-- the type-checker doesn't know it so we put something for it
-- to infer. Could be `Q a ()`, anything that's not ambiguous.
defQ :: Default Q a a => Q a a
defQ = def

t'' :: [R]
t'' = unQ defQ t

这实际上与 one-liner 在内部的工作方式非常相似,尽管它目前使用自己的 ProductProfunctor 风格。

【讨论】:

    【解决方案2】:

    我会做一些类似的事情

    {-# LANGUAGE TypeFamilies, DefaultSignatures #-}
    
    class MultiC cs where
      type MultiR cs :: *
      type MultiR cs = R
      multif :: cs -> MultiR cs
      default multif :: cs -> R
      multif = f
    
    instance MultiC A
    instance MultiC B
    -- ...
    
    instance (MultiC x, MultiC y) => MultiC (x,y) where
      type MultiR (x,y) = (MultiR x, MultiR y)
      multif (x,y) = (multif x, multif y)
    

    那你就可以了

    t :: ((A, (B, B)), ((A, B), (A, B)))
    t = ((A, (B, B)), ((A, B), (A, B)))
    
    l :: ((R, (R, R)), ((R, R), (R, R)))
    l = multif t
    

    您原则上也可以将其扩展为拥有(伪代码)

    instance (MultiC α, MultiC β, MultiC γ ... MultiC ω)
                 => MultiC (α,β,γ ... ω) where
      type MultiC (α,β,γ ... ω) = (MultiR α, MultiR β ... MultiR ω)
      multif (α,β,γ...ω) = (multif α, multif β, multif γ ... multif ω)
    

    但正如我评论的那样,大的扁平元组并不是一个好主意,因为 Haskell 没有适当的方法来抽象它们。

    【讨论】:

      【解决方案3】:

      除了夏立耀的回答中列出的方法,这里有一个使用generics-sop的解决方案:

      {-# language DeriveGeneric #-}
      {-# language FlexibleContexts #-}
      {-# language TypeFamilies #-}
      {-# language DataKinds #-}
      {-# language TypeApplications #-} -- for the Proxy
      
      import Generics.SOP
      
      tTol :: (Generic r, All2 C (Code r)) => r -> [R]
      tTol = hcollapse . hcliftA (Proxy @C) (\(I a) -> K (f a)) . from
      

      此解决方案适用于元组、记录和求和类型,只要该类型具有 Generics.SOP.Generic 实例,并且所有字段都具有 C 实例。

      【讨论】:

        猜你喜欢
        • 2022-12-07
        • 2020-07-29
        • 2021-10-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-02
        • 1970-01-01
        • 2017-09-25
        相关资源
        最近更新 更多