【问题标题】:Treating single and multiple elements the same way ("transparent" map operator)以相同的方式处理单个和多个元素(“透明”映射运算符)
【发布时间】:2021-04-03 01:12:15
【问题描述】:

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

例如(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&lt;BigInteger&gt; 显然没有扩展BigInteger,即使它这样做了“map-add”方法也会有返回 Stream&lt;BigInteger&gt; 而不是 Integer,这与大多数语言的继承规则不兼容)。

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

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

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

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

【问题讨论】:

  • 函子的基本思想不是实现 processing-many-elements-in-a-containter 的概念。这是关于将一​​个或多个值放入结构中,而没有可能再次将其/它们取回。因此,最重要的是,仿函数对您的代码施加了限制。为什么我们首先要这样做?因为如果我们永远无法检索数据,它可能是不纯的,即不纯计算的结果,而不会损害我们 porgram 的纯领域。
  • 如果函数计算可能是一种有效的计算,则没有充分的理由使其隐含,因为隐含的副作用已经贯穿命令式代码。你想要的是明确有效的计算,这正是 FP 的承诺之一。
  • @scriptum 感谢您对函子“关于将一个或多个值放入结构中,而没有可能再次取回它/它们”的评论。虽然它没有直接回答我的问题,但它帮助我理解了以前我无法理解的函子的一个新方面。

标签: collections functional-programming programming-languages functor language-design


【解决方案1】:

您是否来自面向对象的背景?这是我的猜测,因为您将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>

您还可以创建一个常量来表示每个单独的仿函数实例(选项、数组、标识、异步/承诺/任务等),其中这些常量将map 作为方法。然后有一个独立的map 方法,它采用这些“函子常量”之一、映射函数和起始值,并返回新的包装值:

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)

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

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

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

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(许多命令式/OO 语言的术语)。用于将新元素添加到数组/列表尾端的函数可能称为 snoc(“cons”来自“construct”的反向拼写,其中 cons 将元素添加到数组中;snoc 追加)。你也可以称它为pushappend

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

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

相反,将单个元素包装为标识,将多个元素包装为列表/数组,然后数组和标识有自己的函子方法 map 的底层处理程序的方法可能会更好.

【讨论】:

  • 感谢您周到的回答,@user1713450。你提出了很多我仍然需要消化的好观点。尤其是你对我的目标的评论,即“让两个截然不同的东西看起来一样”。我确实有很多例子,其中相同的事情会从根本上简化事情,但我正在研究反例。我也在研究 Haskell 和 TypeScript 如何处理函子。我怀疑 Haskell 对此有一个很好的解决方案,但是,唉,我对那种语言也不够流利......
  • Haskell 的解决方案将比 TypeScript 的解决方案更好,因为 Haskell 的设计考虑了这种事情。使用 TypeScript,您增加了冗长性,因为您必须传递额外的数据才能启用类似于 point-free 样式 的东西,我认为您正在考虑这一点。
猜你喜欢
  • 1970-01-01
  • 2021-12-18
  • 1970-01-01
  • 2022-01-03
  • 2012-11-26
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多