一等函数:Lambda函数和映射
什么是一流的功能?
您之前可能已经听过它说某种特定的语言是有用的,因为它具有“一流的功能”。正如我在本系列关于函数式编程的第一篇文章中所说,我不同意这种流行的看法。我同意一流函数是任何函数式语言的基本特性,但我不认为这是语言功能的充分条件。有很多命令式语言也有此功能。但是,什么是一流的功能?当函数可以被视为任何其他值时,函数被描述为第一类 - 也就是说,它们可以在运行时动态分配给名称或符号。它们可以存储在数据结构中,通过函数参数传入,并作为函数返回值返回。
这实际上并不是一个新颖的想法。函数指针自1972年开始就成为C的一个特性。在此之前,过程引用是Algol 68的一个特性,在1970年实现,当时,它们被认为是一个过程编程特性。回到过去,Lisp(首次在1963年实现)建立在程序代码和数据可互换的概念之上。
这些也不是模糊的功能。在C中,我们通常使用函数作为第一类对象。例如,排序时:
char ** array = randomStrings();
printf(“排序前:\ n”);
for(int s = 0 ; s < NO_OF_STRINGS ; s ++)
printf(“%s \ n”,array [ s ]);
qsort(array,NO_OF_STRINGS,sizeof(char *),compare);
printf(“排序后:\ n”);
for(int s = 0 ; s < NO_OF_STRINGS ; s ++)
printf(“%s \ n”,array [ s ]);
stdlib
C中的库具有针对不同类型的排序例程的函数集合。所有的人都能够分拣任何种类的数据的:它们从编程器需要的唯一的协助将被提供,用于比较数据集的两个元素并返回的功能-1
,1
或者0
,指示哪个元件比其它或更大他们是平等的。
这基本上是战略模式!
我们的字符串指针数组的比较器函数可以是:
int compare(const void * a,const void * b)
{
char * str_a = *(char **)a ;
char * str_b = *(char **)b ;
return strcmp(str_a,str_b);
}
并且,我们将它传递给排序函数,如下所示:
qsort(array,NO_OF_STRINGS,sizeof(char *),compare);
compare
函数名称上没有括号使编译器发出函数指针而不是函数调用。因此,将函数视为C中的第一类对象非常容易,尽管接受函数指针的函数的签名非常难看:
qsort(void * base,size_t nel,size_t width,int(* compar)(const void *,const void *));
函数指针不仅用于排序。早在.NET发明之前,就有用于编写Microsoft Windows应用程序的Win32 API。在此之前,有Win16 API。它使得函数指针的自由使用可以用作回调。当应用程序需要通知已发生的某些事件时,应用程序在调用窗口管理器时由窗口管理器调用它时提供了指向其自身功能的指针。您可以将此视为应用程序(观察者)与其窗口(可观察对象)之间的观察者模式关系 - 应用程序接收到诸如鼠标点击和其窗口上发生的键盘按压等事件的通知。管理窗户的工作 - 移动它们,将它们堆叠在一起,决定哪个应用程序是用户操作的接收者 - 在窗口管理器中抽象。应用程序对与其共享环境的其他应用程序一无所知。在面向对象的编程中,我们通常通过抽象类和接口实现这种解耦,但也可以使用第一类函数来实现。
所以,我们一直在使用一流的功能。但是,可以公平地说,没有任何语言能够广泛宣传作为一等公民的功能而不是简单的Javascript。
Lambda表达式
在Javascript中,将函数传递给用作回调的其他函数一直是标准做法,就像在Win32 API中一样。这个想法是HTML DOM的组成部分,其中第一类函数可以作为事件侦听器添加到DOM元素:
function myEventListener(){
警报(“我被点击了!”)
}
...
var myBtn = document。getElementById(“myBtn”)
myBtn。addEventListener(“click”,myEventListener)
就像在C中一样,myEventListener
在调用中引用函数名称时缺少括号addEventListener
意味着它不会立即执行。相反,该函数与所click
讨论的DOM元素上的事件相关联。单击该元素时,将调用该函数并发出警报。
流行的jQuery库通过提供一个函数来简化流程,该函数通过查询字符串选择DOM元素,并提供有用的函数来操作元素并向它们添加事件监听器:
$(“#myBtn”)。click(function(){
警报(“我被点击了!”)
})
第一类函数也是实现异步I / O的手段,用于XMLHttpRequest
作为Ajax基础的对象。同样的想法在Node.js中也无处不在。当你想进行非阻塞函数调用时,你传递一个函数引用,让它在完成后重新打电话给你。
但是,这里还有其他的东西。其中第二个不仅仅是一流功能的例子。它也是lambda函数的一个例子。具体来说,这部分:
function(){
警报(“我被点击了!”);
}
lambda函数(通常称为lambda)是一个未命名的函数。他们本来可以称他们为匿名函数,然后每个人都会立即知道它们是什么。但是,这听起来并不令人印象深刻,所以lambda的功能就是它!lambda函数的关键是你需要在那个地方只有那里的函数; 因为在其他地方不需要它,你只需在那里定义它。它不需要名字。如果您确实需要在其他地方重用它,那么您可以考虑将其定义为命名函数并通过名称引用它,就像我在第一个Javascript示例中所做的那样。没有lambda函数,使用jQuery和Node编程确实非常烦人。
Lambda函数以不同的方式用不同的语言定义:
在Javascript中: function(a, b) { return a + b }
在Java中: (a, b) -> a + b
在C#中: (a, b) => a + b
在Clojure中: (fn [a b] (+ a b))
在Clojure中 - 速记版本: #(+ %1 %2)
在Groovy中: { a, b -> a + b }
在F#中: fun a b -> a + b
在Ruby中,所谓的“stabby”语法: -> (a, b) { return a + b }
正如我们所看到的,大多数语言都比Javascript更简洁地表达lambda。
地图
您可能已经在编程中使用术语“map”来表示将对象存储为键值对的数据结构(如果您的语言将其称为“字典”,那么很好 - 没问题)。在函数式编程中,该术语具有另外的含义。实际上,基本概念实际上是一样的。在这两种情况下,一组事物被映射到另一组事物。在数据结构的意义上,地图是名词 - 键被映射到值。在编程意义上,映射是动词 - 函数将值数组映射到另一个值数组。
假设你有一个函数f和一个值数组A = [ a1,a2,a3,a4 ]。要映射?F超过甲意味着应用?F在每个元件甲:
- a1 → f(a1)= a1‘
- a2 → f(a2)= a2‘
- a3 → f(a3)= a3‘
- a4 → f(a4)= a4‘
然后,按照与输入相同的顺序组合结果数组:
A‘ = map(f,A)= [ a1‘,a2‘,a3‘,a4‘ ]
按示例地图
好的,所以这很有趣但有点数学。你多久会这样做?实际上,它比你想象的要频繁得多。像往常一样,一个例子最好地解释了事情,所以让我们来看看我在学习Clojure时从exercism.io中提取的一个简单的练习。这项运动被称为“RNA转录”,它非常简单。我们将看一下需要转换为输出字符串的输入字符串。基地翻译如下:
- C→G
- G→C
- A→U
- T→A
除C,G,A,T以外的任何输入均无效。JUnit5中的测试可能如下所示:
class TranscriberShould {
@ParameterizedTest
@CsvSource({
“C,G”,
“G,C”,
“A,U”,
“T,A”,
“ACGTGGTCTTAA,UGCACCAGAAUU”
})
void transcribe_dna_to_rna(String dna,String rna){
var transcriber = new Transcriber();
断言(转录者。转录(dna),是(rna));
}
@测试
void reject_invalid_bases(){
var transcriber = new Transcriber();
assertThrows(
IllegalArgumentException。上课,
()- > 抄写员。转录(“XCGFGGTDTTAA”));
}
}
而且,我们可以通过这个Java实现来完成测试:
class Transcriber {
private Map < Character,Character > pairs = new HashMap <>();
Transcriber(){
对。放(‘C‘,‘G‘);
对。put(‘G‘,‘C‘);
对。放(‘A‘,‘U‘);
对。put(‘T‘,‘A‘);
}
String transcribe(String dna){
var rna = new StringBuilder();
对于(VAR 基:DNA。toCharArray()){
如果(对。的containsKey(基)){
var pair = pair。得到(基础);
rna。追加(对);
} 其他
抛出 新的 IllegalArgumentException(“不是基数:” + 基数);
}
返回 rna。toString();
}
}
不出所料,将功能风格编程的关键是将可能表达为函数的所有内容转换为一个函数。所以,让我们这样做:
char basePair(char base){
if(pairs。包含Key(base))
回归 对。得到(基础);
其他
抛出 新的 IllegalArgumentException(“不是基础” + 基础);
}
String transcribe(String dna){
var rna = new StringBuilder();
对于(VAR 基:DNA。toCharArray()){
var pair = basePair(base);
rna。追加(对);
}
返回 rna。toString();
}
现在,我们可以将地图用作动词。在Java中,Streams API中提供了一个函数:
char basePair(char base){
if(pairs。包含Key(base))
回归 对。得到(基础);
其他
抛出 新的 IllegalArgumentException(“不是基础” + 基础);
}
String transcribe(String dna){
返回 dna。codePoints()
。mapToObj(c - >(char)c)
。地图(基地 - > basePair(基地))
。收集(
StringBuilder :: new,
StringBuilder :: append,
StringBuilder :: append)
。toString();
}
Hmmmm
所以,让我们批评这个解决方案。关于它的最好的事情是循环已经消失了。如果你考虑一下,循环是一种文书活动,我们真的不应该在大多数时候关注它。通常,我们循环是因为我们想为集合中的每个元素做一些事情。我们真正想要做的是获取此输入序列并从中生成输出序列。Streaming负责为我们迭代的基本管理工作。事实上,它是一种设计模式 - 一种功能性的设计模式 - 但是,我还没有提到它的名字。我还不想吓唬你。
我不得不承认代码的其余部分并不是那么好,主要是因为Java中的原语不是对象。第一点非伟大是这样的:
mapToObj(c - >(char)c)
我们必须这样做,因为Java以不同的方式处理原语和对象,虽然该语言确实具有基元的包装类,但是无法直接从String获取Character对象的集合。
另一点不那么令人敬畏的是:
。收集(
StringBuilder :: new,
StringBuilder :: append,
StringBuilder :: append)
很明显为什么有必要再打append
两次电话。我稍后会解释,但现在时间不对。
我不会试图捍卫这个代码 - 它很糟糕。如果有一种方便的方法从String,甚至是一个字符数组中获取Stream of Character对象,那么就没有问题了,但我们并没有幸运。处理原语并不是Java中FP的最佳选择。想想看,它对OO编程来说甚至都不好。所以,也许我们不应该如此着迷原始人。如果我们从代码中设计出来怎么办?我们可以为基数创建一个枚举:
enum Base {
C,G,A,T,U ;
}
而且,我们有一个类作为一个包含一系列基础的一流集合:
class Sequence {
列出< 基地> 基地 ;
序列(List < Base > bases){
这个。碱 = 碱 ;
}
Stream < Base > bases(){
返回 基地。stream();
}
}
现在, Transcriber
看起来像这样:
class Transcriber {
private Map < Base,Base > pairs = new HashMap <>();
Transcriber(){
对。放(C,G);
对。放(G,C);
对。放(A,U);
对。put(T,A);
}
序列 转录(序列 dna){
返回 新的 序列(DNA。基地()
。map(pairs :: get)
。collect(toList()));
}
}
这要好得多。这pairs::get
是一个方法参考; 它指的是get
分配给pairs
变量的实例的方法。通过为基础创建类型,我们设计了无效输入的可能性,因此对该basePair
方法的需求消失,异常也是如此。这是Java对Clojure的一个优势,它本身不能在函数契约中强制执行类型。更重要的是,它StringBuilder
也消失了。当您需要迭代集合,以某种方式处理每个元素并构建包含结果的新集合时,Java Streams非常适合。这可能占你生活中所写循环的很大一部分。大部分的家务管理都不是真正的工作的一部分,而是为您完成的。
在Clojure
缺少打字,Clojure比Java版本更简洁,它给我们映射字符串字符没有任何困难。Clojure中最重要的抽象是序列; 所有集合类型都可以视为序列,字符串也不例外:
(def 对 { \ C , “ G” ,
\ G , “ C” ,
\ A , “ U” ,
\ T , “ A” } )
(defn -base-pair [ base ]
(if-let [ pair (get pairs base )]
对
(throw (IllegalArgumentException。 (str “ not base:” base )))))
(定义 转录 [ dna ]
(地图 基础对 dna ))
这段代码的业务结束是最后一行(map base-pair dna)
- 值得指出,因为你可能错过了它。它表示字符串上map
的base-pair
函数dna
(表现为序列)。如果我们希望它返回一个字符串而不是一个列表,这就是map
我们所要求的,唯一需要做的改变是:
(应用 str (map base-pair dna ))
在C#中
我们来试试另一种语言。C#中解决方案的必要方法如下所示:
命名空间 RnaTranscription
{
公共 类 转录员
{
private readonly Dictionary < char,char > _pairs = new Dictionary < char,char >
{
{ ‘C‘,‘G‘ },
{ ‘G‘,‘C‘ },
{ ‘A‘,‘U‘ },
{ ‘T‘,‘A‘ }
};
public string Transcribe(string dna)
{
var rna = new StringBuilder();
的foreach(炭 b 中 的DNA)
rna。追加(_pairs [ b ]);
返回 rna。ToString();
}
}
}
同样,C#没有向我们展示我们在Java中遇到的问题,因为C#中的字符串是可枚举的,并且所有“基元”都可以被视为具有行为的对象。
我们可以用更加实用的方式重写程序,就像这样,并且它比Java Streams版本要简单得多。对于Java流中的“map”,请在C#中读取“select”:
public string Transcribe(string dna)
{
return String。加入(“”,dna。选择(b => _pairs [ b ]));
}
或者,如果您愿意,可以使用LINQ作为其语法糖:
public string Transcribe(string dna)
{
return String。加入(“” ,从 b 中 的DNA 选择 _pairs [ b ]);
}
为什么我们循环?
你可能会得到这个想法。如果您想到编写循环之前的时间,通常您会尝试完成以下任一操作:
- 将一种类型的数组映射到另一种类型的数组。
- 通过查找满足某个谓词的数组中的所有项来进行过滤。
- 确定数组中的任何项目是否满足某些谓词。
- 累积数组中的计数,总和或其他类型的累积结果。
- 将数组的元素排序为特定顺序。
大多数现代语言中提供的函数式编程功能使您无需编写循环或创建集合来存储结果即可完成所有这些操作。功能样式允许您省去这些内务操作并专注于实际工作。更重要的是,功能样式允许您将操作链接在一起,例如,如果您需要:
- 将数组的元素映射到另一种类型。
- 过滤掉一些映射的元素。
- 对过滤的元素进行排序
在命令式样式中,这需要多个循环或一个循环,其中包含很多代码。无论哪种方式,它涉及许多模糊程序真正目的的管理工作。在功能风格中,您可以免除管理工作并直接表达您的意思。稍后,我们将看到更多功能样式如何让您的生活更轻松的例子。
原文地址:https://www.cnblogs.com/cddehsy/p/9645910.html