Rustの数値型クラス

さびの抽象化は、OOPの通常のものとは異なります。 特に、クラス(オブジェクトのクラス)の代わりに、「トレイト」と呼ばれるタイプのクラスが使用されます(この用語では不純物 -ミックスインが隠されているScalaのトレイトと混同しないでください)。

型クラスはRustに固有のものではなく、Haskell、Mercury、Goでサポートされており、ScalaおよびC ++で少し逆さまに実装できます。



二重数の例を使用して、Rustでそれらがどのように実装されているかを示し、個々の非自明な(またはうまく機能していない)瞬間を分析します。



数値型のインターフェイスはかなり扱いにくいため、ここではコードの断片のみを挿入します。 すべてのコードはgithubで利用可能です(更新:動作バージョンはcrates.ioで利用可能です)。

ここに実装されているインターフェイスのほとんどは、実験的または不安定な状態であり、変更される可能性があります。 コードとテキストの関連性を保つようにします。



Rustはオペレーションのオーバーロードをサポートしますが、C ++とは異なり、オペレーションには一般的なアルファベット名の同義語メソッドがあります。 したがって、a + ba.add(b)と書くことができ、操作 '+'をオーバーライドするには、addメソッドを実装するだけです。





型クラスとは何ですか?

型クラスは、多くの場合、インターフェイスと比較されます。 実際、一部のデータ型で何ができるかを決定しますが、これらの操作は個別に実装する必要があります。 インターフェースとは異なり、特定のタイプのタイプクラスの実装は新しいタイプを作成せず、古いものと一緒に生きますが、実装されたインターフェースに関する古いタイプは何も知らない場合があります。 このインターフェイスを使用するコードがこのデータ型で機能するためには、データ型、インターフェイス、コードを編集する必要はありません-型のインターフェイスを実装するだけで十分です。



OOPスタイルのインターフェイスとは異なり、型クラスは型を複数回参照できます。 Rustでは、このリンクはSelfと呼ばれますが、Haskellでは、ほとんど何でも呼び出すことができます。 たとえば、Haskellでは、「+」メソッドは両方の引数がまったく同じ型を持ち、まったく同じ型のオブジェクトが返されることを要求します(Rustでは、 Add型クラスでは、これらの型は異なる場合があります-特に、DurationとTimespecを追加できます)。 戻り値の型も重要です。引数では、クラスの型はまったく使用されない可能性があり、コンパイラは取得する型に基づいて、使用するメソッドの実装を決定します。 たとえば、RustにはZeroタイプのクラスとコードがあります
let float_zero:f32 = Zero::zero(); let int_zero:i32 = Zero::zero();
      
      



異なるタイプの変数に異なるゼロを割り当てます。



Rustでの方法

説明

型クラスは、traitキーワードを使用して作成され、その後に名前(場合によってはパラメーター付き、C ++など)とメソッドのリストが続きます。 メソッドにはデフォルトの実装がある場合がありますが、そのような実装には型内部へのアクセス権がないため、他のメソッドを使用する必要があります(たとえば、等式を否定することで不等式​​( !=Ne )を表現します)。
 pub trait PartialEq { /// This method tests for `self` and `other` values to be equal, and is used by `==`. fn eq(&self, other: &Self) -> bool; /// This method tests for `!=`. #[inline] fn ne(&self, other: &Self) -> bool { !self.eq(other) } }
      
      



次に、標準ライブラリの型クラスの説明を示します。これには、同等性を比較できる型が含まれています。

各メソッドのselfまたは&selfと呼ばれる最初の引数は、古典的なOOPのこれに類似しています。 アンパサンドの存在は、オブジェクトの所有権を転送する方法を示し、C ++とは異なり、オブジェクトを変更する機能に影響しません(参照渡しまたは値渡し)。 オブジェクトを変更する権利は、明示的にmutを示します。

2番目の引数は、最初の引数と同じ型でなければなりません。これはSelfによって示されます。

後で、この引数が不要であるという事実に遭遇します-実際にはまだ「動的」のままですが、静的メソッドのようなものが判明します-ディスパッチは、他のパラメータまたは期待される結果のタイプに従って実行されます。

 pub trait Add<RHS,Result> { /// The method for the `+` operator fn add(&self, rhs: &RHS) -> Result; }
      
      



Rustの「+」操作は、同じタイプの引数と結果を要求する必要はありません。 このために、型クラスはテンプレートになります。テンプレート引数は、2番目の引数と結果の型です。

比較のために、Haskellでは、型クラスはパラメーター化されません(型自体による場合を除く)が、個別の型ではなく、ペア、トリプル、および他の型の型(拡張MultiParamTypeClasses)を含むことができ、同様のことができます。 彼らは、Rustのリリースにこの機能のサポートを追加することを約束します。

C ++との構文上の違いに注意する価値があります。Rustのエンティティ(この場合は型クラス)の記述自体がテンプレートであり、C ++ではテンプレートはキーワードを使用して個別に宣言されます。 C ++のアプローチは、いくつかの点で、より論理的ですが、読みにくくなっています。

別のゼロの例を考えてみましょう。
 pub trait Zero: Add<Self, Self> { /// Returns the additive identity element of `Self`, `0`. /// /// # Laws /// /// ```{.text} /// a + 0 = a ∀ a ∈ Self /// 0 + a = a ∀ a ∈ Self /// ``` /// /// # Purity /// /// This function should return the same result at all times regardless of /// external mutable state, for example values stored in TLS or in /// `static mut`s. // FIXME (#5527): This should be an associated constant fn zero() -> Self; /// Returns `true` if `self` is equal to the additive identity. #[inline] fn is_zero(&self) -> bool; }
      
      



この型クラスの説明で継承を確認できます-Zeroを実装するには、最初にAddを実装する必要があります(同じ型でパラメーター化されています)。 これは、実装のない通常のインターフェイスの継承です。 多重継承も許可されます。このため、祖先は「+」でリストされます。

fn zero()-> Selfメソッドに注意してください 。 これは静的メソッドと見なすことができますが、後でOOPの静的メソッドよりも動的であることがわかります(特に、「ファクトリ」の実装に使用できます)。



実装

複素数の追加実装を検討してください。
 impl<T: Clone + Num> Add<Complex<T>, Complex<T>> for Complex<T> { #[inline] fn add(&self, other: &Complex<T>) -> Complex<T> { Complex::new(self.re + other.re, self.im + other.im) } }
      
      



複素数は、実数の表現によってパラメーター化された一般化された型です。 加算の実装もパラメータ化されています-実際の数のインターフェイスが実装されている場合、実際の数のさまざまなバリアント上の複素数に適用できます。 この場合、必要なインターフェースは豊富です-Clone (コピーの作成を許可)とNum (数値の基本操作、特に継承するAddを含む)の実装を想定しています。



導出

単純な標準インターフェースの実装を自分で書くのが面倒な場合は、派生ルーチンを使用してこのルーチン作業をコンパイラーに渡すことができます。
 #[deriving(PartialEq, Clone, Hash)] pub struct Complex<T> { /// Real portion of the complex number pub re: T, /// Imaginary portion of the complex number pub im: T }
      
      



ここで、タイプTが必要なすべてをサポートする場合、ライブラリの開発者はPartialEq、Clone、およびHashインターフェイスの実装を作成するように求められます。

現在、Clone、Hash、Encodable、Decodable、PartialEq、Eq、PartialOrd、Ord、Rand、Show、Zero、Default、FromPrimitive、Send、Sync、Copyのタイプのクラスの実装の自動生成がサポートされています。



数値型クラス

std :: numモジュールは、数値のさまざまなプロパティに関連付けられた多数の型クラスを記述します。

これらは、他のいくつかの特性を参照できます-比較およびメモリ操作のために(たとえば、 Copyは、この型をバイト単位でコピーできることをコンパイラに通知します)。

図では、デュアル番号用に実装したインターフェイスを強調表示しました。



二重数の実装

データ型は簡単に配置されます:
 pub struct Dual<T> { pub val:T, pub der:T }
      
      





標準ライブラリの複素数とは異なり、最小限の仮定に基づいてインターフェイスを実装しようとしました。 したがって、Add for meの実装には、ソースタイプのAddインターフェースとMul-Mul + Addのみが必要です。

時々これは奇妙なコードにつながりました。 たとえば、CloneをサポートするためにSignedは不要であり、absメソッドでポジティブデュアルのコピーを返すには、ゼロに追加する必要がありました。
 impl<T:Signed> Signed for Dual<T> { fn abs(&self) -> Dual<T> { if self.is_positive() || self.is_zero() { self+Zero::zero() // XXX: bad implementation for clone } else if self.is_negative() { -self } else { fail!("Near to zero") } } }
      
      



そうでない場合、コンパイラはこのオブジェクトの所有権を追跡できません。

タイプZero :: zero()は明示的に設定されないことに注意してください。 コンパイラーは、 Numを実装するselfを追加しようとするため、それがどうあるべきかを推測します。したがって、 Add <Self、Self>を追加します。 しかし、コンパイル時のタイプSelfはまだ知られていません-それはテンプレートパラメータによって設定されます。 したがって、 zeroメソッドは、 Dual <T>の Num実装メソッドのテーブルに動的に配置されます!



また、Floatが型全体を特徴付ける整数定数をどのように実装するかという興味深いトリックにも注目します。 つまり、入力でインスタンスを受け取ることはできません(正しいコンテキストにない可能性があります)が、静的メソッドに類似している必要があります。 同じ問題がHaskellでしばしば発生し、それを解決するために、必要なタイプの偽のパラメーターがそのようなメソッドに追加されます。 Haskell言語は怠け者であり、未使用の引数として常に「Not used」エラーを渡すことができます。 厳格なRust言語では、この手法は機能せず、このためのオブジェクトの作成には費用がかかりすぎる可能性があります。 したがって、回避策が使用されます- オプション<Self>は渡されません
 #[allow(unused_variable)] impl<T:Float> Float for Dual<T> { fn mantissa_digits(_unused_self: Option<Dual<T>>) -> uint { let n: Option<T> = None; Float::mantissa_digits(n) } }
      
      



パラメーターは使用されないため、デフォルトではコンパイラーは警告を生成します。 これを抑制するには、パラメータ名を「_」文字で開始する方法と#[allow(unused_variable)]ディレクティブを使用する方法の2つがあります。



All Articles