如何仅用Lens实现Tuple版map+sequence功能?求替代方案
现有一段依赖tuple库SequenceT类型类实现的Haskell函数f2:
import Data.Tuple.All -- tuple library import Control.Lens f2 :: (SequenceT a1 b, Each s a1 a2 b2) => s -> (a2 -> b2) -> b f2 tpl f = sequenceT $ tpl & each %~ f
该函数能实现元组上类似列表map+sequence的操作——先对元组每个元素应用指定函数,再按顺序执行返回的Applicative动作,最后返回包裹在Applicative上下文里的元组。示例调用如下:
> f x = print x >> pure x > f2 (1,2,3,4) f 1 2 3 4 (1, 2, 3, 4)
现询问两个问题:
- 能否仅通过Lens库实现功能完全相同的
f2,不依赖sequenceT? - 是否存在其他简单的替代实现方案?
一、仅用Lens库的实现方案
当然可以,而且实现比原代码更简洁。Lens的traverseOf each本身就封装了“映射+序列”的逻辑——traverseOf each f tpl会自动遍历元组的每个元素,应用函数f,然后按Applicative的规则执行动作并组合结果,和原函数f2的行为完全一致。
改写后的代码如下:
import Control.Lens f2 :: Each s a (f b) => s -> (a -> f b) -> f s f2 tpl f = traverseOf each f tpl
这里的traverseOf each等价于原代码的sequenceT . (each %~ f):each负责遍历元组元素,traverseOf则同时完成了元素映射和Applicative序列的工作,完全不需要依赖tuple库的SequenceT类型类。
二、其他替代实现方案
1. 借助GHC泛型推导(无第三方库依赖)
如果不想依赖任何第三方库,可以用GHC的DeriveGeneric扩展,通过泛型机制遍历元组元素并实现序列操作:
{-# LANGUAGE DeriveGeneric #-} import GHC.Generics class GSequence f where gsequence :: f (m a) -> m (f a) instance GSequence U1 where gsequence U1 = pure U1 instance (GSequence f, GSequence g) => GSequence (f :*: g) where gsequence (x :*: y) = (:*:) <$> gsequence x <*> gsequence y instance GSequence (K1 i c) where gsequence (K1 x) = K1 <$> x sequenceGeneric :: (Generic t, GSequence (Rep t)) => t -> m t sequenceGeneric = fmap to . gsequence . from f2 :: (Generic s, GSequence (Rep s)) => s -> (a -> m b) -> m s f2 tpl f = sequenceGeneric $ fmap f tpl
这个方案需要开启泛型扩展,但不需要引入任何第三方库,适合轻量场景。
2. 手动组合Applicative动作(固定长度元组)
如果你的元组长度是固定的,可以直接用Applicative的<*>操作符手动组合每个元素的动作,比如针对4元组:
f2_4 :: (a -> m b) -> (a,a,a,a) -> m (b,b,b,b) f2_4 f (w,x,y,z) = (,,,) <$> f w <*> f x <*> f y <*> f z
优点是完全不需要依赖任何扩展或库,但缺点是只能支持特定长度的元组,扩展性差。
3. 使用Template Haskell自动生成(支持任意长度元组)
可以借助tuple-th库,通过Template Haskell自动生成不同长度元组的sequence函数,这样既能支持任意长度的元组,又不需要手动编写泛型实例,不过需要引入这个新的第三方库。
内容的提问来源于stack exchange,提问作者RandomB

