以相同方式处理单个和多个元素("transparent" 映射运算符)

Treating single and multiple elements the same way ("transparent" map operator)

我正在开发一种应该简单、直观和简洁的编程语言(是的,我知道,我是第一个想到这个目标的人 ;-))。 为了简化容器类型的使用,我正在考虑的功能之一是使容器元素类型的方法在容器类型本身上可用,基本上作为调用 map(...[=13 的快捷方式=] 方法。这个想法是,处理多个元素与处理单个元素没有什么不同:我可以将 add(5) 应用于单个数字或整个数字列表,而且我不必编写略有不同的代码对于“一个”与“多个”场景。

例如(Java伪代码):

import static java.math.BigInteger.*; // ZERO, ONE, ...
...
// NOTE: BigInteger has an add(BigInteger) method
Stream<BigInteger> numbers = Stream.of(ZERO, ONE, TWO, TEN);
Stream<BigInteger> one2Three11 = numbers.add(ONE); // = 1, 2, 3, 11
// this would be equivalent to:  numbers.map(ONE::add)

据我所知,这个概念不仅适用于“容器”类型(流、列表、集合...),而且更普遍地适用于所有具有 map 方法(例如,可选项、状态单子等)。 实现方法可能更符合编译器提供的语法糖,而不是通过操纵实际类型(Stream<BigInteger> 显然不会扩展 BigInteger,即使它做了“map-add " 方法必须 return Stream<BigInteger> 而不是 Integer,这与大多数语言的继承规则不兼容。

关于这样一个提议的功能,我有两个问题:

(1) 提供此类功能的已知注意事项有哪些?容器类型和元素类型之间的方法名称冲突是我想到的一个问题(例如,当我在 List<BigInteger> 上调用 add 时,我是想向列表中添加一个元素还是想要向列表的所有元素添加一个数字?参数类型应该澄清这一点,但这可能会变得棘手)

(2) 是否有任何现有语言提供这种功能,如果有,它是如何在幕后实现的?我做了一些研究,虽然几乎每一种现代语言都有类似 map 运算符的东西,但我找不到任何一种语言,其中一对多的区别是完全透明的(这让我相信有我在这里忽略了一些技术困难)

注意:我在不支持可变数据的纯功能上下文中查看此内容(不确定这对回答这些问题是否重要)

你有面向对象的背景吗?这是我的猜测,因为您将 map 视为属于每个不同“类型”的方法,而不是考虑属于 functor.

类型的各种事物

如果 map 是每个单独函子的 属性,比较 TypeScript 将如何处理:

declare someOption: Option<number>
someOption.map(val => val * 2) // Option<number>

declare someEither: Either<string, number>
someEither.map(val => val * 2) // Either<string,number>
someEither.mapLeft(string => 'ERROR') // Either<'ERROR', number>

您还可以创建一个常量来表示每个单独的仿函数实例(选项、数组、标识,或者 async/Promise/Task 等),其中这些常量具有 map 作为方法。然后有一个独立的 map 方法,它采用这些“仿函数常量”之一、映射函数和起始值,以及 returns 新包装值:

const option: Functor = {
  map: <A, B>(f: (a:A) => B) => (o:Option<A>) => Option<B>
}
declare const someOption: Option<number>
map(option)(val => val * 2)(someOption) // Option<number>

declare const either: Functor = {
  map: <E, A, B>(f: (a:A) => B) => (e:Either<E, A>) => Either<E, B>
}
declare const either: Either<string,number>
map(either)(val => val * 2)(someEither)

本质上,您有一个仿函数“映射”,它使用第一个参数来标识您要映射的类型,然后传入数据和映射函数。

但是,使用 Haskell 等适当的函数式语言,您不必传入该“函子常量”,因为该语言会为您应用它。 Haskell 这样做。不幸的是,我的 Haskell 不够流利,无法为您编写示例。但这是一个非常好的好处,意味着更少的样板文件。它还允许您以“无点”风格编写大量代码,因此,如果您使用自己的语言进行重构,那么重构就会变得容易得多,这样您就不必手动指定所使用的类型来利用 map/chain/bind/等等

考虑一下您最初编写的代码是通过 HTTP 进行大量 API 调用。所以你使用了一个假设的异步 monad。如果您的语言足够聪明,可以知道正在使用哪种类型,您可以编写一些代码,例如

import { map as asyncMap }

declare const apiCall: Async<number>
asyncMap(n => n*2)(apiCall) // Async<number>

现在你改变你的API,让它读取一个文件,你让它同步:

import { map as syncMap }

declare const apiCall: Sync<number>
syncMap(n => n*2)(apiCall)

看看您如何更改多段代码。现在假设您有数百个文件和数万行代码。

使用无点风格,你可以做到

import { map } from 'functor'
declare const  apiCall: Async<number>
map(n => n*2)(apiCall)

并重构为

import { map } from 'functor'
declare const  apiCall: Sync<number>
map(n => n*2)(apiCall)

如果您的 API 电话位于一个集中位置,那将是您唯一需要更改的地方。其他一切都足够智能,可以识别您正在使用的仿函数并正确应用映射。

  1. 就您对名称冲突的担忧而言,无论您的语言或设计如何,这个问题都会存在。但是在函数式编程中,add 将是一个组合器,它是您的映射函数传递给您的 fmap(Haskell 术语)/map(很多 imperative/OO 语言' 学期)。您用来向 array/list 的尾端添加新元素的函数可能称为 snoc(“construct”中的“cons”向后拼写,其中 cons 将元素添加到您的数组;snoc 追加)。您也可以将其命名为 pushappend.

  2. 就您的一对多问题而言,它们不是同一类型。一种是list/array型,一种是identity型。处理它们的底层代码会有所不同,因为它们是不同的仿函数(一个包含单个元素,而一个包含多个元素。

我想您可以通过自动将单个元素包装为单个元素列表然后仅使用该列表来创建一种不允许单个元素的语言 map。但是要让两个截然不同的东西看起来一样,似乎需要做很多工作。

取而代之的是,您将单个元素包装为标识,将多个元素包装为 list/array,然后数组和标识有自己的仿函数方法的底层处理程序 map 可能会更好。