Vimでクラスを作成した方法

画像

まえがき



約4年間、私はJS言語が好きであり、オブジェクト指向とクロージャーのプロトタイプ実装に特に惹かれています。 私はプログラミングの「エクササイズバイク」の大ファンであり、実用的な例で新しいことを学ぶのが大好きなので、私はこれを自分で実装してみたかったので、最近はチャンスがありました。 ある寒い冬の日、私はVimエディターに興味を持ち、そのスクリプト言語を研究して、いくつかの重要な機能、つまり連想配列と参照渡し関数に注目しました。 継承とポリモーフィズムを使用して、Vimでプロトタイプオブジェクトの向きを渡すことができず、実装できませんでした。



Vimスクリプト言語の構文に精通していない人をすぐに喜ばせたいので、詳細なコメントをコードに添付しようとします。 この作業の目的は、Vimで本格的なオブジェクト指向を作成することではなく、プロトタイピングを通じてオブジェクトパラダイムの実装を実践することでした。 もちろん、実装を可能な限り軽量かつ高速にしようとしましたが、その結果が「戦闘」スクリプトに効果的に適用できるかどうかはまだ疑問です。したがって、これを適切に処理してください。





どこから始めなければなりませんでしたか



まず、Vimの「すぐに使える」ものと、私たちが仕事に使用するものについてのいくつかの言葉。

入力 -これがない場合、整数、小数、文字列、関数へのポインタ、配列、連想配列の6つのデータ型があります。 ここで最も興味深いのは、最後のタイプです。 連想配列は、JSと同じ方法で作成されます。

''  «»      let Obj = {'foo': 'bar'}
      
      





この場合、関数は値として機能できます。 将来の施設に最適な拠点。



変数 -重要な「構文糖」を除いて、特別なものはありません-連想配列の要素にドットを介してアクセスする機能:

 ''      foo echo Obj.foo
      
      





その言葉ではなく、便利です!



関数 -重要な機能は、関数を「オブジェクト」に割り当てることができることです。つまり、連想配列から関数を呼び出すと、自己変数を介してアクセスできます。

 function! Obj.getFoo() dict return self.foo endfunction
      
      





また、関数をオブジェクトに直接追加する機能を見失うことはありません。



if、forなどの標準構造 -それらのないスクリプト言語は?!



理論



Vimスクリプト言語の機能を詳細に研究したので(簡潔にするために、単にVim言語を書くことにします)、プロトタイプオブジェクトの向きに必要なものについて考え始めました。





理想的には、すべてが次のようになっているはずです。

 use foo\bar\Class as Class ''   MyClass    Class     foo  bar let MyClass = Class.expand({'foo': '', 'bar': Class}) let obj = new MyClass() call obj.set('prop', 'val') echo obj.get('prop') call obj.set('bar', obj)
      
      







新しいクラスを作成するプロセスに注意してください。 fooプロパティは文字列型で、barプロパティは基本クラス自体、つまりClassに対応する型ですが、プロパティを追加することでいくつかの基本クラスを拡張します。 これが実際に意味することは、setメソッドを使用してfooプロパティに文字列のみを書き込み、barプロパティ(明らかな理由でMyClassを含む)にClassクラスまたはそのサブクラスのオブジェクトのみを書き込むことができるということです。 2番目のケースでは、MyClassクラスのオブジェクトをbarプロパティに書き込もうとする場合、Classクラスの構造に「制限」する必要があります。つまり、fooプロパティとbarプロパティを含めるべきではありません。 オブジェクトを再びMyClassクラスに展開することも可能ですが、必要な場合にのみ(Javaに似ています)。



コンストラクターを忘れないでください。たとえば、次のように、オブジェクトの作成時にオブジェクトの状態を判断する機能が必要です。

 let obj = new MyClass('val')
      
      







ここでは、メソッドとその再定義を定義する機能を追加し、実際のプロトタイプオブジェクト指向を取得します。



最初のステップ



もちろん、実際には、すべてが理論と同じではなく、言語の特性のためにVimで実現することはできません。かさ高さのために何かを放棄しなければなりませんでした。

 Use D/base/Object "   A     Object let A = Object.expand('A', {'x': 1}) "    A  . function! A.new(x) dict let obj = self._construct() call obj.set('x', a:x) return obj endfunction "   B    A let B = A.expand('B', {'y': Object}) "    B   function! B.new(x, y) dict let obj = self._construct() " ,      call obj.set('x', a:x) call obj.set('y', a:y) return obj endfunction "   let s:a = A.new(2) let s:b = B.new(3, s:a) echo s:b.get('x') "       Object,       echo s:b.get('y') "         echo s:b.get('y').instancedown('A').get('x')
      
      







彼は次のアイデアから始めました:すべてのオブジェクトを2つのタイプに分ける必要があります。クラスを表し、インスタンスの構造と継承やコンストラクターなどの特別なメソッドに関する情報を含むオブジェクトと、オブジェクトを表すオブジェクトです。 私はこのアイデアが気に入り、それを実装し始め、基本クラスObjectと2つの主要なメソッドexpandと_constructから始めました。

 let Object = {'class': 'Object'} function! Object.expand(className, properties) dict endfunction function! Object._construct() dict endfunction
      
      







Vimのオブジェクトには名前がないため、これらのクラスでプロパティを類型化できるようにするために、クラス名を何らかの方法で保存する必要がありました。 このために、クラスの名前を保存するクラスプロパティが追加され、Vim連想配列とクラスを区別します(このプロパティが存在する場合は、クラスまたはそのインスタンスを処理します)。 expandメソッドは、新しいクラスの名前とそのプロパティの2つのパラメーターを取り、呼び出されたクラス(つまり、プロトタイプ)に基づいて作成された新しいクラスを表すオブジェクトを返します。 _constructメソッドは、呼び出されたクラスのオブジェクトを作成して返すだけです。



最初の方法から始めましょう。 そのアルゴリズムは非常に簡単です。新しいオブジェクトを作成し、最初のパラメーターで渡された値でクラスプロパティを追加して、将来のクラスに名前を付けます。 parentプロパティを使用して親クラスにリンクを追加します。 便宜上、現在のオブジェクトに親クラスのすべてのメソッドへのリンクを作成します。 結果のオブジェクトに、2番目のパラメーターに基づくプロパティを追加します。 最後のステップが最も重要です。実際には、パラメーターからプロパティをコピーするだけでなく、将来のクラスの構造を作成します。 特に、プロパティのタイプとそのデフォルト値を定義します。



 function! Object.expand(className, properties) dict let obj = {'class': a:className} "  . "           parent. let obj.parent = self "  . "             . for k in keys(self) let t = type(self[k]) "      if t == 2 let obj[k] = self[k] endif endfor "  . "          . for k in keys(a:properties) "          class  parent,    if k == 'class' || k == 'parent' continue endif let t = type(a:properties[k]) "     ,     if t == 0 || t == 1 || t == 5 let obj[k] = {'value': a:properties[k], 'type': t} "   ,     elseif t == 3 let obj[k] = {'value': deepcopy(a:properties[k]), 'type': t} "   ,        elseif t == 4 "     if has_key(a:properties[k], 'class') let obj[k] = {'value': '', 'type': a:properties[k].class} "     elseif let obj[k] = {'value': a:properties[k], 'type': 4} endif endif endfor return obj endfunction
      
      







親クラスへの参照のおかげで、すべてのプロパティを子クラスにコピーする必要はありません。親プロパティが存在しないということは、それがObjectのルートクラスであることを意味します。



次に、コンストラクターについて説明します。 _constructメソッドは、クラスプロパティの値をオブジェクトにコピーすることにより、クラスに基づいてオブジェクトを作成して返します。 メソッドのコピーはすべてのオブジェクトで同じであるため、意味がありません。したがって、便宜上、オブジェクトからのリンクのみをクラスメソッドに追加します。

 function! Object._construct() dict let obj = {} "     let obj.class = self if has_key(self, 'parent') "            let obj.parent = self.parent._construct() "       ,       let obj.parent.child = obj endif for property in keys(self) let type = type(self[property]) "          ,     if type == 2 && property != 'expand' && property != '_construct' && property != 'new' let obj[property] = self[property] "    elseif type == 4 && has_key(self[property], 'type') let propertyType = self[property].type if propertyType == 0 || propertyType == 1 || propertyType == 5 let obj[property] = self[property].value elseif propertyType == 3 let obj[property] = deepcopy(self[property].value) elseif propertyType == 4 if has_key(self[property].value, 'class') let obj[property] = self[property].value._construct() else let obj[property] = deepcopy(self[property].value) endif endif endif endfor return obj endfunction
      
      







継承階層に従ってオブジェクトがどのように組み立てられるかに注意してください。 クラスごとに個別のオブジェクト(サブオブジェクト)が作成され、その後、それらはすべてリンク付きの1つのオブジェクトに接着されます。 これにより、親クラスのタイプのプロパティにオブジェクトを書き込むときにオブジェクトのリグレッションを実装でき、オブジェクトの作成プロセスがよりエレガントになります(すべてのクラスのすべてのプロパティをオブジェクトにコピーしますか?)

現在、プロトタイプのいくつかのプロパティが明示されています。つまり、すべてがオブジェクトです。 オブジェクトは、プロパティを他のオブジェクトにコピーすることにより実装されます。 つまり、オブジェクト指向のプロトタイプは、プログラミング言語でオブジェクトを実装するための最も簡単なアプローチです。



拡大



実際、説明されている方法は、実装全体の基盤です。 さらに、便宜上、いくつかのメソッドを追加するだけで済みます:

  1. new-パラメーターを持つコンストラクター。
  2. get-プロパティ値を取得します。
  3. set-型チェックを使用してプロパティの値を設定します。
  4. has-継承階層を考慮して、オブジェクトに指定されたプロパティがあるかどうか。
  5. instanceup-継承階層の上のサブオブジェクトを取得(回帰);
  6. instancedown-継承階層でサブオブジェクトを取得(進行);
  7. instanceof-オブジェクトが指定されたクラスまたはそのサブクラスに属するかどうか。




これらのメソッドの実装を説明する前に(メソッドの実装方法は既にご存知だと思います)、これらのメソッドの使用方法を示します。

 let A = Object.expand('A', {'a': 1}) let B = Object.expand('B', {'b': A}) ''    function! B.new(b) dict let obj = self._construct() obj.set('b', b) return obj endfunction let s:a = A.new() call s:a.set('a', 2) let s:b = B.new(s:a) ''   ,     echo s:b.class.class '' B ''   ,      b echo s:b.get('b').class.class '' A ''    b   B.  ,    B       A.     call s:b.set('b', s:b) ''   ,     A echo s:b.get('b').class.class '' A ''    s:a,     a   2 (   ),  1 (   s:b) echo s:b.get('b').get('a') '' 1 ''  ,      b ,       B echo s:b.get('b').instancedown('B').class.class '' B
      
      







面白いですね。 メソッドの詳細なリストは提供しません。詳細なコメント付きでGitHubにあります。各メソッドの操作アルゴリズムのみを説明します。

  1. 新規-なぜ余分なコンストラクターなのか? 実際、標準コンストラクタをオーバーライドする場合、このクラスのレベルプロパティをコピーするメカニズムは呼び出されたクラスでは機能しませんが、これは必要ありません。
  2. get-プロパティが指定されたオブジェクトまたは継承階層の上位のサブオブジェクトにある場合、どこも単純ではありません。それ以外の場合はエラーを報告します。
  3. set-また、複雑なことはありません。型を確認し、現在のオブジェクトまたはサブオブジェクトに書き込みます。 クラスのオブジェクトを特定し、必要に応じて回帰を実行することが重要です。
  4. has-すべても非常に簡単です。現在のオブジェクトとサブオブジェクトのプロパティを探しています。
  5. instanceup-親プロパティを使用してサブオブジェクトの階層を上に移動し、クラス名を要求されたものと比較し、見つかった場合はオブジェクトを返します。
  6. instancedown-前のものと同様、子プロパティのみを使用します。
  7. instanceof-instanceupを呼び出し、オブジェクトが返された場合、trueを返します。




使用と名前空間



残念ながら、賢明な名前空間をまだ実装することはできませんでした。 YUI 3の名前空間との類推で試しましたが、あまりきれいではありませんでした。 非常に簡単に実装された使用:

 comm! -nargs=1 Use so $HOME/.vim/ftplugin/vim/<args>.vim
      
      







ファイル.vim / ftplugin / vim / file address.vimを含むUseコマンドを定義します。各クラスは個別のファイルにあります。

 Use D/base/Object
      
      





これは、.vim / ftplugin / vim / D / baseディレクトリにあるObject.vimファイルです。

使用例



受信した基本クラスをテストするために、「Stack」アクセスタイプのセットを表すStackクラスを実装しました。 以下に使用例を示します。

 if exists('Stack') finish endif Use D/base/Object let Stack = Object.expand('Stack', {'_val': [], '_index': 0}) function! Stack.push(el) dict … endfunction function! Stack.pop() dict … endfunction function! Stack.length() dict … endfunction let s:stack = Stack.new() call s:stack.push(1) call s:stack.push(2) call s:stack.push(3) echo s:stack.length() '' 3 echo s:stack.pop() '' 3 echo s:stack.pop() '' 2 echo s:stack.pop() '' 1 echo s:stack.pop() '' ERROR
      
      







私の意見では、かなりきれいで、最も重要なのは機能的です。 わずか4キロの重さのVimのプロトタイプに基づいた本格的な(希望)オブジェクト指向。



参考資料とリソース



  1. GitHubプロジェクト
  2. 書籍「Just About Vim」
  3. Vimでのスクリプトの明確な作成と例について



All Articles