Perl 6 程式設計教學:繼承 (Inheritance)

PUBLISHED ON DEC 24, 2017 — PROGRAMMING
FacebookTwitter LinkedIn LINE Skype EverNote GMail Email Email

    除了組合以外,繼承 (inheritance) 也是重覆利用程式碼的一種方法。透過繼承,達到子類型 (subtyping) 的功能,也是實作多型 (polymorphism) 的一種手段;因此,繼承有時會被過度濫用。Go 和 Rust 不約而同拿掉繼承,就是對這個現象的一個反思。Perl 6 仍然保留繼承的特性,也是本文所要介紹的主題。

    單一繼承

    Perl 6 使用 is 表達繼承關係,如下例:

    class Employee {
        has Numeric $!_salary;
        
        submethod BUILD(:salary($s)) {
            self.salary($s);
        }
        
        multi method salary {
            $!_salary;
        }
        
        multi method salary($s) {
            if $s <= 0.0 {
                die "Invalid salary";
            }
            
            $!_salary = $s;
        }
    }
    
    class Programmer is Employee {
        has Str @!_langs;
        has Str @!_editors;
        
        submethod BUILD(:salary($s), :@langs, :@editors) {
            self.salary($s);
            self.langs(@langs);
            self.editors(@editors);
        }
        
        multi method langs {
            @!_langs;
        }
        
        multi method langs(@l) {
            @!_langs = @l;
        }
        
        multi method editors {
            @!_editors;
        }
        
        multi method editors(@e) {
            @!_editors = @e;
        }
        
        method solve(Str $problem) {
            my $lang = @!_langs.roll;
            my $editor = @!_editors.roll;
            
            "The programmer solved {$problem} in {$lang} with {$editor}".say;
        }
    }
    
    my $p = Programmer.new(
            salary => 100.0,
            langs => ("Perl", "Python", "Ruby", "Java", "Go"),
            editors => ("Vim", "Emacs", "Atom", "Visual Studio Code"),
        );
    
    $p.salary == 100.0 or die "Wrong salary";
    $p.solve("Sorting");
    $p.solve("Tower of Hanoi");
    

    在這個例子中,Programmer 繼承 Employee 的程式碼,我們不需要重新撰寫有關 salary 部分的代碼。

    多重繼承

    Perl 6 可以繼承多個物件,理論上,可以寫出類似以下代碼:

    class GeometricRetangle is Retangle is Point {
        # Implement it here.
    }
    

    但是,在 Perl 6 使用多重繼承不是好主意。因為在碰到方法衝突 (不同物件但同名稱的方法) 時,Perl 是依照特定的演算法自動處理,卻無法手動調整;而且,編譯器不會對此發出警告。因此,建議避開這項機制。

    註:此演算法是 C3 linearization,有興趣的讀者可自行查閱相關資料。

    以下節錄來自 Perl 6 官網的一個反例:

    class Bull {
        has Bool $.castrated = False;
        method steer {
            # Turn your bull into a steer 
            $!castrated = True;
            return self;
        }
    }
    class Automobile {
        has $.direction;
        method steer($!direction) { }
    }
    class Taurus is Bull is Automobile { }
     
    my $t = Taurus.new;
    $t.steer;
    

    在這個例子裡,編譯器不會發出警告,但程式運行可能不如預期。

    通常會誤用多重繼承,是對物件導向的思維有些誤解,或是想要達到多型的效果。Perl 6 提供另外一個更好的機制,就是我們下文要介紹的 role。

    Role

    Role 目前沒有直接對應的中文翻譯,在別的程式語言中,接近介面 (interface) 或是 mixin。Role 的作用在於提供一組公開方法,藉此約束類別的行為。

    要繼承 roles,使用 does。以下例子使用沒有實作程式碼的 role:

    role Speak {
        method speak { ... }
    }
    
    class Duck does Speak {
        method speak {
            "Pack pack".say;
        }
    }
    
    class Dog does Speak {
        method speak {
            "Wow wow".say;
        }
    }
    
    class Tiger does Speak {
        method speak {
            "Halum halum".say;
        }
    }
    
    my Speak @animals = (
            Duck.new,
            Dog.new,
            Tiger.new,
        );
        
    for @animals -> $a {
        $a.speak;
    }
    

    在我們這個例子中,我們刻意在 role Speak 不提供實作,若繼承 Speak 的類別沒有自行實作相對應的 speak 方法,會引發錯誤。透過這種方法來約束類別。

    Roles 在碰到方法衝突時,編譯器會引發錯誤,這裡節錄 Perl 6 官網的例子如下:

    role Bull-Like {
        has Bool $.castrated = False;
        method steer {
            # Turn your bull into a steer 
            $!castrated = True;
            return self;
        }
    }
    role Steerable {
        has Real $.direction;
        method steer(Real $d = 0) {
            $!direction += $d;
        }
    }
    class Taurus does Bull-Like does Steerable { }
    

    這樣的程式會引發以下錯誤:

    ===SORRY!===
    Method 'steer' must be resolved by class Taurus because it exists in
    multiple roles (Steerable, Bull-Like)
    

    可能的解決方法如下:

    class Taurus does Bull-Like does Steerable {
        method steer($direction?) {
            self.Steerable::steer($direction?)
        }
    }
    

    對照 roles 和繼承,筆者認為,Perl 6 的多重繼承,某種程度上是設計的失誤,應避免使用。

    Role 也可以加入一些實作內容,以下節錄 Perl 6 的官網:

    use MONKEY-SEE-NO-EVAL;
    role Serializable {
        method serialize() {
            self.perl; # very primitive serialization
        }
        method deserialize($buf) {
            EVAL $buf; # reverse operation to .perl
        }
    }
    
    class Point does Serializable {
        has $.x;
        has $.y;
    }
    my $p = Point.new(:x(1), :y(2));
    my $serialized = $p.serialize;      # method provided by the role
    my $clone-of-p = Point.deserialize($serialized);
    say $clone-of-p.x;
    

    EVAL 本身僅能解析字串,對於變數或其他複雜結構則無法處理;第一行的 MONKEY-SEE-NO-EVAL 可以解除此限制。

    如果類別,roles 之間也可以繼承,如以下例子:

    role R1 {
        # methods here 
    }
    role R2 does R1 {
        # methods here 
    }
    class C does R2 { }