やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Rustのトレイト(trait)に衝撃を受けた

トレイト(trait)すごい

「コンセプトから理解するRust」を読んで衝撃を受けました...。
トレイト(trait)は今まで自分が出会ったことがない概念です。

多くの言語ではinterfaceや基底クラスを作って「あなたはこの関数orメソッドを実装してください」というルールを定めることが出来ます。pythonであれば、以下のコードのように基底クラスを作成して、継承させることで実装が出来ます。

from abc import ABCMeta, abstractmethod

# 基底クラス
class MonsterInterface(metaclass=ABCMeta):
  @abstractmethod
  def name(self): # nameの実装をルール化
    pass

class Slime(MonsterInterface):
  def name(self):
    return 'スライム'

class KingSlime(MonsterInterface):
  def name(self):
    return 'キングスライム'

print(Slime().name()) # スライム
print(KingSlime().name()) # キングスライム

nameが実装されていないとエラーになる

class MetalSlime(MonsterInterface):
  pass

MetalSlime()

# Traceback (most recent call last):
#   File "Main.py", line 12, in <module>
#     MetalSlime()
# TypeError: Can't instantiate abstract class MetalSlime with abstract methods name

このように基底クラスを継承、interfaceに対して実装するという方法が従来の方法かと思うのですが、Rustの場合はトレイト(trait)と呼ばれるものを使用します。トレイトは構造体に対して1つ1つ実装することが可能で、不要であれば実装しなくても構いません。

struct Slime;
struct KingSlime;
struct MetalSlime;

trait Monster {
  fn name(&self) -> &'static str;
}

impl Monster for Slime {
  fn name(&self) -> &'static str {
    "スライム"
  }
}

impl Monster for KingSlime {
  fn name(&self) -> &'static str {
    "キングスライム"
  }
}

fn main() {
  println!("{}", Slime{}.name()); // スライム
  println!("{}", KingSlime{}.name()); // キングスライム
  MetalSlime{};
}

「ふーん、そうなんだ」という声が聞こえてきますが、面白いのはここからです。

体力(hp関数)の追加

仮にMonasterに体力を返すhpという関数を実装します。先ほどのpythonの例であれば基底クラスに新しくhpという関数を作成して継承先のクラスで実装するようにルール化するか、もしくはそれぞれのクラスでhpを適宜、追加します。

from abc import ABCMeta, abstractmethod

# 基底クラス
class MonsterInterface(metaclass=ABCMeta):
  @abstractmethod
  def name(self): # nameの実装をルール化
    pass
  
  @abstractmethod
  def hp(self):
    pass

class Slime(MonsterInterface):
  def name(self):
    return 'スライム'
    
  def hp(self):
    return 10

class KingSlime(MonsterInterface):
  def name(self):
    return 'キングスライム'
    
  def hp(self):
    return 70

slime = Slime()
king_slime = KingSlime()
print(f"{slime.name()}の体力: {slime.mp()}") # スライムの体力: 10
print(f"{king_slime.name()}の体力: {king_slime.mp()}") # キングスライムの体力: 70

特筆することはありません。次はRustのコードへのhpの追加を行います。

struct Slime;
struct KingSlime;
struct MetalSlime;

trait Monster {
  fn name(&self) -> &'static str;
  fn hp(&self) -> i32;
}

impl Monster for Slime {
  fn name(&self) -> &'static str {
    "スライム"
  }

  fn hp(&self) -> i32 {
    10
  }
}

impl Monster for KingSlime {
  fn name(&self) -> &'static str {
    "キングスライム"
  }

  fn hp(&self) -> i32 {
    70
  }
}


fn main() {
  let slime = Slime{};
  let king_slime = KingSlime{};
  println!("{}の体力: {}", slime.name(), slime.hp());
  println!("{}の体力: {}", king_slime.name(), king_slime.hp());
  MetalSlime{};
}

こちらも同じく、Monsterトレイトに対して新しくhpを追加して、それぞれの構造体に対するimplでhp関数を実装しました。何も問題ありません。

必殺技(special attack関数)の追加

では、次に必殺技(special attack)を実装します。なおスライムは必殺技は使えず、キングスライムは必殺技が使えるものとします。必殺技の有無はモンスターによって変わるため、別の基底クラスを用意して実装してみます。
(※通常攻撃もないのに必殺技から作るのかよというのは多めに見てください🙏)

class SpecialAttackInterface(metaclass=ABCMeta):
  @abstractmethod
  def sp_attack(self):
    pass


class Slime(MonsterInterface):
  def name(self):
    return 'スライム'
    
  def hp(self):
    return 10

class KingSlime(MonsterInterface, SpecialAttackInterface):
  def name(self):
    return 'キングスライム'
    
  def hp(self):
    return 70
    
  def sp_attack(self):
    return 100
      

slime = Slime()
king_slime = KingSlime()
print(f"{slime.name()}は必殺技が使えません") # スライムは必殺技が使えません
print(f"{king_slime.name()}の必殺技: ダメージ={king_slime.sp_attack()}") # キングスライムの必殺技: ダメージ=100

pythonの場合は別の基底クラスを用意して、それをKingSlimeクラスに継承させました。継承したKingSlimeクラスのコードには当然ながらsp_attackが追加されました。次にRustの場合ですが、新しくSpecialAttackというトレイトを作成します。

trait SpecialAttack {
  fn sp_attack(&self) -> i32;
}

impl SpecialAttack for KingSlime {
  fn sp_attack(&self) -> i32 {
    100
  }
}


fn main() {
  let slime = Slime{};
  let king_slime = KingSlime{};
  println!("{}は必殺技が使えません", slime.name());
  println!("{}の必殺技: ダメージ={}", king_slime.name(), king_slime.sp_attack());
  MetalSlime{};
}

先程のpythonのコードとの一番の違いは既存のコードに対して全く手を入れていないという点です。
pythonでは基底クラスを作成後、継承して、継承したクラス内に新しい関数を作成しましたが、Rustでトレイトを使った場合は既存コードに手を加えることはなく、必要な構造体だけトレイトを実装すれば良いです。

既存のコードに手を加えなくても良いというのが、コード品質、安全性、ファイル分割などの視点で見れば非常に優れています。Haskellにはtypeclassという概念がありましたが「必要なら実装してね」という点で非常に似ていると感じました。

www.okb-shelf.work

当時、Haskellを触っていた時に、なぜ感動しなかったのか分かりませんがRustのトレイトを知って衝撃を受けました。自分のおすすめは「コンセプトから理解するRust」という書籍です。文法や型とかはいいから、とりあえずRustの概念に触れたいという自分のような方にはぴったりです。