JavaでのLispインタープリターの作成





少し前に、私はただの楽しみのために、深刻なタスクを設定することなく、独自の小さなインタープリタ型スクリプト言語を書きたかった。 その後、有名な魔法の本であるSICP(コンピュータープログラムの構造と解釈)を積極的に読み、それに記載されている抽象化を実装したいと考えました-ファーストクラスオブジェクト、クロージャー、マクロなどとして機能します。 同時に、私はHaskell言語に興味があり、楽しい言語と楽しい言語を組み合わせて、言語インタープリターを作成することにしました。 私の言語では、値による呼び出しと可変変数を使用した厳密なセマンティクスが必要でした。 私のローカル環境では、結果のLispファミリーの言語をLiscriptという名前に接続しました。その完全な実装は、パーサー、カーネル、コンソール/ファイルI / Oを含めて約250行に収まります。 これは、興味のために問題を解決するのに十分すぎるほどでした。通常、カリキュラムでLispを勉強することに興奮した学生を苦しめます。



時間が経つにつれて、インタープリターにAndroidに移植する見込みのあるクロスプラットフォームGUIインターフェースを作成したかったため、Javaでインタープリターの2番目のバージョンを実装しました。 はい、グラフィカル出力と一般的にJavaとの相互運用性をサポートします。このテトリスはLiscriptで記述されており、コードの一部が表示されます。 誰が詳細に興味があるのか​​-私は猫の下で尋ねます。



1つの記事内で、言語のすべての機能とその実装を適合させることは困難であり、謙虚さにより、標準ライブラリを使用したパーサーの実装の長い説明から始まる一連の多くの記事を書くことができません。 (ちなみに、両方の実装-HaskellとJavaの両方で、解析ライブラリを使用しませんでしたが、すべてを手動で記述しました-Haskellでは、10行ものコードが必要でした)。 したがって、私は「カム、ソー、ウォン」のスタイルでいくつかのことを書きます。 一般的に、私はAST型でテキストパーサーを書きました。 いくつかの点に注意したいのは、私が括弧のファンではないということですが、解析の容易さ、コードの同格性(データとしてのコード)、および優先順位と結合性を考慮に入れる必要のある中置二項演算子の欠如、およびポーランドを介した構文解析またはステーションのソートは、そのような構文に対するすべての主張よりも重要です。 解析の結果、解釈関数に渡されるリストを取得します。 Java実装では、ライブラリタイプのリストを使用しませんでしたが、シンプルでシンプルな接続、スレッドセーフなど、独自のリストに影響を与えました。



ネタバレ見出し
/**   Liscript -   */ public static class ConsList { /**  -     */ public Object car; /**  -     */ public ConsList cdr; /**      . * @param h  -   * @param t  -   */ ConsList(Object h, ConsList t) { car = h; cdr = t; } /** ,     * @return / */ public boolean isEmpty() { return this.car == null && this.cdr == null; } /**    * @return  */ public int size() { int r = 0; ConsList p = this; while (!p.isEmpty()) {r += 1; p = p.cdr;} return r; } /** @return     */ @Override public String toString() { return showVal(this); } }
      
      







同様に、クラスタイプは関数、マクロに対して実装されます。 関数のクラス型のコード例:



ネタバレ見出し
  /**   Liscript -  */ public static class Func { /**      */ public ConsList pars; /**   */ public Object body; /** ,     */ public Env clojure; /**  * @param p      * @param b   * @param c ,     */ Func(ConsList p, Object b, Env c) { pars = p; body = b; clojure = c; } /** @return    */ @Override public String toString() { return showVal(this); } }
      
      







環境



SICP-辞書の階層構造である各辞書がHashMap <String、Object>に完全に準拠して実装され、クラスには文字列名で値を追加、受信、変更するためのメソッドが含まれます。 そして、ここであなたはすでに創造的なアプローチを取ることができます:たとえば、欠落しているキー(変数名)によって値を取得しようとしている場合はどうすればよいでしょうか? エラーで実行を中止しますか? または、キーの文字列表現を返しますか? 同じことは、現在の環境フレームのディクショナリにすでに名前が含まれている変数を追加しようとした場合、エラーを与えるか、変数を再定義しますか? その結果、このような些細なことで言語の機能が決まります。たとえば、自分で定義できることが好きです。 このアプローチのすべての長所と短所を使用して、自動引用とディープリンクを取得します。



また、標準ライブラリではなく、言語の中核で特殊な形式の可変アリティを実装したいと考えました。 それらの多くは多くのパラメータの転送を許可し、および/または他のLisp方言の同名の対応するものとまったく同じように動作しません-これはインタープリターの実装を複雑にしません。 環境での作業の例(=>インタープリターが答えた後):



 ++ "a1 = " a1 ", b1 = " b1 ", c1 = " c1 => a1 = a1, b1 = b1, c1 = c1 def a1 1 b1 (+ a1 1) (++ "c" a1) (+ a1 b1) => OK ++ "a1 = " a1 ", b1 = " b1 ", c1 = " c1 => a1 = 1, b1 = 2, c1 = 3 set! (++ "a" 1) 5 c1 10 => OK ++ "a1 = " a1 ", b1 = " (get (++ "b" 1)) ", c1 = " c1 => a1 = 5, b1 = 2, c1 = 10
      
      





機能



これらはファーストクラスのオブジェクトです。 作成されると、関数は現在のコンテキストをキャプチャします。 呼び出されると、引数は厳密に順番に評価されます。 自動テールコール最適化の実装-TCO:



 defn is-even (n) (cond (= n 0) true (is-odd (- n 1)) ) => OK defn is-odd (n) (cond (= n 0) false (is-even (- n 1)) ) => OK is-even 10000 => true is-even 10001 => false
      
      





トレイの特殊な形状により、関数を適用するときにコールスタックを印刷できます。 したがって、たとえば、3の階乗が計算されます。



ネタバレ見出し
 defn f (i) (cond (< i 2) 1 (* i (f (- i 1)))) => OK tray f 3 => 1 ← (f 3) 2 ← f 2 → (lambda (i) (cond (< i 2) 1 (* i (f (- i 1))))) 2 ← (cond (< i 2) 1 (* i (f (- i 1)))) 3 ← (< i 2) 4 ← i 43 3 → false 3 ← (* i (f (- i 1))) 4 ← i 43 4 ← (f (- i 1)) 5 ← f 5 → (lambda (i) (cond (< i 2) 1 (* i (f (- i 1))))) 5 ← (- i 1) 6 ← i 63 52 5 ← (cond (< i 2) 1 (* i (f (- i 1)))) 6 ← (< i 2) 7 ← i 72 6 → false 6 ← (* i (f (- i 1))) 7 ← i 72 7 ← (f (- i 1)) 8 ← f 8 → (lambda (i) (cond (< i 2) 1 (* i (f (- i 1))))) 8 ← (- i 1) 9 ← i 92 81 8 ← (cond (< i 2) 1 (* i (f (- i 1)))) 9 ← (< i 2) 10 ← i 101 9 → true 81 71 62 52 42 36 26 16 6
      
      







引数関数が呼び出されたときに、仮パラメーターの数を超える数が渡された場合、最後の仮パラメーターは、計算された残りの渡された引数のリストにアクセスします。 少ない場合、これらの仮パラメータは関数の計算環境で接続されず、その本体を計算するときに、階層の上位レベルの環境で検索されます。 カリー化のオプションを選択できます-部分的に適用された関数を返すか、引数の数に一貫性のないエラーを与えることができます。



マクロ



Liscriptは、いわゆるランタイムマクロをサポートします。これは、言語の最初のクラスのオブジェクトです。 それらを作成し、名前に関連付け、関数の結果として返され、作業の過程で実行することができます(コード解釈)。 ソースコードテキストから取得した式は、マクロを展開する予備段階なしですぐに解釈が開始されるため、マクロは言語のすべてのタイプのままであり、マクロを計算するためのすべてのルールに従って解釈中に展開および計算されます-最初に、計算された引数がマクロ本体に代入され、次にこのマクロ本体が計算されます現在の環境で(関数本体とは対照的に、常に独自の別の環境で計算され、 実パラメータの計算値)aritelno。



  def m (macro (ir) (cond (<= i 0) 'r (m (- i 1) (cons ir)))) => OK m 5 nil => (cons (- (- (- (- 5 1) 1) 1) 1) (cons (- (- (- 5 1) 1) 1) (cons (- (- 5 1) 1) (cons (- 5 1) (cons 5 nil))))) eval (m 5 nil) => (1 2 3 4 5)
      
      





Javaとの相互運用性



Java Reflectionエンジンを介して実装されます。 特別な2つの形式のみ:class-クラスをフルネームで定義し、java-渡されたパラメーターでクラスメソッドを呼び出します。 同じ名前のオーバーロードされたメソッドの中から目的のメソッドを検索するには、渡されたパラメーターの数とタイプを考慮します。 作業を高速化するために、インタープリターの現在のセッションで一度呼び出されて呼び出されたクラスメソッドは、呼び出されたメソッドのテーブルに格納され、メソッドが呼び出されると、最初にテーブルが検索されます。



 def m (java (class "java.util.HashMap") "new") => OK java m "put" 1 "a" => OK java m "put" "b" 2 => OK java m "get" 1 => a m => {1=a, b=2}
      
      





したがって、グラフィックを含む実装言語の多くのリソースにアクセスできます。 このコードは、白い背景に赤い線が描かれた別のウィンドウを開きます。



 (def image (java (class "java.awt.image.BufferedImage") "new" 100 100 1)) (def imageGraphics (java image "createGraphics")) (java imageGraphics "setBackground" (java (class "java.awt.Color") "new" 255 255 255)) (java imageGraphics "clearRect" 0 0 100 100) (java imageGraphics "setColor" (java (class "java.awt.Color") "new" 255 0 0)) (java imageGraphics "drawLine" 10 10 90 90) (def icon (java (class "javax.swing.ImageIcon") "new")) (java icon "setImage" image) (def label (java (class "javax.swing.JLabel") "new")) (java label "setIcon" icon) (def window (java (class "javax.swing.JFrame") "new")) (java window "setLayout" (java (class "java.awt.FlowLayout") "new")) (java window "add" label) (java window "setVisible" true) (java window "pack")
      
      





もちろん、典型的なブロックを選択して別々の関数またはマクロにし、それらをインタープリターの起動時にロードされる標準ライブラリに一度書き込むことができます。 そして、インタープリターは、共通の可変環境を備えた個別のブックマークでタスクのマルチスレッド実行を実装しているため(はい、辞書のストレージとして選択されたHashMapはスレッドセーフではなく、環境をパラレルスレッドから変更する際に問題が発生する可能性があり、HashTableを使用した方が良いことを知っています)、そのため、特定のタイマー待機時間の後に自分自身を呼び出す1つのタブでプロシージャを開始し、別のウィンドウ(ストリーム)でユーザー入力を要求して実行するプロシージャを開始できます。 特定のアクション。 そこで、テトリスが実装されました(ユーザー入力をブロックする機能を備えています-各コマンドはCtrl + Enterで確認する必要があります)。



このプロジェクトはGithubのgithub.com/Ivana-/Liscript-GUI-Java-Swingで入手できます。 これには、SwingのインタープリターのソースファイルとそのGUI(ヤードで既に21世紀になっていることは知っていますが、JavaFXで書き直すのは急いではありません)、標準ライブラリのスクリプトファイル、言語記述、インタープリター、および十分な数のデモ例が含まれています。 批判したい場合は、これがJavaおよびOOP言語全般での私の最初のプロジェクトであるため、多くが非最適に実装される可能性があることを考慮してください。 しかし、私はこのプロジェクトに関する意見やフィードバックに興味があります。



All Articles