関数型プログラミング言語:LISP / Schemeマイナー言語の紹介

1。概要

Qiu Zongyan教授が第2版コンピュータプログラムの構造と解釈」(コンピュータプログラムの構造と解釈、SICP以降)を翻訳して以来、この入門プログラミング教科書MITコンピュータサイエンスは中国の開発者にますます注目を集め始めました。同時に、それが導入する関数型プログラミング(関数型プログラミング)、および例で使用されているScheme言語についても懸念しています。

30年前の1975年に、ビル・ゲイツとポール・アレンが伝説的なBASICバージョンを作成しました。これらは、後に「最初の金の壺」BASICバージョンと引き換えにMITSに販売されました。同じ年に、ジェラルド・サスマン(彼はSICPの作者でした)がScheme言語を発明しました。実際、Schemeは新しい言語ではありません。正確には、SchemeはLISPの変形であり方言です。1958年には、ジョン・マッカーシーは「リストデータの処理に使用される」言語の研究を開始しました。これは、LISP(LISt Processing)という名前の由来であります。リスト処理」は一見かなり特殊な問題ですが、実際、この種の問題には広範囲にわたる重要な意味合いがあり、後でここで説明します。

 

2.Scheme言語の機能と例の紹介

LISPの他の方言と比較すると、Schemeの最大の特徴は、マシンコードにコンパイルできることかもしれません。言い換えれば、それはより効率的に実行されます。また、Schemeは言語的にはかなり満足のいくものと言えます。LISPが常に賞賛している哲学は「マイクロコア+高スケーラビリティ」であり、Schemeもこの機能を最大限に活用しています。Schemeの組み込みキーワード(キーワード)は残念ながら少なく、大なり記号、足し算、引き算、掛け算、割り算などの演算も関数の形で現れます。定義キーワードと括弧があれば、すべてのプログラムを書くことができると言っても過言ではありません。ただし、このスタイルの副作用は、プログラムに多くの括弧が含まれることです。そのため、一部の人々は冗談めかしてLISPを「多くの刺激的で偽の括弧」(多くの刺激的で偽の括弧)と呼びます。たとえば、次のプログラムを使用して値を2乗します。

(定義(正方形x)

      (* xx))

(表示(四角3))

C言語から始めて(「関数型プログラミング」ではなく)手続き型プログラミングに慣れている私たちにとって、LISP / Schemeに最初に触れたとき、最初のタッチはおそらく次のとおりでした。Schemeはデータを区別しません。操作「二乗」を例にとると、「xの二乗」は「1を底とし、xを2回掛ける」と表現できます。C ++言語を使用する場合、このロジックは次のように実装できます。

int square(int x){

      1 * x * xを返します。

}

スキームでは、これを達成することもできます。

(define(twice func base arg)

      (func base(func base arg)))

(定義(正方形x)

      (2回* 1 x))

この実現の特徴は何ですか?最大の特徴は、演算(乗算演算)がパラメータとして渡されることです。プログラム設計の「ブラックワード」によれば、プログラムユニットをパラメータと戻り値として渡すことができる場合、このユニットは「ファーストクラス(ファーストクラス)と呼ばれます。C / C ++ / Javaなどの言語では、関数ポインターやファンクターなどの形式で「演算」を渡すこともできますが、結局のところパッケージ化されています。Schemeでは、別の関数を次のように直接渡すことができます関数へのパラメータであり、戻り値として別の関数に戻すことができ、関数(つまり「操作」)は完全にファーストクラスの市民として扱われます。

これの利点は何ですか?上記の例では、「操作を2回実行する」というロジックを抽象化し、double関数を取得しました。「0をベースとして加算演算を2回実行する」(つまり、「2を掛ける」)を実現したい場合は、次のように記述するだけで済みます。

(定義(double x)

      (2回+ 0 x))

ここでのdouble関数は「他の関数を操作する関数」であり、その結果は、パラメーターとして渡される関数によって異なります。このような「関数の関数」は、関数型プログラミング用語では「高階関数」と呼ばれます。高階関数を自然に実装する機能は、Schemeの2番目の重要な機能です。前述のように、LISPという名前は「リスト処理」の略であり、実際、リストデータを処理する機能は高階関数の使用に由来します。たとえば、次のようなリストがあります。

{1、2、3、4、5}

このリストでは、次の2つのことを行う必要があります。

1.各要素を2倍にして、新しいリストを取得します:{2、4、6、8、10}

2.各要素を2乗して、新しいリストを取得します:{1、4、9、16、25}

Java言語を使用すると、これを実現できます。

List <int> doubleList(List <int> src){

      List <int> dist = new ArrayList <int>();

      for(int item:src){

            dist.add(item * 2);

}

distを返します。

}

List <int> squareList(List <int> src){

      List <int> dist = new ArrayList <int>();

      for(int item:src){

            dist.add(item * item);

}

distを返します。

}

問題は一目でわかります。2つの太字のコード行を除いて、これら2つのメソッドはほぼ完全に重複しています。あなたがそれについて考えるとき、これらの2つの方法は実際には非常に似たようなことをします:リストをトラバースし、「特定のルール」に従って元のリストの各要素を新しいリストにマップします。この「特定のルール」は要素のマッピング操作であるため、このリスト操作を抽象化するには、実際のマッピング操作をパラメーターの形式で渡す高階関数を実装する必要があります。したがって、高階関数を簡単に実装できるSchemeでは、上記のロジックは非常に簡単に実装できます。

(define(double-list src)

      (map double src))

(define(square-list src)

      (マップスクエアsrc))

演算論理の抽象化という点で高階関数の力と利便性のために、多くの人々が「主流」の手続き型言語で演算を第一級市民として扱い、高階関数を実現しようとし始めています。たとえば、デリゲートの形式では、C#を使用すると、メソッドをパラメーターまたは戻り値として渡すことができ、findなどの高レベルの操作がList <T>に追加されます。Javaの世界ではFunctionalJ(http://functionalj.sourceforge .net /)Java5によって提供されるジェネリックに基づいて、フィルターやマップなどの一般的に使用されるリスト操作が提供されます。前の例がFunctionalJで実装されている場合、次のように記述できます。

// doubleとsquareは関数インスタンスです

List <int> doubleList(List <int> src){

      Functions.map(double、src);を返します。

}

List <int> squareList(List <int> src){

Functions.map(square、src);を返します。

}

 

3.長所と短所

「データと操作を区別しない」という言葉は簡単に言えますが、実際にはその背後にある重要な哲学的問題、つまり「時間とは何か」という問題があります。手続き型プログラミングの概念によれば、「時間」は内部変数の操作であり、プログラムはさまざまな時点でのシステムの瞬間的な状態をローカル変数の形式で記録します。関数型プログラミングの概念によれば、 「時間」は外部変数の操作です。関数はパラメーターとして渡され、関数内にローカル状態はなく、割り当て操作もありません。または、もっと簡単に言えば、いつでも同じパラメーターを使用して同じ関数を呼び出すと、間違いなく同じ結果が得られます。このプロパティは「参照透過性」と呼ばれます。操作に参照透過性がない場合、呼び出し環境とシーケンスによって高階関数の結果が変わる可能性があるため、パラメーターまたは戻り値として渡すことはできません。

参照透過性のあるプログラムには、追加の利点があります本質的にスレッドセーフです。スレッドの数とアクセス順序に関係なく、プログラムが参照透過性である限り、正しい結果を保証するために追加のスレッド同期メカニズムは必要ありません。これは、同時に多くのユーザーが直面するサーバー側アプリケーション、特にWebアプリケーションでは特に重要です。ロッドジョンソンは、彼の著書「EJBを使用しないJ2EE開発」で「ステートレスJavaサーバーサイドアプリケーション」を提唱しており、エンタープライズアプリケーション開発者も関数型プログラミングのアイデアから多くの恩恵を受けています。

LISPの最初の発明はまったく意図的ではなかったと言われています。マッカーシーはラムダ演算に基づいた抽象的な文法を実装してそれを捨てただけでしたが、彼の学生はそのような最小限の文法でプログラムを書くのがとても楽しいことを発見しました。コンピュータ科学者は還元が好きな人々のグループであると言う人もいますが、LISPの発明は実用的な観点から証明されています。基本的にすべてのプログラム構造をラムダ演算に還元することができます。アロンゾチャーチによって発明された教会微積分理論によれば、固定値関数を含む効率的に計算できるすべての関数は、ラムダ演算によって定義できます。たとえば、データ「0」と演算「プラス1」は、ラムダ演算によって次のように定義できます。

(ゼロを定義する(ラムダ(f)(ラムダ(x)x)))

(define(add-1 n)
  (lambda(f)(lambda(x)(f((nf)x)))))

これに基づいて、ラムダ演算を使用して、自然数システム全体を定義できます。これは極端な例です。他の多くの場所でも、LISP / Schemeは私たちが慣れ親しんでいる概念を同様の方法で分析することができ、より深い洞察を得ることができます。たとえば、SICP「StructuralData Abstractionの第2章では、私たち自身の目で見ました。通常の「プロシージャ指向プログラミング」と「オブジェクト指向プログラミング」は、大部分が異なる構文糖衣にすぎません。同じラムダ演算のセットを使用します。それだけです。オブジェクト指向学習スキームに精通しているプログラマーは、「データ」と「操作」の間のギャップを埋めるため、新しい理解を得ることがよくあります。さらに、Schemeの構文は非常に単純で、最も一般的に使用されるキーワードはおそらく5つ以下であるため、教育言語として独自の利点があります。多くの学校では、大学生のプログラミング言語としてJavaを使用しています。これらの貧しい学生たちで2か月後、「匿名の内部クラス」などの奇妙な構文や「IOストリームデコレータ」などの複雑なライブラリにまだ巻き込まれているとき、私が何を意味するのかを理解するのは難しくありません。

しかし、この単純さは、エンタープライズアプリケーションでSchemeを推進する上での最大の障害にもなっています。エンタープライズアプリケーションは、多くの洗練された可能性を必要としませんが、実行可能なソリューションを必要とします。PLTなどのScheme実装バージョンは、XMLやサーブレットなどのツールライブラリを提供しますが、構文が非常に柔軟で、ベストプラクティスがなく、大規模ベンダーからのサポートがないため、Schemeは最終的にエンタープライズアプリケーションの主流になることができなくなりました。ただし、実際のアプリケーションはほとんどありませんが、関数型プログラミングからのアイデアは、エンタープライズアプリケーション開発者を刺激します。たとえば、WebWork2.2によって導入された継続機能は、関数型プログラミングの概念から派生しています。

最後に、重要なことですが、次のように述べておく必要があります。エンタープライズアプリケーションではめったに見られませんが、LISP / Schemeは科学計算、人工知能、数学的モデリングなどの分野で広く使用されているため、 「マイナー言語」。全体として、LISP / Schemeはアルゴリズムロジックを書くよりはましですが、I / O操作は得意ではありません。もちろん、これがエンタープライズアプリケーションの分野で不満を感じている理由と言えますが、なぜそれが結果ではないのでしょうか。

おすすめ

転載: blog.csdn.net/smilejiasmile/article/details/107644992