web-dev-qa-db-ja.com

LISPのような構文拡張メカニズムを備えたプログラミング言語

私はLISPについて限られた知識しか持っていませんが(私の自由な時間に少し学ぼうとしています)、LISPマクロを理解する限り、LISPマクロをLISP自体に記述することで新しい言語構成と構文を導入できます。つまり、LISPコンパイラ/インタープリタを変更せずに、新しい構成をライブラリとして追加できます。

このアプローチは、他のプログラミング言語のアプローチとは大きく異なります。たとえば、Pascalを新しい種類のループまたは特定のイディオムで拡張したい場合は、言語の構文とセマンティクスを拡張してから、コンパイラにその新しい機能を実装する必要があります。

LISPファミリーの外部に他のプログラミング言語がありますか(つまり、Common LISP、Scheme、Clojure(?)、Racket(?)などを除いて)、言語自体の中で言語を拡張する同様の可能性を提供していますか?

[〜#〜]編集[〜#〜]

詳しい議論は避け、具体的に回答してください。何らかの方法で拡張できるプログラミング言語の長いリストではなく、概念的な観点から、拡張メカニズムとしてのLISPマクロに固有のものと、LISP以外のプログラミング言語がいくつかの概念を提供するものを理解したいと思いますそれは彼らに近いです。

20
Giorgio

Scalaはこれも可能にします(実際、新しい言語構成の定義と完全なDSLの定義をサポートするように意識的に設計されました)。

関数型言語で一般的な高階関数、ラムダ、カリーの他に、これを可能にする特別な言語機能がいくつかあります*:

  • 演算子なし-すべてが関数ですが、関数名には「+」、「-」、「:」などの特殊文字を含めることができます
  • 単一パラメーターのメソッド呼び出しでは、ドットと中括弧を省略できます。つまり、a.and(b)は、インフィックス形式の_a and b_と同等です。
  • 単一パラメーターの関数呼び出しでは、通常の中括弧の代わりに中括弧を使用できます-これは(カリーと一緒に)次のように書くことができます

    _val file = new File("example.txt")
    
    withPrintWriter(file) {
      writer => writer.println("this line is a function call parameter")
    }
    _

    ここで、withPrintWriterは2つのパラメーターリストを持つ単純なメソッドで、どちらも1つのパラメーターを含みます

  • 名前によるパラメータを使用すると、ラムダで空のパラメータリストを省略できるため、myAssert(() => x > 3)のような呼び出しをmyAssert(x > 3)のように短い形式で記述できます。

サンプルDSLの作成については、無料の本の Chapter 11. Domain-Specific Languages in Scala で詳しく説明しています Programming Scala

*これらがScalaに固有のものであることを意味するわけではありませんが、少なくともそれほど一般的ではないようです。私は関数型言語の専門家ではありません。

19
Péter Török

ハスケル

Haskellには「テンプレートHaskell」と「Quasiquotation」があります。

http://www.haskell.org/haskellwiki/Template_Haskell

http://www.haskell.org/haskellwiki/Quasiquotation

これらの機能により、ユーザーは通常の手段以外で言語の構文を劇的に追加できます。これらはコンパイル時にも解決され、これは(少なくともコンパイルされた言語では)大きな必須事項だと思います[1]。

Haskellで準引用符を使用して、Cのような言語で高度なパターンマッチャーを作成しました。

moveSegment :: [Token] -> Maybe (SegPath, SegPath, [Token])
moveSegment [hc| HC_Move_Segment(@s, @s); | s1 s2 ts |] = Just (mkPath s1, mkPath s2, ts)
moveSegment _ = Nothing

[1]それ以外の場合、次の構文の拡張として使用できます:runFeature "some complicated grammar enclosed in a string to be evaluated at runtime"、もちろんがらくたの負荷です。

13
Thomas Eding

Perlはその言語の前処理を可能にします。これは言語の構文を根本的に変更するほど頻繁には使用されませんが、いくつかの奇妙なモジュールで見ることができます:

  • Acme :: Bleach 本当にクリーンなコード
  • Acme :: Morse モールス信号で記述する場合
  • Lingua :: Romana :: Perligata ラテン語で書く場合(たとえば、名詞は変数であり、数、大文字、小文字の区別は名詞のタイプを変更しますnextum ==> $ next、nexta ==> @次)

また、Pythonで作成されたように見えるコードをPerlが実行できるようにするモジュールもあります。

Perl内でこれを行うためのより現代的なアプローチは、 Filter :: Simple (Perl5のコアモジュールの1つ)を使用することです。

これらすべての例には、「マッドドクターオブペルル」と呼ばれているダミアンコンウェイが関係していることに注意してください。それでも、Perlには驚くほど強力な機能があり、言語がどのように望まれるかを変えることができます。

これと他の代替の詳細なドキュメントは perlfilter にあります。

13
user40980

Tcl には、拡張可能な構文をサポートする長い歴史があります。たとえば、次は、カーディナル、その正方形、およびキューブに対して3つの変数を(停止するまで)繰り返すループの実装です。

proc loopCard23 {cardinalVar squareVar cubeVar body} {
    upvar 1 $cardinalVar cardinal $squareVar square $cubeVar cube

    # We borrow a 'for' loop for the implementation...
    for {set cardinal 0} true {incr cardinal} {
        set square [expr {$cardinal ** 2}]
        set cube [expr {$cardinal ** 3}]

        uplevel 1 $body
    }
}

その後、次のように使用されます。

loopCard23 a b c {
    puts "got triplet: $a, $b, $c"
    if {$c > 400} {
        break
    }
}

この種の手法はTclプログラミングで広く使用されており、正しく実行するための鍵はupvarおよびuplevelコマンドです(upvarは別のスコープ内の名前付き変数をローカル変数、およびuplevelは別のスコープでスクリプトを実行します。どちらの場合も、1は、問題のスコープが呼び出し元のスコープであることを示します。 Tk for GUI(イベントへのコールバックのバインディングを行うため)など、データベース(結果セットの各行に対していくつかのコードを実行する)と結合するコードでもよく使用されます。

しかし、それはほんの一部です。埋め込み言語はTclである必要さえありません。それは事実上何でもかまいません(中括弧のバランスが取れている限り、そうでなければ構文的に恐ろしくなります—これはプログラムの大多数です)、Tclは必要に応じて組み込みの外国語にディスパッチできます。これを行う例には、 Tclコマンドを実装するためのCの埋め込み およびFortranと同等のものがあります。 (間違いなく、すべてのTclの組み込みコマンドは、ある意味でこの方法で実行されます。つまり、これらのコマンドは、実際には単なる標準ライブラリであり、言語自体ではありません。)

12
Donal Fellows

これは部分的には意味論の問題です。 LISPの基本的な考え方は、プログラムはそれ自体が操作可能なデータであるということです。 Schemeなど、LISPファミリで一般的に使用される言語では、パーサーの意味で新しいsyntaxを実際に追加することはできません。それはすべて、スペースで区切られた括弧で囲まれたリストです。コア構文はほとんど機能しないため、ほとんどすべてのsemantic構成要素から作成できます。 Scala(以下で説明)は同様です:変数名のルールは非常に自由で、(同じコア構文ルール内にとどまりながら)Nice DSLを簡単に作成できます。

これらの言語は、実際にはPerlフィルターの意味で新しい構文を定義することはできませんが、DSLの構築や言語構成の追加に使用できる十分に柔軟なコアを備えています。

重要な共通機能は、言語によって公開されている機能を使用して、機能する言語構成と組み込みの構成を定義できることです。この機能のサポートの程度は異なります。

  • 多くの古い言語は、独自の関数を実装する方法なしで、sin()round()などの組み込み関数を提供していました。
  • C++は限定的なサポートを提供します。たとえば、キャスト(static_cast<target_type>(input)dynamic_cast<>()const_cast<>()reinterpret_cast<>())などの組み込みキーワードは、テンプレート関数を使用してエミュレートできます。 Boostはlexical_cast<>()polymorphic_cast<>()any_cast<>()、...を使用します。
  • Javaには組み込みの制御構造(for(;;){}while(){}if(){}else{}do{}while()synchronized(){}strictfp{})を使用して独自に定義することはできません。 Scalaは代わりに、便利な制御構造のような構文を使用して関数を呼び出すことができる抽象構文を定義し、ライブラリはこれを使用して新しい制御構造を効率的に定義します(例:react{}アクターライブラリ内)。

また、Mathematicaのカスタム構文機能は Notation package で見ることができます。 (技術的にはLISPファミリに含まれていますが、通常のLISPの拡張性と同様に、いくつかの拡張機能が異なって実行されています。)

10

Rebol は、あなたが説明しているように聞こえますが、少し横向きです。

特定の構文を定義するのではなく、Rebolのすべてが関数呼び出しです-キーワードはありません。 (はい、本当に望むならifwhileを再定義できます)。たとえば、これはifステートメントです。

if now/time < 12:00 [print "Morning"]

ifは、条件とブロックの2つの引数を取る関数です。条件が真の場合、ブロックが評価されます。ほとんどの言語のように聞こえますよね?さて、ブロックはデータ構造であり、コードに限定されていません-これはたとえばブロックのブロックであり、「コードはデータ」の柔軟性の簡単な例です:

SomeArray: [ [foo "One"] [bar "Two"] [baz "Three"] ]
foreach action SomeArray [action/1: 'print] ; Change the data
if now/time < 12:00 SomeArray/2 ; Use the data as code - right now, if now/time < 12:00 [print "Two"]

構文規則に固執する限り、この言語を拡張することは、ほとんどの場合、新しい関数を定義することに他なりません。たとえば、一部のユーザーはRebol 3の機能をRebol 2にバックポートしています。

8
Izkata

Rubyはかなり柔軟な構文を持っています。これは「言語自体の中で言語を拡張する」方法だと思います。

例は rake です。 Rubyで書かれているRubyですが、 make のように見えます。

いくつかの可能性を確認するには、キーワードRubyおよびmetaprogrammingを探します。

7
knut

話している方法で構文を拡張すると、 ドメイン固有の言語を作成できます。 質問を言い換えるのにおそらく最も役立つ方法は、言語はドメイン固有の言語を適切にサポートしていますか?

Rubyには非常に柔軟な構文があり、rakeなど、多くのDSLがそこで栄えています。 Groovyにはその優れた点がたくさん含まれています。また、AST変換も含まれます。これは、LISPマクロにより直接的に類似しています。

統計計算用の言語であるRを使用すると、関数は引数を評価せずに取得できます。これを使用して、回帰式を指定するためのDSLを作成します。例えば:

y ~ a + b

「k0 + k1 * a + k2 * bの形式の線をyの値に合わせる」ことを意味します。

y ~ a * b

「k0 + k1 * a + k2 * b + k3 * a * bという形式の線をyの値に合わせる」という意味です。

等々。

7

収束 は、別のLispyでないメタプログラミング言語です。そして、ある程度、C++も適格です。

間違いなく MetaOCaml はLISPとはかなりかけ離れています。まったく異なるスタイルの構文拡張性がありながら非常に強力な場合は、 CamlP4 をご覧ください。

Nemerle は、LISPスタイルのメタプログラミングを備えた別の拡張可能な言語ですが、Scalaなどの言語に近いです。

そして、Scala自体 間もなく そのような言語も。

編集:最も興味深い例- JetBrains MPS を忘れました。 Lispishから非常に離れているだけでなく、テキスト以外のプログラミングシステムでもあり、エディタはASTレベルで直接動作します。

Edit2:更新された質問に答えるために-LISPマクロにはユニークで例外的なものはありません。理論的には、どの言語でもこのようなメカニズムを提供できます(私はプレーンなCでもそれを行いました)。必要なのは、ASTへのアクセスと、コンパイル時にコードを実行する機能です。いくつかのリフレクションが役立つ場合があります(型、既存の定義などのクエリ)。

7
SK-logic

Prologでは、同じ名前の複合語に変換される新しい演算子を定義できます。たとえば、これはhas_cat演算子を使用して、リストにatom cat:が含まれているかどうかを確認するための述語として定義します。

:- op(500, xf, has_cat).
X has_cat :- member(cat, X).

?- [Apple, cat, orange] has_cat.
true ;
false.

xfは、has_catは後置演算子です。 fxを使用すると前置演算子になり、xfxを使用すると中置演算子になり、2つの引数を取ります。 Prologでの演算子の定義の詳細については、 このリンク を確認してください。

6
Ambroz Bizjak

Boo を使用すると、構文マクロを使用して、コンパイル時に言語を大幅にカスタマイズできます。

Booには「拡張可能なコンパイラパイプライン」があります。つまり、コンパイラはコードを呼び出して、AST変換をコンパイラパイプラインの任意の時点で実行できます。ご存じのように、JavaのGenericsやC#のLinqなどは、コンパイル時の構文変換にすぎないため、これはかなり強力です。

LISPと比較した場合の主な利点は、これがあらゆる種類の構文で機能することです。 BooはPython風の構文を使用していますが、CまたはPascal構文を使用して拡張可能なコンパイラを作成することもできます。また、マクロはコンパイル時に評価されるため、パフォーマンスが低下することはありません。

LISPと比較した場合の欠点は次のとおりです。

  • ASTでの作業は、s式での作業ほどエレガントではありません
  • マクロはコンパイル時に呼び出されるため、ランタイムデータにアクセスできません。

たとえば、これは新しい制御構造を実装する方法です。

macro repeatLines(repeatCount as int, lines as string*):
    for line in lines:
        yield [| print $line * $repeatCount |]

使用法:

repeatLines 2, "foo", "bar"

その後、コンパイル時に次のようなものに変換されます。

print "foo" * 2
print "bar" * 2

(残念ながら、Booのオンラインドキュメントは常に絶望的に時代遅れであり、このような高度なものさえカバーしていません。私が知っている言語の最良のドキュメントはこの本です: http://www.manning.com/rahien/

5
Dan

TeXはリストから完全に欠落しています。みんな知ってるでしょ?次のようになります。

Some {\it ``interesting''} example.

…制限なしで構文を再定義できることを除いて。言語のすべての(!)トークンには、新しい意味を割り当てることができます。 ConTeXtは、中括弧を角括弧に置き換えたマクロパッケージです。

Some \it[``interesting''] example.

より一般的なマクロパッケージLaTeXも、その目的のために言語を再定義します。 \begin{environment}…\end{environment}構文を追加します。

しかし、それだけではありません。技術的には、トークンを再定義して以下を解析することもできます。

Some <it>“interesting”</it> example.

はい、可能です。一部のパッケージは、これを使用して小さなドメイン固有の言語を定義します。たとえば、TikZパッケージは、テクニカルドローイングの簡潔な構文を定義します。これにより、次のことが可能になります。

\foreach \angle in {0, 30, ..., 330} 
  \draw[line width=1pt] (\angle:0.82cm) -- (\angle:1cm);

さらに、TeXは完全にチューリングされているので、文字通りすべてを実行できます。これはかなり無意味で非常に複雑であるため、これが最大限に活用されるのを見たことはありませんが、トークンを再定義するだけで次のコードを解析可能にすることは完全に可能です(ただし、これはおそらく、構築方法):

for Word in [Some interesting example.]:
    if Word == interesting:
        it(Word)
    else:
        Word
5
Konrad Rudolph

評価 Mathematica はパターンマッチングと置換に基づいています。これにより、独自の制御構造を作成したり、既存の制御構造を変更したり、式の評価方法を変更したりできます。たとえば、次のような「ファジーロジック」を実装できます(少し簡略化)。

fuzzy[a_ && b_]      := Min[fuzzy[a], fuzzy[b]]
fuzzy[a_ || b_]      := Max[fuzzy[a], fuzzy[b]]
fuzzy[!a_]           := 1-fuzzy[a]
If[fuzzy[a_], b_,c_] := fuzzy[a] * fuzzy[b] + fuzzy[!a] * fuzzy[c]

これにより、事前定義された論理演算子&&、|| 、!の評価が上書きされます。組み込みのIf句。

これらの定義は関数定義と同様に読み取ることができますが、本当の意味は次のとおりです。式が左側に記述されているパターンと一致する場合、式は右側の式に置き換えられます。次のように独自のIf節を定義できます。

myIf[True, then_, else_] := then
myIf[False, then_, else_] := else
SetAttributes[myIf, HoldRest]

SetAttributes[..., HoldRest]は、パターンマッチングの前に最初の引数を評価する必要があることをエバリュエーターに通知しますが、パターンのマッチングと置換が完了するまで残りの評価を保持します。

これはMathematica標準ライブラリ内で広く使用されています。式を受け取り、その記号導関数に評価される関数Dを定義します。

4
nikie

Metaluaは、これを提供するLuaと互換性のある言語とコンパイラです。

  • Lua 5.1ソースとバイトコードとの完全な互換性:クリーンでエレガントなセマンティクスと構文、驚くべき表現力、優れたパフォーマンス、ほぼユニバーサルな移植性。 -完全なマクロシステム。LISPの方言やテンプレートハスケルによって提供されるものと同様の機能を備えています。操作されたプログラムを見ることができます
    ソースコードとして、抽象構文ツリーとして、または任意の組み合わせとして
    そのどちらか、あなたのタスクにより適した方。
  • 動的に拡張可能なパーサー。これにより、他の言語とうまく調和する構文でマクロをサポートできます。

  • 言語拡張機能のセットで、すべて通常のmetauaマクロとして実装されています。

LISPとの違い:

  • 開発者がマクロを作成していないときは、マクロに煩わされないでください:言語の構文とセマンティクスは、マクロを作成していないときの95%の時間に最適です
  • 開発者に、言語の規則に従うように促してください。「ベストプラクティスに耳を傾ける」ことだけでなく、Metalua Wayの記述を容易にするAPIを提供することもできます。仲間の開発者による可読性は、コンパイラによる可読性よりも重要であり、実現が困難です。そのため、共通の尊重される規約のセットを用意することは、大きな助けになります。
  • それでも、自分の意志で処理するすべてのパワーを提供します。 LuaもMetaluaも強制的な束縛や規律を身につけていないので、自分が何をしているのかわかっていれば、言語は邪魔になりません。
  • 何か面白いことが起こったときにそれを明らかにします:すべてのメタ操作は+ {...}と-{...}の間で起こり、通常のコードから視覚的に突き出します

アプリケーションの例は、MLのようなパターンマッチングの実装です。

参照: http://lua-users.org/wiki/MetaLua

3
Clement J.

拡張可能な言語を探しているなら、Smalltalkを見てみるべきです。

Smalltalkでは、プログラミングする唯一の方法は、実際に言語を拡張することです。 IDE、ライブラリ、または言語自体の間に違いはありません。それらはすべて絡み合っているため、Smalltalkは言語ではなく環境と呼ばれることがよくあります。

Smalltalkでスタンドアロンアプリケーションを作成するのではなく、代わりに言語環境を拡張します。

いくつかのリソースと情報については、 http://www.world.st/ を確認してください。

Smalltalkの世界への入り口の方言としてPharoをお勧めしたいと思います: http://pharo-project.org

お役に立てば幸いです。

2
Bernat Romagosa

コンパイラ全体を最初から作成せずにカスタム言語を作成できるツールがあります。たとえば、コード変換ツールである Spoofax があります。入力文法と変換ルール(非常に高レベルの宣言的な方法で記述)を入力すると、Java独自に設計したカスタム言語からのソースコード(または、必要に応じて他の言語)。

したがって、言語Xの文法を取り、言語X '(カスタム拡張機能を使用したX)の文法と変換X'→Xを定義すると、SpoofaxはコンパイラーX '→Xを生成します。

現在、私が正しく理解している場合、C#のサポートが開発されている(または私が聞いた)ので、Javaのサポートが最適です。この手法は、静的文法を使用するすべての言語に適用できます(たとえば、 おそらくPerlではない )。

1
liori

Forth は、拡張性の高い別の言語です。多くのForth実装は、アセンブラまたはCで記述された小さなカーネルで構成されており、残りの言語はForth自体で記述されています。

Factor など、Forthに着想を得てこの機能を共有するスタックベースの言語もいくつかあります。

1
Dave Kirby

Funge-98

Funge-98のフィンガープリント機能を使用すると、言語の構文とセマンティクス全体を完全に再構築できます。ただし、実装者がユーザーがプログラムで言語を変更できるフィンガープリントメカニズムを提供する場合のみ(これは、通常のFunge-98構文およびセマンティクス内で実装することが理論的に可能です)。そうであれば、文字どおりにファイルの残りの部分(またはファイルの任意の部分)をC++またはLISP(または彼が望むもの)として機能させることができます。

http://quadium.net/funge/spec98.html#Fingerprints

0
Thomas Eding