功能样式:Lambda函数和映射

一等函数: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 ]);

stdlibC中的库具有针对不同类型的排序例程的函数集合。所有的人都能够分拣任何种类的数据的:它们从编程器需要的唯一的协助将被提供,用于比较数据集的两个元素并返回的功能-11或者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 = [ a1a2a3a4 ]。要映射?F超过意味着应用?F在每个元件

  • a1 → fa1)= a1‘
  • a2 → fa2)= a2‘
  • a3 → fa3)= a3‘
  • a4 → fa4)= a4‘

然后,按照与输入相同的顺序组合结果数组:

A‘ = map(fA)= [ 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)- 值得指出,因为你可能错过了它。它表示字符串上mapbase-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 ]);
}

为什么我们循环?

你可能会得到这个想法。如果您想到编写循环之前的时间,通常您会尝试完成以下任一操作:

  • 将一种类型的数组映射到另一种类型的数组。
  • 通过查找满足某个谓词的数组中的所有项来进行过滤。
  • 确定数组中的任何项目是否满足某些谓词。
  • 累积数组中的计数,总和或其他类型的累积结果。
  • 将数组的元素排序为特定顺序。

大多数现代语言中提供的函数式编程功能使您无需编写循环或创建集合来存储结果即可完成所有这些操作。功能样式允许您省去这些内务操作并专注于实际工作。更重要的是,功能样式允许您将操作链接在一起,例如,如果您需要:

  1. 将数组的元素映射到另一种类型。
  2. 过滤掉一些映射的元素。
  3. 对过滤的元素进行排序

在命令式样式中,这需要多个循环或一个循环,其中包含很多代码。无论哪种方式,它涉及许多模糊程序真正目的的管理工作。在功能风格中,您可以免除管理工作并直接表达您的意思。稍后,我们将看到更多功能样式如何让您的生活更轻松的例子。

原文地址:https://www.cnblogs.com/cddehsy/p/9645910.html

时间: 2024-10-10 05:41:10

功能样式:Lambda函数和映射的相关文章

C++11新特性:Lambda函数(匿名函数)

声明:本文参考了Alex Allain的文章http://www.cprogramming.com/c++11/c++11-lambda-closures.html 加入了自己的理解,不是简单的翻译 C++11终于知道要在语言中加入匿名函数了.匿名函数在很多时候可以为编码提供便利,这在下文会提到.很多语言中的匿名函数,如C++,都是用Lambda表达式实现的.Lambda表达式又称为lambda函数.我在下文中称之为Lambda函数. 为了明白Lambda函数的用处,请务必先搞明白C++中的自动

Python的lambda函数与排序

Python的lambda函数与排序 2010-03-02 15:02 2809人阅读 评论(0) 收藏 举报 lambdapythonlistlispclass工作 目录(?)[+] 前几天看到了一行求1000的阶乘的Python代码: print    reduce ( lambda    x , y : x * y ,    range ( 1 ,    1001 )) 一下子被python代码的精简 与紧凑所折服,故对代码进行了简单的分析. reduce与range都是Python的内置

lambda函数

C++11一个最激动人心的特性是支持创建lambda函数(有时称为闭包).这意味着什么?一个Lambda函数是一个可以内联写在你代码中的函数(通常也会传递给另外的函数,类似于仿函数或函数指针).使用Lambda,创建机动函数会更简单,而以前你必须创建一个有名函数.在这篇文章中,我先用一些例子解释为什么lambda很酷,然后我会讲解可能会用到的关于lambda的所有细节. 为什么Lambda很酷 想象你有一个地址簿类,并且你想要提供一个可供检索的函数.你可能会提供一个简单的函数,接受一个字符串然后

lambda 函数所引起的闭包问题

之前在某本书上看到一道题,要求是:用字符串sign中的每一个字符去分割s字符串,并得到最后的结果 s = 'ab;cd|efg|hi,jkl|mn\opq;rst,uvw/xyz' sign = ';|\/,' 书中给的答案是这样的: def my_split(s, sign): s = [s] for i in sign: t = [] for x in s: map(lambda x: t.extend(x.split(i)), s) s = t return s print(my_spli

lambda函数的特性

lambda表达式可以理解为一种抽象的函数实现方法,这种方式只有最基本的三个步骤:给与参数,表达式实现,返回结果.这种方式非常干净,减少了内存的使用,整个程序少了函数的污染,代码格式也会更为简练.但在python中的使用是受限的,只能进行简单的表达式计算. 下面简单给一个知乎扒的代码示范一下: func = (lambda x:x**i for i in xrange(4)) for i in func: i(4) 1 4 16 64 上面代码的含义很简单,for i in xrange(4)会

【C++】【lambda】lambda函数介绍和个人理解(3)——lambda的语法甜点

导航: lambda函数介绍和个人理解(1)--初识lambda lambda函数介绍和个人理解(2)--lambda与仿函数 lambda函数介绍和个人理解(3)--lambda的语法甜点 其实,与其说这是一篇介绍lambda语法甜点的文章,不如说是一篇教大家使用lambda函数的一篇文章.当然不可避免的会用到一些有趣的实验.文章略长,大家耐心耐心看吧!当然,这也是本人写的关于lambda函数的最后一篇博文了,如果大家有其他更好的想法或者更深入的理解,请联系我~ 本文大概讲这些内容:基础使用,

【C++】【lambda】lambda函数介绍和个人理解(1)——初识lambda

导航: lambda函数介绍和个人理解(1)--初识lambda lambda函数介绍和个人理解(2)--lambda与仿函数 lambda函数介绍和个人理解(3)--lambda的语法甜点 什么是lambda函数? 其实,lambda函数我个人更愿意称为lambda运算(lambdacalculus),它是用来表示一种匿名函数.这个严格意义上属于"函数式编程"(Functional Programming)的范畴.当然还是先解释下函数式编程的概念的好.按照当时冯·诺依曼机器的基本计算

【C++11】新特性——Lambda函数

本篇文章由:http://www.sollyu.com/c11-new-lambda-function/ 文章列表 本文章为系列文章 [C++11]新特性--auto的使用 http://www.sollyu.com/c11-new-features-auto/ [C++11]新特性--Lambda函数 http://www.sollyu.com/c11-new-lambda-function/ 说明 在标准 C++,特别是当使用 C++ 标准程序库算法函数诸如 sort 和 find,用户经常

【C++】lambda函数介绍和个人理解

一般数据类型的别名 众所周知,在C++中,有一种不同于地址引用的值引用类型,也就是这种定义. int a = 10; cout << a << endl;//:10 int& d = a;//d为a的别名 cout << d << endl;//:10 a++; cout << a << endl;//:11 d++; cout << a << endl;//:12 cout << d &l