【问题标题】:How can I best represent a cartesian product of some fixed dimension?我怎样才能最好地表示某个固定维度的笛卡尔积?
【发布时间】:2018-07-22 10:57:39
【问题描述】:

根据@leftaroundabout 的建议,这个问题被严格重写。在编辑历史记录中可能会看到更早的版本。

Haskell 以促进思考而闻名,允许对数学抽象进行更直接的编码。笛卡尔积是许多人从小就熟悉的非常基本的心理对象。然而,在 Haskell 中几乎没有它的类型。我想我需要一个,让我的思想流动,如果没有别的。 (虽然这篇文章实际上是受到我手头的一些实际代码的启发。)然后让我们对笛卡尔的东西形成一个共同的理解(我将把它称为笛卡尔短)是。

给定一个长度为d :: Int 的集合序列(例如[[1,2], ['a', 'b']]),我希望它们的元素的所有组合都在短距离内。这意味着对它们进行操作,就好像它们在通常的 Functor、Foldable、Traversable、Monoid 等中一样。实际上,我们可以将任何笛卡尔表示为适当嵌套的元组列表:

type Lattice = [[(x, y)]]

type WeightedLattice = [[(x, y, w)]]

zipWith2 :: (a -> b -> c) -> [[a]] -> [[b]] -> [[c]]
zipWith2 f = zipWith (zipWith f)

fmap2 :: (Functor f, Functor g) => (a -> b) -> g (f a) -> g (f b)
fmap2 = fmap . fmap

sequence2 :: (Traversable s, Traversable t, Monad m) => t (s (m a)) -> m (t (s a))
sequence2 = sequence . fmap sequence

对于任何嵌套深度,都可以手动编写类似的结构。

我们现在介绍a笛卡尔和初始笛卡尔之间的区别:

  • 一个 n 维笛卡尔是从一个异构的集合序列中构造的,方法是从每个集合中获取一个元素,然后将它们有序地与适当类型的函数组合。这样,签名为 [Int, Char, Bool] 的笛卡尔可以由如下函数形成:

    f :: Int -> Char -> Bool -> Either Char Int
    f i c b = if b then Left c else Right i 
    
  • 初始笛卡尔是用匹配元组的元组构造函数形成的:

    initial :: Int -> Char -> Bool -> (Int, Char, Bool)
    initial = (,,)
    

很容易看出,我们可以将表示为嵌套列表的初始笛卡尔坐标转换为具有类似嵌套深度的任何其他笛卡尔坐标,其函数类似于:

    (fmap . ... . fmap) (uncurryN f)

但是,我们可能不会总是回来;实际上,很难从Right 3 中恢复正确的Char。因此,初始笛卡尔坐标可以用来代替任何特定笛卡尔坐标,但并非总是相反。

作为一个例子,我们可以使用上面定义的Lattice 类型来可视化一个字段,为空间中一些规则分布的点计算它的值。我们将使用一个分配坐标值的函数来做到这一点。可能有任意数量的这样的函数,在相同的点上描述不同的字段,每个函数对应一个相似维度的格子。但是只有一个初始格子,它只包含坐标。

但是,我们的嵌套列表编码有其缺点。除了令人厌烦地为每个下一个维度拼出所有必要的函数之外,它是不安全的:没有什么可以使您免于将 128 x 64 矩阵与 64 x 128 矩阵混淆并将它们压缩在一起,最终得到 64 x 64 一个代替;元组中事物的顺序可能对应于列表嵌套的顺序,也可能不对应。另一方面,类型系统对你很不利,不允许像foldr (.) id [replicate d concat] 这样可以节省一些痛苦的东西。一点都没有。

但对这个系统最失望的根源在于,它不以任何明显的方式支持笛卡尔最基本的直觉:它的 Monoid 实例。它是一个属性,使我们可以将一个点视为不是一个,不是一些,而是任意数量的p 属性,可以轻松添加、组合或丢弃它们——实际上就像列表的元素一样。被钉在一定深度的嵌套和一定的元组数量上,你的翅膀就会被剪掉。笛卡尔积是 Set 范畴中的 Monoid 是范畴论的一个基本事实,但是我们可以在任意类型元组的任意嵌套列表上定义 Monoid 吗?

因此,编写正确的笛卡尔坐标的挑战涉及以下目标:

  • 任何维度。列表、矩阵和任何其他有限维空间都应该有类似的接口。一些常用的Data.List 函数应该可以实现。

  • 类型安全。也就是说,在类型系统中编码给定笛卡尔的类型和维度。例如,如果我形成一个类似[1..3] x ['a', 'b'] 的空间,以及另一个类似[1,2] x ['a'..'c'] 的空间,它们应该具有不同的可读类型,而不是压缩在一起。

  • 由于笛卡尔是由维度的选择决定的,任何两个笛卡尔都可以像它们维度的列表一样组合起来。例如:

    Cartesian [[1..3], ['a', 'b']] <> Cartesian [[True, False]]
    

    -- 应该是一样的东西:

    Cartesian [[1..3], ['a', 'b'], [True, False]]
    

    -- 就像它们的生成列表一样。

  • 应该有一些关于初始笛卡尔坐标和放置在它上面的装饰的概念,这样点的坐标就不会丢失,除非强制丢失。例如,Lattice 的点坐标应与其描述的字段的派生属性分开存储。比如说,如果描述它们的格“匹配”,我们就可以获得场的叠加。

  • 初始笛卡尔应该是 Monoid。

我画了一些至少有点可用的可怜的东西,我会马上把它作为答案发布,但对于上述大多数点,我都不知所措。它必须采取一些类型的诡计。我很欣赏有关如何制作它的任何想法。

【问题讨论】:

  • 如果IArray 为您工作,会有一个实例吗?你可以选择它的索引(Ix)作为一个元组,但是由于数组是线性存储的,你有简单的Traversable等实例。
  • 这似乎是一个有趣的问题,但我发现很难准确理解它的含义。您能否添加更多代码/伪代码/伪签名?特别是,与元组的关系是什么——你可以得到最接近的元组,为什么这对你来说不满意?
  • 这听起来很像一篇(我认为相当有名的)论文,我不记得它的标题,关于使用类型类和类型组合器构造一个可扩展的表达式类型。基本上,它是一个开放副产品的模型。你可以做同样的事情,但是构建一个开放的产品。我希望这里的其他人可以帮助我了解论文的名称
  • 啊,在这里,Data Types A La Carte。我认为您可以使用类似的技术来解决您的问题。
  • 也许是个愚蠢的建议,但如果您只想从一些列表到元组列表,并且每个列表都有固定数量,您可以使用应用程序:f = liftA2 (,) 然后f [1,2] [T, F] 是@ 987654346@,您也可以使用 liftA3 (,,)。显然,如果您想使用任意 d 执行此操作,这将不起作用。

标签: haskell data-structures cartesian-product


【解决方案1】:

这个问题相当模糊,但看起来您可能对vinyl 样式的记录感兴趣。我的定义与vinyl 有点不同;用你喜欢的。

{-# language DataKinds, PolyKinds, TypeOperators, GADTs #-}
module Cart where
import Data.Kind (Type)
import Data.Functor.Identity

infixr 4 :<
data Rec :: [k] -> (k -> Type) -> Type where
  Nil :: Rec '[] f
  (:<) :: f a -> Rec as f -> Rec (a ': as) f

newtype HList xs = HList (Rec xs Identity)

prod :: Rec as [] -> [HList as]
prod = map HList . go
  where
    go :: Rec as [] -> [Rec as Identity]
    go Nil = [Nil]
    go (xs :< xss) = [ Identity x :< r | x <- xs, r <- go xss]

使用适当的 Show 实例(简单但有点烦人),你会得到类似的东西

> prod $ [3,4,5] :< ["hello", "goodbye"] :< ['x'] :< Nil

[ H[3,"hello",'x'], H[3,"goodbye",'x'], H[4,"hello",'x']
, H[4,"goodbye",'x'], H[5,"hello",'x'], H[5,"goodbye",'x'] ]

这个版本的prod 可能有点太具体了,因为它只适用于Identity 和列表。这是一个简单的概括,其中遍历函数“拆分”了Rec 的基本函子,我们使用任意的Applicative 而不仅仅是[]

class Trav (t :: (k -> Type) -> Type) where
  trav :: Applicative g => (forall a. f a -> g (h a)) -> t f -> g (t h)

instance Trav (Rec as) where
  trav f Nil = pure Nil
  trav f (xs :< xss) = (:<) <$> f xs <*> trav f xss

这与Data.Vinyl.rtraverse 类似,因为我终于笨手笨脚地认出了。


此类记录不会形成Monoid,因为无法输入mappend。但你当然可以附加它们:

type family (++) xs ys where
  '[] ++ ys = ys
  (x ': xs) ++ ys = x ': xs ++ ys

(><) :: Rec xs f -> Rec ys f -> Rec (xs ++ ys) f
Nil >< ys = ys
(x :< xs) >< ys = x :< (xs >< ys)

这表现得很好。特别是,您可以追加记录然后遍历它们,或者遍历它们然后追加结果。

trav f (xs >< ys) = (><) <$> trav f xs <*> trav f ys

您还可以按原则重新排列它们(类似于您的气泡设备)。类型

forall f k (as :: [k]) (bs :: [k]). Rec as f -> Rec bs f

可以赋予任何重新排列 Rec 的函数,而无需关心其中的内容。


既然你提到了映射:

class Functor1 (t :: (k -> Type) -> Type) where
  map1 :: (forall x. f x -> g x) -> t f -> t g

instance Functor1 (Rec as) where
  map1 f = runIdentity . trav (\x -> Identity (f x))

压缩也可以:

rzip :: (forall x. f x -> g x -> h x)
     -> Rec as f -> Rec as g -> Rec as h
rzip f Nil Nil = Nil
rzip f (x :< xs) (y :< ys) = f x y :< rzip f xs ys

【讨论】:

  • 我重新提出了这个问题;现在不那么模糊了吗?
  • @Kindaro,不,我还是很迷茫。
【解决方案2】:

这样的类型可能会带走一些痛苦:

type A e = Array Int e

data Cartesian v = Cartesian
    { _dimensions :: [Int]
    , _values :: A v
    } deriving (Show, Eq, Ord, Functor, Foldable, Traversable)

-- # Some helper functions.

autoListArray :: [a] -> A a
autoListArray xs = listArray (0, pred (length xs)) xs

-- | Get elements of an array such that they all belong to the
--   congruence class c modulo n.
getByCongruentIndices :: Int -> Int -> A v -> [v]
getByCongruentIndices n c arr =
    let (low, high) = bounds arr
    in  (arr !) <$> [low + c, low + c + n.. high]

arr :: [v] -> A v
arr = autoListArray

unarr :: A v -> [v]
unarr = elems

congr :: Int -> Int -> A v -> [v]
congr = getByCongruentIndices

congr0 :: Int -> A v -> [v]
congr0 n = congr n 0

我现在将检查它。

  • 函子、可折叠、可遍历:开箱即用的神奇推导。

  • 创建:您可以通过将unicons 逐步增长来创建任何您想要的笛卡尔坐标。

    -- | Consruct a uni-dimensional Cartesian.
    uni :: [v] -> Cartesian v
    uni vs = Cartesian { _dimensions = [length vs], _values = arr vs }
    
    -- | Dimension increment.
    cons :: (u -> v -> w) -> [u] -> Cartesian v -> Cartesian w
    cons f xs Cartesian{..} = Cartesian
        { _dimensions = length xs: _dimensions
        , _values = arr [ x `f` y | x <- xs, y <- unarr _values ]
        }
    
  • 销毁:您可以使用uncons 剥离笛卡尔坐标系的层。

    -- | Dimension decrement.
    uncons :: (u -> (v, w)) -> Cartesian u -> Maybe ([v], Cartesian w)
    uncons _ Cartesian { _dimensions = [] } = Nothing
    uncons f Cartesian { _dimensions = (_: ds), _values = xs } =
        let ys = fmap (fst . f) . congr0 (product ds) $ xs
            zs = fmap (snd . f) . take (product ds) . unarr $ xs
        in  Just (ys, Cartesian { _dimensions = ds, _values = arr zs })
    
  • 转置:您可以使用(↑) 更改维度的顺序 气泡设备。我没有证据,但我几乎可以肯定你可以获得任何你想要的换位。特别是,(↑) x 1 等价于Data.List.traverse

    -- | Bubble: apply a cycle from 0 to (i - 1) to the dimensions. That is, make the i-th dimension 
    --   the first. I believe bubbles to be the generators of the symmetric group.
    (↑) :: Cartesian u -> Int -> Cartesian u
    Cartesian{..} ↑ i =
        let d  = product . drop i $ _dimensions
            ds = take i _dimensions ++ drop (succ i) _dimensions  -- Delete the i-th.
        in  Cartesian
                { _dimensions = ds
                , _values = arr . concat $ ($ _values) <$> (congr d <$> [0..pred d])
                }
    
  • consuncons(↑) 允许您再次切片和组合任意两个笛卡尔或其部分。但是还有一种方法可以直接组合笛卡尔:

    appendWith :: (u -> v -> w) -> Cartesian u -> Cartesian v -> Cartesian w
    appendWith f x y = Cartesian { _dimensions = _dimensions x ++ _dimensions y
                                 , _values = arr [ x `f` y | x <- unarr (_values x), y <- unarr (_values y) ]
                                 }
    
  • 你也可以压缩东西:

    glue f x y | _dimensions x == _dimensions y
                    = Cartesian
                        { _dimensions = _dimensions x
                        , _values = arr $ zipWith f (unarr $ _values x) (unarr $ _values y)
                        }
               | otherwise = undefined
    

不利的一面是,您必须显式地提供一个二进制函数,该函数可以在任何地方组合您在笛卡尔中的类型。这通常是一个元组构造函数,但你不限于它。

我在定义 Monoid 的实例时遇到了麻烦,尤其是 mempty 部分,而 initial 甚至不在这附近。

【讨论】:

    猜你喜欢
    • 2019-05-26
    • 1970-01-01
    • 1970-01-01
    • 2018-12-06
    • 2021-04-26
    • 2014-08-30
    • 1970-01-01
    • 2016-02-24
    • 1970-01-01
    相关资源
    最近更新 更多