【问题标题】:In Haskell, how to fmap between distinct Traversables?在 Haskell 中,如何在不同的 Traversable 之间进行 fmap?
【发布时间】:2019-11-16 14:34:17
【问题描述】:

我们知道,fmap 的签名是(a -> b) -> f a -> f b,其中fFunctor

为了尽可能通用,更好地分解代码,人们可能希望将“事物列表”映射到另一个可能不同的“事物列表”,这似乎很自然。凭直觉,我不明白为什么它不可行或不可取。

我正在寻找的是一个函数gmap,其行为与fmap 相同,但具有gmap :: (a -> b) -> (f a) -> (g b) 的签名,我允许到达和离开容器不同。

我不确定这在fgFunctors 的一般情况下是否有意义,但是“事物列表”的概念听起来更本质上是由Traversable 类捕获的,假设我最感兴趣的是迭代数据。

所以也许签名应该是gmap :: (Traversable f, Traversable g) => (a -> b) -> (f a) -> (g b)

即使gf 的性质不同,它仍然是可以从左到右遍历的东西,所以仍然感觉应该能够将f 的第k 个访问元素映射到g 的第 k 个访问元素。

假设我的思路没有出错,Haskell中有这样的功能吗?

本质上,我的问题是,您如何在 Haskell 中以最简洁和优雅的方式将一个类似列表的东西转换为另一个?

【问题讨论】:

  • 如何将列表变成树?两者都是可遍历的。即使一棵树可以从左到右遍历,知道它的遍历也不允许以独特的方式重建树。我们可以生成“一棵树”,但这需要一个非常随意的选择。像 foldable/traversable 这样的类本质上描述了消费容器的方式,而不是生产容器。也许你想要另一个课程。
  • @chi 是的,它可能不是正确的类,那么我会为此目的寻找最通用的类​​。
  • 假设存在这样一个类,它是否只支持类似列表的容器?你能想象任何其他可以从列表中以有意义的方式创建的容器吗?
  • 我不确定,但这看起来不像fmap 的组合和函子之间的自然转换..?
  • Yeah I thought of natural transformations, somehow what I want is mapping a functor to another functor.. - 虽然我对 Haskell 中的自然变换不是很熟悉,但在数学中(“从函子到另一个函子的映射”)准确地是什么一个自然的转变是。

标签: haskell traversal functor


【解决方案1】:

我们在 Haskell 中经常使用的一个技巧是试图用它来产生“假”——也就是产生一个类型的值

data Void

没有构造函数的类型。如果可以使用您的类型签名生成Void 类型的值,那么您的类型签名将无法实现。这也被称为“reducto ad absurdum”或“反证法”。如果您的类型签名允许我们生成Void...类型的值...那么“显然”您的类型签名是废话,无法实现。

在这种情况下,我们“返回”了一个Traversable 实例,所以让我们使用Traversable,比如(,) Void

instance Traversable ((,) w) where
    traverse f (x, y) = (x,) <$> f y

现在让我们使用f 作为任何旧仿函数。它可以是任何东西......让我们使用Maybe,因为似乎每个人都已经理解它了。

那么,你可以这样写:

gmap :: (a -> b) -> Maybe a -> (Void, b)

哦,不,这不对……看起来使用 gmap 你可以通过传入任何旧的东西来创建Void

gmap :: (() -> ()) -> Maybe () -> (Void, ())

所以现在我创建Void的策略:

bad :: Void
bad = fst (gmap id Nothing)

因为Void 没有构造函数,所以不应该存在bad :: Void 类型的值(忽略诸如无限循环或偏函数之类的东西)。所以,如果仅仅存在 if gmap 就可以让我们创建一个 Void 类型的值...那一定意味着 gmap 不能以你给的形式存在。


对于您更普遍的问题,Traversable 工作方式的“原因”是它只能修改 结构。它不能创建它们。在这里,你想创建一个g b 的值,但是Traversable 不能“创建”它,它只能“转换”它。您的误解可能来自您认为Traversable 是一个“类似列表”的类型类:它不是,完全不是。使用 [] 作为原型可能会让你误入歧途。

我的“典型”Traversable 想象类型类的属性是Map k,来自容器Data.MapMap 不是一个,而是相关的值用钥匙。对其进行的任何操作都必须能够尊重此关联属性...而不是将其视为没有额外结构的大列表。

所以 可能是这样的:

replace :: (Foldable f, Traversable g) => f a -> g b -> g a

g b 的所有值都替换为f a 的所有值。如果您正在寻找练习,这实际上是一种有趣的写作。基本上,replace保持g a 相同的结构,但只是替换 .所以你可以从f a“创建”一个g a,只要你有一个“示例g b”,可以这么说。如果你使用类似的东西:

replace :: [a] -> Map k b -> Map k a

然后replace 将用列表中的项目替换第二个映射中的所有值,并将它们替换为正确的键值。

然后你可以写:

gmap :: (Traversable a, Traversable g) => (a -> b) -> f a -> g c -> g b

在哪里你要复制的结构的“示例”g a

最接近能够在 Haskell 的常见类型类中“构造”结构的方法是 IsList,来自 https://hackage.haskell.org/package/base-4.12.0.0/docs/GHC-Exts.html#t:IsList

这个类型类给你两个函数,fromListtoList,所以你可以这样写:

throughIsList :: (IsList l, IsList m, Item l ~ Item m) => l -> m
throughIsList = fromList . toList

让它在Functors 上运行:

gmap :: (IsList (f a), IsList (g b), Item (f a) ~ a, Item (g b) ~ b) => (a -> b) -> f a -> g b
gmap f = fromList . map f . toList

现在的问题是大多数Functors 不是IsList 的实例...而且许多实际实例不是全部。所以它对大多数Functors 来说不太有用。


所以最后我认为没有任何令人满意的答案。如果您正在做的事情依赖于有一个好的答案这一事实(除了“否”的答案)......也许我可以问一下您的“最终目标”是什么?你打算用这种类型做什么?

(例如,在 90% 的情况下,人们会问诸如“有什么方法可以转换 monad”之类的问题,通常他们不想在 一般,但他们有特定的类型。)

【讨论】:

  • 不错的答案。我认为replace 的签名不够通用:replace :: (...) =&gt; f a -&gt; g b -&gt; g a。或者g (),如果我们真的想明确说明我们只是在使用它的形状。
  • @Justin 不,我并不想实现特定的目标。我觉得我的问题偏离了我真正想知道的内容。也许 Traversable 不适合这里,我不想让人们专注于它。有不同种类的列表,例如向量、数组、列表。我想知道这些结构的共同点是什么。它们有什么共同点使它们“像列表一样”?是否有表达这一点的 Haskell 类型?如果可以,这种通用的“类列表”类型可以用于在不同的“类列表”类型之间进行转换吗?
  • 我应该重新发布一个问题。我认为无论如何可能没有类似列表的类型..它太具体而不能成为基础的一部分。无论如何使用这个替换功能似乎已经足够了
  • @J.M.如果您正在寻找什么单位向量、数组、列表等,那么IsList 可能是一个开始。但是一个更有原则的方法可能是 mono-traversable 库,它为各种类型的具有不同程度的特异性/功能的“容器”提供了类型类。
【解决方案2】:

您可以创建自己的类型类,以及从一个函子到另一个函子的方法,这是将列表转换为树的一种方法的示例,但您可以使用任何您认为正确的方法来解决您的问题。

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}

class (Functor f, Functor g) => GFunctor f g where
  toG :: f a -> g a
  gmap :: (a -> b) -> (f a) -> (g b)
  gmap fn functor = toG $ fmap fn functor

data Tree a = Leaf | Node (Tree a) a (Tree a) deriving Show

instance Functor Tree where
  fmap f Leaf = Leaf
  fmap f (Node t1 x t2) = Node (fmap f t1) (f x) (fmap f t2)

instance GFunctor [] Tree  where
  toG [] = Leaf 
  toG [x] = Node Leaf x Leaf
  toG (x:xs) = Node (toG $ (takeHalf xs)) x ((toG $ dropHald xs))

takeHalf xs = take ((length xs) `div` 2) xs
dropHald xs = drop ((length xs) `div` 2) xs

res :: Tree Int
res = gmap (+1) [1,2,3,4,5]

输出:

   res
=> Node (Node Leaf 3 (Node Leaf 4 Leaf)) 2 (Node Leaf 5 (Node Leaf 6 Leaf))

【讨论】:

    【解决方案3】:

    我认为您可以通过fmap 和自然转换来做到这一点。这是一个使用 natural-transformations 包的示例,只是为了演示这个想法。

    我想到的第一个自然转变是safeHead

    safeHead :: [a] -> Maybe a
    safeHead [] = Nothing
    safeHead (x:_) = Just x
    

    使用 natural-transformations 包运行 GHCi:

    Prelude Control.Natural> t = wrapNT safeHead
    Prelude Control.Natural> t # [1..3]
    Just 1
    Prelude Control.Natural> t # []
    Nothing
    Prelude Control.Natural> fmap show (t # [1..3])
    Just "1"
    Prelude Control.Natural> fmap show (t # [])
    Nothing
    

    那么,对于t 的自然转换,像fmap f . (t #) 这样的东西会做你想做的吗?

    (这里,我想象f是一个普通函数a -&gt; b。)

    【讨论】:

    • 我没有投反对票,但似乎提问者正在寻找适用于 all f 和 g 的通用版本,而不是特定的 f 和 g(如 safeHead )
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-02
    • 2015-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-08
    相关资源
    最近更新 更多