跳到主要内容

MoonBit中的面向对象编程

· 阅读需 22 分钟
刘子悦

alt text

引言

在软件开发的世界里,面向对象编程(OOP)无疑是一座绕不开的话题。Java、C++ 等语言凭借其强大的 OOP 机制构建了无数复杂的系统。然而,Moonbit,作为一门围绕函数式编程构建的现代语言,它如何实现 OOP?

Moonbit 是一门以函数式编程为核心的语言,它的面向对象编程思路与传统编程语言有很大不同。它抛弃了传统的继承机制,拥抱"组合优于继承"的设计哲学。乍一看,这可能让习惯了传统OOP的程序员有些不适应,但细细品味,你会发现这种方法有着意想不到的优雅和实用性。

本文将通过一个生动的RPG游戏开发例子,带你深入体验Moonbit中的面向对象编程。我们会逐一剖析封装、继承和多态这三大特性,并与C++的实现方式进行对比,最后提供一些实际开发中的最佳实践建议。

封装(Encapsulation)

想象一下,我们要开发一款经典的单机RPG游戏。在这个奇幻世界里,英雄四处游历,与怪物战斗,向NPC商人购买装备,最终拯救被困的公主。要构建这样一个世界,我们首先需要对其中的所有元素进行建模。

不管是勇敢的英雄、凶恶的怪物,还是朴实的桌椅板凳,它们在游戏世界中都有一些共同的特征。我们可以将这些对象都抽象为Sprite(精灵),每个Sprite都应该具备几个基本属性:

  • ID:对象的唯一标识符,就像身份证号码一样。
  • xy:在游戏地图上的坐标位置。

C++的经典封装方式

在C++的世界里,我们习惯于用class来构建数据的封装:

// 一个基础的 Sprite 类
class Sprite {
private:
    int id;
    double x;
    double y;

public:
    // 构造函数,用来创建对象
    Sprite(int id, double x, double y) : id(id), x(x), y(y) {}

    // 提供一些公共的 "getter" 方法来访问数据
    int getID() const { return id; }
    double getX() const { return x; }
    double getY() const { return y; }

    // 可能还需要 "setter" 方法来修改数据
    void setX(double newX) { x = newX; }
    void setY(double newY) { y = newY; }
};

你可能会问:"为什么要搞这么多get方法,直接把属性设为public不就好了?"这就涉及到封装的核心思想了。

为什么需要封装?

想象一下,如果你的同事直接通过sprite.id = enemy_id来修改ID,英雄瞬间就能"变身"成敌人的同伙,直接大摇大摆地走到终点——但这显然不是我们想要的游戏机制!封装就像给数据加了一道防护网,private字段配合getter方法,确保外部只能读取而无法随意修改关键数据。这样的设计让代码更加健壮,避免了意想不到的副作用。

Moonbit的优雅封装

到了Moonbit这里,封装的思路发生了微妙而重要的变化。让我们先看一个简单的版本:

// 在 Moonbit 中定义 Sprite
pub struct Sprite {
  id: Int          // 默认不可变,外部可读但不可写
  mut x: Double    // mut 关键字表示可变
  mut y: Double
}

// 我们可以为 struct 定义方法
pub fn Sprite::get_x(self: Self) -> Double {
  self.x
}

pub fn Sprite::get_y(self: Self) -> Double {
  self.y
}

pub fn Sprite::set_x(self: Self, new_x: Double) -> Unit {
  self.x = new_x
}

pub fn Sprite::set_y(self: Self, new_y: Double) -> Unit {
  self.y = new_y
}

注意到这里有两个关键的不同点:

1. 可变性的显式声明

在Moonbit中,字段默认是不可变的(immutable)。如果你想让某个字段可以被修改,必须明确使用mut关键字。在我们的Sprite中,id保持不可变——这完美符合我们的设计意图,毕竟我们不希望对象的身份被随意篡改。而xy被标记为mut,因为精灵需要在世界中自由移动。

2. 更简洁的访问控制

由于id本身就是不可变的,我们甚至不需要为它编写get_id方法!外部代码可以直接通过sprite.id来读取它,但任何尝试修改的行为都会被编译器坚决拒绝。这比C++的"private + getter"模式更加简洁明了,同时保持了同样的安全性。

💡 实践建议

在设计数据结构时,优先考虑哪些字段真正需要可变。Moonbit的默认不可变设计能帮你避免很多意外的状态修改bug。

继承(Inheritance)

面向对象编程的第二大支柱是继承。在我们的RPG世界中,会有多种不同类型的Sprite。为了简化示例,我们定义三种:

  • Hero(英雄):玩家操控的角色
  • Enemy(敌人):需要被击败的对手
  • Merchant(商人):售卖道具的NPC

C++的继承层次

在C++中,我们很自然地使用类继承来构建这种层级关系:

class Hero : public Sprite {
private:
    double hp;
    double damage;
    int money;

public:
    Hero(int id, double x, double y, double hp, double damage, int money)
        : Sprite(id, x, y), hp(hp), damage(damage), money(money) {}

    void attack(Enemy& e) { /* ... */ }
};

class Enemy : public Sprite {
private:
    double hp;
    double damage;

public:
    Enemy(int id, double x, double y, double hp, double damage)
        : Sprite(id, x, y), hp(hp), damage(damage) {}

    void attack(Hero& h) { /* ... */ }
};

class Merchant : public Sprite {
public:
    Merchant(int id, double x, double y) : Sprite(id, x, y) {}
    // 商人专有的方法...
};

C++的面向对象建立在 "is-a" 关系基础上:Hero是一个SpriteEnemy是一个Sprite。这种思维方式直观且容易理解。

Moonbit的组合式思维

现在轮到Moonbit了。这里需要进行一次重要的思维转换:Moonbit的struct不支持直接继承。取而代之的是使用trait(特质)和组合(Composition)。

这种设计迫使我们重新思考问题:我们不再将Sprite视为可被继承的"父类",而是将其拆分为两个独立的概念:

  1. SpriteData:一个纯粹的数据结构,存储所有Sprite共享的数据
  2. Sprite:一个trait,定义所有Sprite应该具备的行为能力

让我们看看实际的代码:

// 1. 定义共享的数据结构
pub struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}

// 2. 定义描述通用行为的 Trait
pub trait Sprite {
  getSpriteData(Self) -> SpriteData  // 必须实现的核心方法
  getID(Self) -> Int = _             // = _ 表示有默认实现
  getX(Self) -> Double = _
  getY(Self) -> Double = _
  setX(Self, Double) -> Unit = _
  setY(Self, Double) -> Unit = _
}

// Sprite的默认实现
// 只要实现了 getSpriteData,就自动拥有了其他方法
impl Sprite with getID(self) {
  self.getSpriteData().id
}

impl Sprite with getX(self) {
  self.getSpriteData().x
}

impl Sprite with getY(self) {
  self.getSpriteData().y
}

impl Sprite with setX(self, new_x) {
  self.getSpriteData().x = new_x
}

impl Sprite with setY(self, new_y) {
  self.getSpriteData().y = new_y
}

理解Trait的威力

Sprite trait定义了一个"契约":任何声称自己是Sprite的类型,都必须能够提供它的SpriteData。一旦满足了这个条件,getIDgetXgetY等方法就会自动可用。这里的= _语法表示该方法有默认实现,这是Moonbit的最新语法特性。

有了这个基础架构,我们就可以实现具体的游戏角色了:

// 定义Hero
pub struct Hero {
  
SpriteData
sprite_data
:
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
// 组合SpriteData
Double
hp
:
Double
Double
Int
damage
:
Int
Int
Int
money
:
Int
Int
} // 实现Sprite trait,只需要提供getSpriteData方法 pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(self : Hero) -> SpriteData
getSpriteData
(
Hero
self
) {
Hero
self
.
SpriteData
sprite_data
} pub fn
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::
(self : Hero, e : Enemy) -> Unit
attack
(
Hero
self
:
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Self
,
Enemy
e
:
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
) ->
Unit
Unit
{
// 攻击逻辑... } // 定义Enemy pub struct Enemy {
SpriteData
sprite_data
:
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
Double
hp
:
Double
Double
Int
damage
:
Int
Int
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(self : Enemy) -> SpriteData
getSpriteData
(
Enemy
self
) {
Enemy
self
.
SpriteData
sprite_data
} pub fn
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::
(self : Enemy, h : Hero) -> Unit
attack
(
Enemy
self
:
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Self
,
Hero
h
:
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
) ->
Unit
Unit
{
// 攻击逻辑... } // 定义Merchant pub struct Merchant {
SpriteData
sprite_data
:
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Merchant {
  sprite_data: SpriteData
}
Merchant
with
(self : Merchant) -> SpriteData
getSpriteData
(
Merchant
self
) {
Merchant
self
.
SpriteData
sprite_data
}

注意这里的思维方式转变:Moonbit采用的是 "has-a" 关系,而不是传统OOP的 "is-a" 关系。Hero拥有SpriteData,并且实现Sprite的能力。

看起来Moonbit更复杂?

初看之下,Moonbit的代码似乎比C++要写更多"模板代码"。但这只是表面现象!我们这里刻意回避了C++的诸多复杂性:构造函数、析构函数、const正确性、模板实例化等等。更重要的是,Moonbit这种设计在大型项目中会展现出巨大优势——我们稍后会详细讨论这一点。

多态(Polymorphism)

多态是面向对象编程的第三大支柱,指的是同一个接口作用于不同对象时产生不同行为的能力。让我们通过一个具体例子来理解:假设我们需要实现一个who_are_you函数,它能够识别传入对象的类型并给出相应回答。

C++的多态机制

C++的多态机制实际上是一个比较复杂的问题,笼统地说,它包括静态多态(模板)和动态多态(虚函数、RTTI等)。对C++多态机制的讨论超出了我们这篇文章的内容范围,读者如果有兴趣可以自行查阅相关书籍。这里我们重点讨论两种经典的运行时多态方法。

方法一:虚函数机制

最传统的做法是为基类定义虚函数,让子类重写:

class Sprite {
public:
    virtual ~Sprite() = default;  // 虚析构函数
    // 定义一个"纯虚函数",强制子类必须实现它
    virtual std::string say_name() const = 0;
};

// 在子类中"重写"(override)这个函数
class Hero : public Sprite {
public:
    std::string say_name() const override {
        return "I am a hero!";
    }
    // ...
};

class Enemy : public Sprite {
public:
    std::string say_name() const override {
        return "I am an enemy!";
    }
    // ...
};

class Merchant : public Sprite {
public:
    std::string say_name() const override {
        return "I am a merchant.";
    }
    // ...
};

// 现在 who_are_you 函数变得极其简单!
void who_are_you(const Sprite& s) {
    std::cout << s.say_name() << std::endl;
}

方法二:RTTI + dynamic_cast

如果我们不想为每个类单独定义虚函数,还可以使用C++的运行时类型信息(RTTI):

class Sprite {
public:
    // 拥有虚函数的类才能使用 RTTI
    virtual ~Sprite() = default;
};

// who_are_you 函数的实现
void who_are_you(const Sprite& s) {
    if (dynamic_cast<const Hero*>(&s)) {
        std::cout << "I am a hero!" << std::endl;
    } else if (dynamic_cast<const Enemy*>(&s)) {
        std::cout << "I am an enemy!" << std::endl;
    } else if (dynamic_cast<const Merchant*>(&s)) {
        std::cout << "I am a merchant." << std::endl;
    } else {
        std::cout << "I don't know who I am" << std::endl;
    }
}

RTTI的工作原理

开启RTTI后,C++编译器会为每个有虚函数的对象维护一个隐式的type_info结构。当使用dynamic_cast时,编译器检查这个类型信息:匹配则返回有效指针,不匹配则返回nullptr。这种机制虽然功能强大,但也带来了运行时开销。

不过,第二种方法在大型项目中存在一些问题:

  1. 类型不安全。如果你新增了一个子类但忘记修改who_are_you函数,这个bug只能在运行时才能被发现!在现代软件开发中,我们更希望此类错误能在编译时就被捕获。
  2. 性能不够好。开启RTTI后,每一次判断类型都会调用一个比较麻烦的类型信息读取方法,这不太利于优化,因此很容易出现性能上的问题。
  3. 数据不透明。开启RTTI后,C++会为每一个类隐式地添加一块类型信息,但是代码的编写者是看不到的,这对于一些期望对代码拥有更强掌控力的库编写者而言非常头疼。事实上,不少大型项目会考虑禁用RTTI,最典型的就是LLVM,这个C++的编译器项目反而自己并不愿意使用RTTI.

Moonbit的ADT机制

Moonbit通过引入代数数据类型(Algebraic Data Type,ADT)来优雅地解决多态问题。我们需要添加一个新的结构——SpriteEnum

pub trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum  // 新增:类型转换方法
}

// Moonbit允许enum的标签名和类名重名
pub enum SpriteEnum {
  Hero(Hero)
  Enemy(Enemy)
  Merchant(Merchant)
}

// 我们仍然需要实现Sprite中的getSpriteData
pub impl Sprite for Hero with getSpriteData(self) {
  self.sprite_data
}

pub impl Sprite for Enemy with getSpriteData(self) {
  self.sprite_data
}

pub impl Sprite for Merchant with getSpriteData(self) {
  self.sprite_data
}

// 为三个子类实现 asSpriteEnum 方法
// 这里实际上是将具体类型"装箱"到enum中
pub impl Sprite for Hero with asSpriteEnum(self) {
  Hero(self)  // 注意:这里的Hero是enum标签,不是类型
}

pub impl Sprite for Enemy with asSpriteEnum(self) {
  Enemy(self)
}

pub impl Sprite for Merchant with asSpriteEnum(self) {
  Merchant(self)
}

现在我们可以实现类型安全的who_are_you函数了:

test "who are you" {
  fn who_are_you(s: &Sprite) -> String {
    // 使用模式匹配进行类型分发
    match s.asSpriteEnum() {
      Hero(_) => "hero"
      Enemy(_) => "enemy"
      Merchant(_) => "merchant"
    }
  }

  let hero = Hero::new();
  let enemy = Enemy::new();
  let merchant = Merchant::new();
  inspect(who_are_you(hero), content="hero")
  inspect(who_are_you(enemy), content="enemy")
  inspect(who_are_you(merchant), content="merchant")
}

这种方法的美妙之处在于:它是编译时类型安全的!如果你添加了一个新的Sprite子类但忘记修改who_are_you函数,编译器会立即报错,而不是等到运行时才发现问题。

静态分发 vs 动态分发

你可能注意到函数签名中的&Sprite。这在Moonbit中被称为Trait Object,支持动态分发,类似于C++的虚函数机制。如果你写成fn[S: Sprite] who_are_you(s: S),那就是静态分发(泛型),编译器会为每种具体类型生成专门的代码。

两者的关键区别在于处理异构集合的能力。假设英雄有AOE技能需要攻击一个包含不同类型敌人的数组,你必须使用Array[&Sprite]而不是Array[V],因为后者无法同时容纳不同的具体类型。

当然,Moonbit也支持类似C++虚函数的直接方法调用:

pub trait SayName {
  say_name(Self) -> String
}

pub impl SayName for Hero with say_name(_) {
  "hero"
}

pub impl SayName for Enemy with say_name(_) {
  "enemy"
}

pub impl SayName for Merchant with say_name(_) {
  "merchant"
}

test "say_name" {
  fn who_are_you(s: &SayName) -> String {
    s.say_name()  // 直接调用trait方法,类似虚函数
  }

  let hero = Hero::new();
  let enemy = Enemy::new();
  let merchant = Merchant::new();
  inspect(who_are_you(hero), content="hero")
  inspect(who_are_you(enemy), content="enemy")
  inspect(who_are_you(merchant), content="merchant")
}

显式化的RTTI

实际上,Moonbit的ADT方法就是将C++隐式的RTTI过程显式化了。开发者明确知道有哪些类型,编译器也能在编译时进行完整性检查。

多层继承:构建复杂的能力体系

随着游戏系统的发展,我们发现HeroEnemy都有hp(生命值)、damage(攻击力)和attack方法。能否将这些共同特征抽象出来,形成一个Warrior(战士)层级呢?

C++的多层继承

在C++中,我们可以很自然地在继承链中插入新的中间层:

class Warrior : public Sprite {
protected: // 使用 protected,子类可以访问
    double hp;
    double damage;

public:
    Warrior(int id, double x, double y, double hp, double damage)
        : Sprite(id, x, y), hp(hp), damage(damage) {}

    virtual void attack(Sprite& target) = 0; // 战士都能攻击

    double getHP() const { return hp; }
    double getDamage() const { return damage; }
};

class Hero final : public Warrior {
    private:
        int money;
    public:
        Hero(int id, double x, double y, double hp, double damage, int money)
            : Warrior(id, x, y, hp, damage), money(money) {}
};

class Enemy final : public Warrior {
    public:
        Enemy(int id, double x, double y, double hp, double damage)
            : Warrior(id, x, y, hp, damage) {}
};

class Merchant final : public Sprite {
    public:
        Merchant(int id, double x, double y) : Sprite(id, x, y) {}
}; // 商人仍然直接继承 Sprite

这形成了一个清晰的继承链:Sprite → Warrior → Hero/EnemySprite → Merchant

Moonbit的组合式多层能力

在Moonbit中,我们继续坚持组合的思路,构建一个更灵活的能力体系:

pub struct WarriorData {
  hp: Double
  damage: Double
}

// Warrior trait 继承自 Sprite,形成能力层次
pub trait Warrior : Sprite {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target: &Warrior) -> Unit = _  // 默认实现
}

pub enum WarriorEnum {
  Hero(Hero)
  Enemy(Enemy)
}

// 重新定义Hero,现在它组合了两种数据
pub struct Hero {
  sprite_data: SpriteData    // 基础精灵数据
  warrior_data: WarriorData  // 战士数据
  money: Int                 // 英雄特有数据
}

// Hero 需要实现多个 trait
pub impl Sprite for Hero with getSpriteData(self) {
  self.sprite_data
}

pub impl Warrior for Hero with getWarriorData(self) {
  self.warrior_data
}

pub impl Warrior for Hero with asWarriorEnum(self) {
  Hero(self)
}

// 重新定义Enemy
pub struct Enemy {
  sprite_data: SpriteData
  warrior_data: WarriorData
}

pub impl Sprite for Enemy with getSpriteData(self) {
  self.sprite_data
}

pub impl Warrior for Enemy with getWarriorData(self) {
  self.warrior_data
}

pub impl Warrior for Enemy with asWarriorEnum(self) {
  Enemy(self)
}

有时我们也可能会遇到需要将父基类转换成子基类的场景。例如,我们的商人可能对不同的Sprite做出不同的反应:当他遇到一个Warrior时,他会说"Want to buy something?",当他遇到另一个商人时,则什么也不做。这个时候,我们就需要把Sprite父基类转换成Warrior子基类。推荐的方式是为Sprite trait添加一个tryAsWarrior的函数:

pub trait Sprite {
  // other methods
  tryAsWarrior(Self) -> &Warrior? = _  // 尝试转换为Warrior
}

impl Sprite with tryAsWarrior(self) {
  match self.asSpriteEnum() {
    // 第一项需要添加 as &Warrior, 来告知编译器整个表达式返回一个&Warrior
    // 如果不加这个as语句,编译器就会根据第一个表达式的类型
    // 判断整个表达式的类型为Hero,从而引发编译错误。
    Hero(h) => Some(h as &Warrior)
    Enemy(e) => Some(e)
    _ => None
  }
}

pub fn Merchant::ask(self: Merchant, s: &Sprite) -> String {
  match s.tryAsWarrior() {
    Some(_) => "Want to buy something?"  // 对战士说话
    None => ""                           // 对其他类型保持沉默
  }
}

这种设计的精妙之处在于它的极致灵活性

  • HeroEnemy通过组合SpriteDataWarriorData,同时实现SpriteWarrior两个trait,获得了所需的全部能力
  • Merchant只需要组合SpriteData并实现Sprite trait即可
  • 如果将来要引入Mage(法师)能力,只需定义MageDataMage trait
  • 一个角色甚至可以同时是WarriorMage,成为"魔剑士",而不需要处理C++中的菱形继承问题

菱形继承问题

假设我们要创建一个既是商人又是敌人的Profiteer(奸商)类。在C++中,如果Profiteer同时继承EnemyMerchant,就会出现菱形继承:Profiteer会拥有两份Sprite数据!这可能导致修改了一份数据,但调用时却使用了另一份的诡异bug。Moonbit的组合方式从根本上避免了这个问题。


传统面向对象编程的深层问题

看到这里,你可能会想:"Moonbit的方法需要写更多代码,看起来更复杂啊!"确实,从代码行数来看,Moonbit似乎需要更多的"模板代码"。但是,在真实的软件工程实践中,传统的面向对象编程方式实际上存在诸多深层问题:

1. 脆弱的继承链

问题:对父类的任何修改都会影响所有子类,可能产生难以预估的连锁反应。

想象一下你的RPG游戏已经发布了两年,拥有上百种不同的Sprite子类。现在你需要给基础的Sprite类做一个重构。然而,你可能很快就会发现这并不现实。在传统继承体系中,这个改动会影响到每一个子类,即便是很小的改动可能也影响巨大。某些子类可能因为这个改动出现意外的行为变化,而你需要逐一检查和测试所有相关代码。

Moonbit的解决方案:组合式设计让我们可以通过ADT直接找到Sprite的所有子类,立刻知道重构代码的影响范围。

2. 菱形继承的噩梦

问题:多重继承容易导致菱形继承,产生数据重复和方法调用歧义。

如前所述,Profiteer类同时继承EnemyMerchant时,会拥有两份Sprite数据。这不仅浪费内存,更可能导致数据不一致的bug。

Moonbit的解决方案:组合天然避免了这个问题,Profiteer可以拥有SpriteDataWarriorDataMerchantData,清晰明了。

3. 运行时错误的隐患

问题:传统OOP的许多问题只能在运行时被发现,增加了调试难度和项目风险。

还记得前面dynamic_cast的例子吗?如果你添加了新的子类但忘记更新相关的类型判断代码,只有在程序运行到那个分支时才会暴露问题。在大型项目中,这可能意味着bug在生产环境中才被发现。

Moonbit的解决方案:ADT配合模式匹配提供编译时类型安全。遗漏任何一个case,编译器都会报错。

4. 复杂度爆炸

问题:深层继承树变得难以理解和维护。

经过几年的开发,你的游戏可能演化出这样的继承树:

Sprite
├── Warrior
│   ├── Hero
│   │   ├── Paladin
│   │   ├── Berserker
│   │   └── ...
│   └── Enemy
│       ├── Orc
│       ├── Dragon
│       └── ...
├── Mage
│   ├── Wizard
│   └── Sorceror
└── NPC
    ├── Merchant
    ├── QuestGiver
    └── ...

当需要重构时,你可能需要花费大量时间来理解这个复杂的继承关系,而且任何改动都可能产生意想不到的副作用。

Moonbit的解决方案:扁平化的组合结构让系统更容易理解。每个能力都是独立的trait,组合关系一目了然。

结语

通过这次深入的比较,我们看到了两种截然不同的面向对象编程哲学:

  • C++的传统OOP:基于继承的"is-a"关系,直观但可能陷入复杂度陷阱
  • Moonbit的现代OOP:基于组合的"has-a"关系,初学稍复杂但长期更优雅

Moonbit的方法虽然需要编写更多的"模板代码",但这些额外的代码换来的是:

  • 更好的类型安全:编译时捕获更多错误
  • 更清晰的架构:组合关系比继承关系更容易理解
  • 更容易的维护:修改影响范围更可控
  • 更少的运行时错误:ADT和模式匹配提供完整性保证

尽管我们必须承认,对于小型项目或特定场景,传统继承依然有其价值。但现实情况是,随着软件系统复杂度的增长,Moonbit这种组合优于继承的设计哲学确实展现出了更强的适应性和可维护性。

希望这篇文章能为你的Moonbit编程之旅提供有价值的指导,让你在构建复杂系统时能够充分利用Moonbit的设计优势。


完整版代码

pub struct SpriteData {
  
Int
id
:
Int
Int
mut
Double
x
:
Double
Double
mut
Double
y
:
Double
Double
} pub fn
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
::
(id : Int, x : Double, y : Double) -> SpriteData
new
(
Int
id
:
Int
Int
,
Double
x
:
Double
Double
,
Double
y
:
Double
Double
) ->
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
{
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
::{
Int
id
,
Double
x
,
Double
y
}
} // 2. 定义描述通用行为的 Trait pub trait
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
{
(Self) -> SpriteData
getSpriteData
(

type parameter Self

Self
) ->
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
(Self) -> SpriteEnum
asSpriteEnum
(

type parameter Self

Self
) ->
enum SpriteEnum {
  Hero(Hero)
  Enemy(Enemy)
  Merchant(Merchant)
}
SpriteEnum
(Self) -> &Warrior?
tryAsWarrior
(

type parameter Self

Self
) -> &
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
? = _
(Self) -> Int
getID
(

type parameter Self

Self
) ->
Int
Int
= _
(Self) -> Double
getX
(

type parameter Self

Self
) ->
Double
Double
= _
(Self) -> Double
getY
(

type parameter Self

Self
) ->
Double
Double
= _
(Self, Double) -> Unit
setX
(

type parameter Self

Self
,
Double
Double
) ->
Unit
Unit
= _
(Self, Double) -> Unit
setY
(

type parameter Self

Self
,
Double
Double
) ->
Unit
Unit
= _
} // Sprite的默认实现 // 只要实现了 getSpriteData,就自动拥有了其他方法 impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self) -> Int
getID
(
Self
self
) {
Self
self
.
(Self) -> SpriteData
getSpriteData
().
Int
id
} impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self) -> Double
getX
(
Self
self
) {
Self
self
.
(Self) -> SpriteData
getSpriteData
().
Double
x
} impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self) -> Double
getY
(
Self
self
) {
Self
self
.
(Self) -> SpriteData
getSpriteData
().
Double
y
} impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self, new_x : Double) -> Unit
setX
(
Self
self
,
Double
new_x
) {
Self
self
.
(Self) -> SpriteData
getSpriteData
().
Double
x
=
Double
new_x
} impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self, new_y : Double) -> Unit
setY
(
Self
self
,
Double
new_y
) {
Self
self
.
(Self) -> SpriteData
getSpriteData
().
Double
y
=
Double
new_y
} impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
with
(self : Self) -> &Warrior?
tryAsWarrior
(
Self
self
) {
match
Self
self
.
(Self) -> SpriteEnum
asSpriteEnum
() {
(Hero) -> SpriteEnum
Hero
(
Hero
h
) =>
(&Warrior) -> &Warrior?
Some
(
Hero
h
as
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
&Warrior
)
(Enemy) -> SpriteEnum
Enemy
(
Enemy
e
) =>
(&Warrior) -> &Warrior?
Some
(
Enemy
e
)
_ =>
&Warrior?
None
} } pub enum SpriteEnum {
(Hero) -> SpriteEnum
Hero
(
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
)
(Enemy) -> SpriteEnum
Enemy
(
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
)
(Merchant) -> SpriteEnum
Merchant
(
struct Merchant {
  sprite_data: SpriteData
}
Merchant
)
} pub struct WarriorData {
Double
hp
:
Double
Double
Double
damage
:
Double
Double
} pub trait
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
:
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
{ // Warrior 继承自 Sprite
(Self) -> WarriorData
getWarriorData
(

type parameter Self

Self
) ->
struct WarriorData {
  hp: Double
  damage: Double
}
WarriorData
(Self) -> WarriorEnum
asWarriorEnum
(

type parameter Self

Self
) ->
enum WarriorEnum {
  Hero(Hero)
  Enemy(Enemy)
}
WarriorEnum
(Self, &Warrior) -> Unit
attack
(

type parameter Self

Self
, target: &
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
) ->
Unit
Unit
= _
} impl
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
with
(self : Self, target : &Warrior) -> Unit
attack
(
Self
self
,
&Warrior
target
) {
(t : (Self, &Warrior)) -> Unit

Evaluates an expression and discards its result. This is useful when you want to execute an expression for its side effects but don't care about its return value, or when you want to explicitly indicate that a value is intentionally unused.

Parameters:

  • value : The value to be ignored. Can be of any type.

Example:

  let x = 42
  ignore(x) // Explicitly ignore the value
  let mut sum = 0
  ignore([1, 2, 3].iter().each(fn(x) { sum = sum + x })) // Ignore the Unit return value of each()
ignore
((
Self
self
,
&Warrior
target
))
// ... } pub enum WarriorEnum {
(Hero) -> WarriorEnum
Hero
(
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
)
(Enemy) -> WarriorEnum
Enemy
(
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
)
} // 定义Hero pub struct Hero { sprite_data: SpriteData warrior_data: WarriorData money: Int } pub fn
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::
() -> Hero
new
(
) ->
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
{
let
SpriteData
sprite_data
=
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
::
(id : Int, x : Double, y : Double) -> SpriteData
new
(0, 42, 33)
let
WarriorData
warrior_data
=
struct WarriorData {
  hp: Double
  damage: Double
}
WarriorData
::{
Double
hp
: 100,
Double
damage
: 20 }
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::{
SpriteData
sprite_data
,
WarriorData
warrior_data
,
Int
money
: 1000}
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(self : Hero) -> SpriteData
getSpriteData
(
Hero
self
) {
Hero
self
.
SpriteData
sprite_data
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(self : Hero) -> SpriteEnum
asSpriteEnum
(
Hero
self
) {
(Hero) -> SpriteEnum
Hero
(
Hero
self
)
} pub impl
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(self : Hero) -> WarriorData
getWarriorData
(
Hero
self
) {
Hero
self
.
WarriorData
warrior_data
} pub impl
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(self : Hero) -> WarriorEnum
asWarriorEnum
(
Hero
self
) {
WarriorEnum::
(Hero) -> WarriorEnum
Hero
(
Hero
self
)
} // 定义Enemy pub struct Enemy { sprite_data: SpriteData warrior_data: WarriorData } pub fn
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::
() -> Enemy
new
() ->
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
{
let
SpriteData
sprite_data
=
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
::
(id : Int, x : Double, y : Double) -> SpriteData
new
(0, 42, 33)
let
WarriorData
warrior_data
=
struct WarriorData {
  hp: Double
  damage: Double
}
WarriorData
::{
Double
hp
: 100,
Double
damage
: 5}
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::{
SpriteData
sprite_data
,
WarriorData
warrior_data
}
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(self : Enemy) -> SpriteData
getSpriteData
(
Enemy
self
) {
Enemy
self
.
SpriteData
sprite_data
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(self : Enemy) -> SpriteEnum
asSpriteEnum
(
Enemy
self
) {
(Enemy) -> SpriteEnum
Enemy
(
Enemy
self
)
} pub impl
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(self : Enemy) -> WarriorData
getWarriorData
(
Enemy
self
) {
Enemy
self
.
WarriorData
warrior_data
} pub impl
trait Warrior {
  getWarriorData(Self) -> WarriorData
  asWarriorEnum(Self) -> WarriorEnum
  attack(Self, target : &Warrior) -> Unit
}
Warrior
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(self : Enemy) -> WarriorEnum
asWarriorEnum
(
Enemy
self
) {
WarriorEnum::
(Enemy) -> WarriorEnum
Enemy
(
Enemy
self
)
} // 定义Merchant pub struct Merchant { sprite_data: SpriteData } pub fn
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::
() -> Merchant
new
() ->
struct Merchant {
  sprite_data: SpriteData
}
Merchant
{
let
SpriteData
sprite_data
=
struct SpriteData {
  id: Int
  mut x: Double
  mut y: Double
}
SpriteData
::
(id : Int, x : Double, y : Double) -> SpriteData
new
(0, 42, 33)
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::{
SpriteData
sprite_data
}
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Merchant {
  sprite_data: SpriteData
}
Merchant
with
(self : Merchant) -> SpriteData
getSpriteData
(
Merchant
self
) {
Merchant
self
.
SpriteData
sprite_data
} pub impl
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
for
struct Merchant {
  sprite_data: SpriteData
}
Merchant
with
(self : Merchant) -> SpriteEnum
asSpriteEnum
(
Merchant
self
) {
(Merchant) -> SpriteEnum
Merchant
(
Merchant
self
)
} pub fn
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::
(self : Merchant, s : &Sprite) -> String
ask
(
Merchant
self
:
struct Merchant {
  sprite_data: SpriteData
}
Merchant
,
&Sprite
s
: &
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
) ->
String
String
{
(t : Merchant) -> Unit

Evaluates an expression and discards its result. This is useful when you want to execute an expression for its side effects but don't care about its return value, or when you want to explicitly indicate that a value is intentionally unused.

Parameters:

  • value : The value to be ignored. Can be of any type.

Example:

  let x = 42
  ignore(x) // Explicitly ignore the value
  let mut sum = 0
  ignore([1, 2, 3].iter().each(fn(x) { sum = sum + x })) // Ignore the Unit return value of each()
ignore
(
Merchant
self
)
match
&Sprite
s
.
(&Sprite) -> &Warrior?
tryAsWarrior
() {
&Warrior?
Some
(_) =>"what to buy something?"
&Warrior?
None
=> ""
} } test "who are you" { fn
(&Sprite) -> String
who_are_you
(
&Sprite
s
: &
trait Sprite {
  getSpriteData(Self) -> SpriteData
  asSpriteEnum(Self) -> SpriteEnum
  tryAsWarrior(Self) -> &Warrior?
  getID(Self) -> Int
  getX(Self) -> Double
  getY(Self) -> Double
  setX(Self, Double) -> Unit
  setY(Self, Double) -> Unit
}
Sprite
) ->
String
String
{
match
&Sprite
s
.
(&Sprite) -> SpriteEnum
asSpriteEnum
() {
SpriteEnum
Hero
(_) => "hero"
SpriteEnum
Enemy
(_) => "enemy"
SpriteEnum
Merchant
(_) => "merchant"
} } let
Hero
hero
=
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::
() -> Hero
new
();
let
Enemy
enemy
=
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::
() -> Enemy
new
();
let
Merchant
merchant
=
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::
() -> Merchant
new
();
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&Sprite) -> String
who_are_you
(
Hero
hero
),
String
content
="hero")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&Sprite) -> String
who_are_you
(
Enemy
enemy
),
String
content
="enemy")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&Sprite) -> String
who_are_you
(
Merchant
merchant
),
String
content
="merchant")
} pub trait
trait SayName {
  say_name(Self) -> String
}
SayName
{
(Self) -> String
say_name
(

type parameter Self

Self
) ->
String
String
} pub impl
trait SayName {
  say_name(Self) -> String
}
SayName
for
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
with
(Hero) -> String
say_name
(_) {
"hero" } pub impl
trait SayName {
  say_name(Self) -> String
}
SayName
for
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
with
(Enemy) -> String
say_name
(_) {
"enemy" } pub impl
trait SayName {
  say_name(Self) -> String
}
SayName
for
struct Merchant {
  sprite_data: SpriteData
}
Merchant
with
(Merchant) -> String
say_name
(_) {
"merchant" } test "say_name" { fn
(&SayName) -> String
who_are_you
(
&SayName
s
: &
trait SayName {
  say_name(Self) -> String
}
SayName
) ->
String
String
{
&SayName
s
.
(&SayName) -> String
say_name
()
} let
Hero
hero
=
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::
() -> Hero
new
();
let
Enemy
enemy
=
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::
() -> Enemy
new
();
let
Merchant
merchant
=
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::
() -> Merchant
new
();
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&SayName) -> String
who_are_you
(
Hero
hero
),
String
content
="hero")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&SayName) -> String
who_are_you
(
Enemy
enemy
),
String
content
="enemy")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
(&SayName) -> String
who_are_you
(
Merchant
merchant
),
String
content
="merchant")
} test "merchant ask" { let
Hero
hero
=
struct Hero {
  sprite_data: SpriteData
  hp: Double
  damage: Int
  money: Int
}
Hero
::
() -> Hero
new
();
let
Enemy
enemy
=
struct Enemy {
  sprite_data: SpriteData
  hp: Double
  damage: Int
}
Enemy
::
() -> Enemy
new
();
let
Merchant
merchant
=
struct Merchant {
  sprite_data: SpriteData
}
Merchant
::
() -> Merchant
new
();
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Merchant
merchant
.
(self : Merchant, s : &Sprite) -> String
ask
(
Hero
hero
),
String
content
="what to buy something?")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Merchant
merchant
.
(self : Merchant, s : &Sprite) -> String
ask
(
Enemy
enemy
),
String
content
="what to buy something?")
(obj : &Show, content~ : String, loc~ : SourceLoc = _, args_loc~ : ArgsLoc = _) -> Unit raise InspectError

Tests if the string representation of an object matches the expected content. Used primarily in test cases to verify the correctness of Show implementations and program outputs.

Parameters:

  • object : The object to be inspected. Must implement the Show trait.
  • content : The expected string representation of the object. Defaults to an empty string.
  • location : Source code location information for error reporting. Automatically provided by the compiler.
  • arguments_location : Location information for function arguments in source code. Automatically provided by the compiler.

Throws an InspectError if the actual string representation of the object does not match the expected content. The error message includes detailed information about the mismatch, including source location and both expected and actual values.

Example:

  inspect(42, content="42")
  inspect("hello", content="hello")
  inspect([1, 2, 3], content="[1, 2, 3]")
inspect
(
Merchant
merchant
.
(self : Merchant, s : &Sprite) -> String
ask
(
Merchant
merchant
),
String
content
="")
}