The Awesome Errors of Perl 6
如果你一直在讀技術(shù)相關(guān)的東西,你現(xiàn)在可能知道 Rust 里面的令人驚喜的錯誤報告能力净赴。 既然 Perl 6 也因它的絕妙的錯誤處理而聞名, mst 查詢了一些例子來炫耀 rust 的錯誤處理能力, 但是不幸的是我沒有發(fā)現(xiàn)玖翅。
我盡力避免錯誤并且我很少完整地讀完它烧栋。所以我會搜尋一些很酷的關(guān)于令人驚嘆的錯誤方面的例子并寫出來审姓。雖然我能夠用頭撞擊鍵盤并把輸出粘貼出來, 但是那將會慘不忍讀, 所以我會談?wù)撘恍Τ鯇W(xué)者來說沒那么明顯的棘手的錯誤, 還有怎樣修復(fù)那些錯誤魔吐。
讓我們開始用頭部猛擊吧!
基礎(chǔ)
下面是一段有錯誤的代碼;
say "Hello world!;
say "Local time is {DateTime.now}";
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Two terms in a row (runaway multi-line "" quote starting at line 1 maybe?)
# at /home/zoffix/test.p6:2
# ------> say "Local time is {DateTime.now}";
# expecting any of:
# infix
# infix stopper
# postfix
# statement end
# statement modifier
# statement modifier loop
第一行丟失了字符串上的閉合引號, 所以直到第二行的開括號之間的所有東西都會被認為是字符串的一部分嗜桌。一旦推測的閉合引號被找到, Perl 6 就看到單詞 "Local", 這個單詞被定義為一個項(item)骨宠。因為在 Perl 6 中一行中同時存在兩個項(item)是不允許的, 所以編譯器拋出了錯誤, 并對它所期望的提供了一些建議, 并且它探測到了我們正處在一個字符串中, 并且建議我們檢測, 我們忘記在行 1 中閉合引號了层亿。
===SORRY!=== 部分并不是意味著你運行的是加拿大版本的編譯器, 而是意味著該錯誤是一個編譯時錯誤(和運行時相比)立美。
Nom-nom-nom-nom
下面有一個有趣的錯誤建蹄。我們有一個返回東西的子例程, 所以我們調(diào)用了它并使用了 for 循環(huán)來迭代值:
sub things {1 ... ∞}
for things {
say "Current stuff is $_";
}
# ===SORRY!===
# Function 'things' needs parens to avoid gobbling block
# at /home/zoffix/test.p6:5
# ------> }<EOL>
# Missing block (apparently claimed by 'things')
# at /home/zoffix/test.p6:5
# ------> }<EOL>
Perl 6 允許你在調(diào)用子例程的時候省略圓括號洞慎。上面的錯誤提到了全局塊兒(globbing blocks)。實際發(fā)生的是我們希望傳給 for 循環(huán)的塊兒被作為參數(shù)傳遞給了子例程桦他。輸出中的第二個錯誤證實 for 循環(huán)丟失了它的塊兒(并且給出了一個建議, 它被我們的 things 子例程接收了)快压。
第一個錯誤告訴我們怎樣修復(fù)那個問題: Function 'things' needs parens, 所以我們的循環(huán)需要是:
for things() {
say "Current stuff is $_";
}
然而, 如果我們的子例程真的期望傳遞一個塊兒, 那么圓括號就不是必須的蔫劣。兩個代碼塊肩并肩地在一塊兒會導(dǎo)致 "two terms in a row" 錯誤, 所以 Perl 6 知道把第一個 block 傳遞給子例程并使用第二個 block 作為 for 循環(huán)的主體:
sub things (&code) { code }
for things { 1 ... ∞ } {
say "Current stuff is $_";
}
Did You Mean Levestein?
下面有一個很酷的特性, 它不僅告訴你出錯了, 還能指出你可能想要的:
sub levenshtein {}
levestein;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Undeclared routine:
# levestein used at line 2. Did you mean 'levenshtein'?
當(dāng) Perl 6 遇到它不認識的名字時它會為它知道的東西計算Levenshtein distance以嘗試提供一個有用的建議脉幢。在上面的距離中它遇到了一個它不知道的子例程調(diào)用嫌松。它注意到我們確實有一個相似的子例程, 所以它把它作為備選提供了出來。不要再盯著屏幕了, 嘗試找到你在哪里敲擊的鍵盤液走!
然而, 這個特性不可能在觸發(fā)時面面俱到缘眶。假設(shè)我們把子例程的名字變?yōu)榇髮懙?Levenshtein, 我們就不會得到那個建議, 因為對于以大寫字母開頭的東西, 編譯器認為它看起來像一個類型名而非子例程名, 所以它檢測這些東西來代替:
class Levenshtein {}
Lvnshtein.new;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Undeclared name:
# Lvnshtein used at line 2. Did you mean 'Levenshtein'?
一旦你成了 Seq, 你再也變不回來
我們假設(shè)你生成了一個短的斐波納契數(shù)字序列巷懈。你打印了它然后你想再打印它一次, 但是這一次打印每個成員的平方顶燕。發(fā)生了什么?
my $seq = (1, 1, * + * ... * > 100);
$seq.join(', ').say;
$seq.map({ $_2 }).join(', ').say;
# 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144
# This Seq has already been iterated, and its values consumed
# (you might solve this by adding .cache on usages of the Seq, or
# by assigning the Seq into an array)
# in block <unit> at test.p6 line 3
嗷, 運行時錯誤涌攻。我們從序列操作符得到的 Seq 類型不能保留東西。當(dāng)你迭代序列的時候, 每次它給你一個值之后就丟棄這個值, 所以一旦你迭代完整個 Seq 序列, 就結(jié)束了维咸。
上面的例子中, 我們嘗試再次迭代那個序列, 所以 Rakudo 運行時奔潰并抱怨了, 因為它做不了虽画。錯誤消息的確提供了兩種可能的解決方案。我們要么使用 .cache 方法來獲得一個我們將要迭代的 List:
my $seq = (1, 1, * + * ... * > 100).cache;
$seq .join(', ').say;
$seq.map({ $_2 }).join(', ').say;
# 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144
# 1, 1, 4, 9, 25, 64, 169, 441, 1156, 3025, 7921, 20736
或者我們可以從一開始就使用數(shù)組 Array:
my @seq = 1, 1, * + * … * > 100;
@seq .join(', ').say;
@seq.map({ $_2 }).join(', ').say;
# 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144
# 1, 1, 4, 9, 25, 64, 169, 441, 1156, 3025, 7921, 20736
并且即使我們把序列 Seq 存儲進了 Array 中, 它不會被具體化直到真正被需要時:
my @a = 1 … ∞;
say @a[^10];
# OUTPUT:
# (1 2 3 4 5 6 7 8 9 10)
These Aren't The Attributes You're Looking For
假設(shè)你有一個類。在類里面, 你有一些私有屬性并且你有一個使用屬性的值作為它的一部分的正則匹配方法:
class {
has $!prefix = 'foo';
method has-prefix ($text) {
so $text ~~ /^ $!prefix/;
}
}.new.has-prefix('foobar').say;
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Attribute $!prefix not available inside of a regex, since regexes are methods on Cursor.
# Consider storing the attribute in a lexical, and using that in the regex.
# at /home/zoffix/test.p6:4
# ------> so $text ~~ /^ $!prefix?/;
# expecting any of:
# infix stopper
糟糕! 發(fā)生什么了?
就像編譯器所指出的那樣, Perl 6 實際上是由幾種語言編織而成: Perl 6, Quote, 和 Regex 語言是這個編織物的一部分。這就是為什么像下面這樣的東西就能起效:
say "foo { "bar" ~ "meow" } ber ";
# OUTPUT:
# foo barmeow ber
盡管內(nèi)插的代碼塊中使用了同樣的引號", 但是沒有發(fā)生沖突糟港。然而, 同樣的機制在正則表達式中有限制, 因為在正則表達式中, 所查詢的屬性屬于 Cursor 對象, 它負責(zé)這個正則表達式秸抚。
為了避免這個錯誤, 就像錯誤信息暗示的那樣, 僅僅使用一個臨時的變量來存儲 $!prefix 好了, 或者使用 given 塊兒:
class {
has $!prefix = 'foo';
method has-prefix ($text) {
given $!prefix { so $text ~~ /^ $_/ }
}
}.new.has-prefix('foobar').say;
De-Ranged
嘗試過訪問列表中超出范圍的元素嗎?
my @a = <foo bar ber>;
say @a[*-42];
# Effective index out of range. Is: -39, should be in 0..Inf
# in block <unit> at test.p6 line 2
在 Perl 6 中, 如果從列表末端索引一個條目, 要使用時髦的語法: [*-42]
。它實際上是一個接收一個參數(shù)(它是列表中元素的個數(shù))的閉包, 然后減去 42, 然后返回的值作為實際的索引颠放。如果你特別無聊, 你可以使用 @a[sub ($total) { $total - 42 }]
代替碰凶。
在上面的錯誤中, 那個索引以 3 - 42
結(jié)束, 或者說是 -39
, 這是我們在錯誤信息中看到的那個值省有。因為索引不能是負的, 所以我們收到了錯誤, 這也告訴我們索引必須從 0 到 正無窮大(任何超過列表所包含的索引會在被查詢時返回 Any)伸头。
A Rose By Any Other Name, Would Code As Sweet
如果你是 Perl 6 的姐妹語言, Perl 5 的活躍使用者, 你可能會發(fā)現(xiàn)有時候你在 Perl 6 代碼中寫出 Perl 5 風(fēng)格的代碼:
say "foo" . "bar";
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Unsupported use of . to concatenate strings; in Perl 6 please use ~
# at /home/zoffix/test.p6:1
# ------> say "foo" .? "bar";
在上面, 我們嘗試使用 Perl 5 的字符串連接操作符來連接兩個字符串舷蟀。這個錯誤機制足夠聰明地檢測到這樣的用法并推薦了正確的 ~
操作符來代替。
這不是這種探測的唯一使用場景扫步。有很多場景河胎。這兒有另外一個例子, 用于探測 Perl 5 的鉆石操作符的意外使用, 伴隨著幾個程序員可能想要的建議:
while <> {}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Unsupported use of <>; in Perl 6 please use lines() to read input, ('') to
# represent a null string or () to represent an empty list
# at /home/zoffix/test.p6:1
# ------> while <?> {}
Heredoc, Theredoc, Everywheredoc
為了拋出問題, 請先閱讀底部的錯誤, 假裝就是你自己寫的程序代碼:
my $stuff = qq:to/END/;
Blah blah blah
END;
for ^10 {
say 'things';
}
for ^20 {
say 'moar things';
}
sub foo ($wtf) {
say 'oh my!';
}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# Variable '$wtf' is not declared
# at /home/zoffix/test.p6:13
# ------> sub foo (?$wtf) {
哈? 編譯器哭著說有一個未聲明的變量, 但是它指向的卻是子例程中的簽名游岳。當(dāng)然它不會被聲明胚迫。
那些沒有發(fā)現(xiàn)問題的人: 問題就出在 heredoc 中的閉合 END 后面的分號上访锻。 heredoc 在閉合分隔符單獨出現(xiàn)在一行的地方結(jié)束期犬。在編譯器看來, 我們還沒有在 END;
這兒看到分隔符, 所以它繼續(xù)解析就像它仍舊在解析 heredoc 一樣哭懈。qq
heredoc 能讓你插入變量, 所以當(dāng)解析器解析到簽名中的 $wtf
變量時, 解析器并不知道它是一段實際代碼中的簽名還是某些隨機的文本, 所以編譯器哭著說變量未找到遣总。
Won't Someone Think of The Reader?
下面有一個極好的錯誤能阻止你寫出恐怖的代碼:
my $a;
sub {
$a.say;
$^a.say;
}
# ===SORRY!=== Error while compiling /home/zoffix/test.p6
# $a has already been used as a non-placeholder in the surrounding sub block,
# so you will confuse the reader if you suddenly declare $^a here
# at /home/zoffix/test.p6:4
# ------> $^a.say;
這里有一點背景: 你可以在變量身上使用 $^ twigil
來創(chuàng)建一個隱式的簽名旭斥。為了能在嵌套的塊中使用這樣的變量, 這個語法實際上創(chuàng)建了不帶 twigil 的相同變量, 所以 $^a
和 $a
是同一個東西, 并且上面的子例程的簽名是 ($a)
垂券。
在我們的代碼中, 我們還在外部作用域中有個 $a
并且推測我們首先打印出它, 在使用 $^
twigil 在同樣一個作用域中創(chuàng)建另外一個 $a
之前, 但是這個子例程包含了參數(shù)... 真繞腦! 為了避免這樣, 就把你的變量重命名為某個不會沖突的東西好了菇爪。改成泰文怎么樣?
my $??????? = 'peace';
sub {
$???????.say;
$^???????????????.say;
}('to your variables');
# OUTPUT:
# peace
# to your variables
Well, Colour Me Errpressed!
如果你的終端支持它, 那么編譯器就會發(fā)出 ANSI 代碼來給輸出著點色:
for ^5 {
say meow";
}
那很好也很顯眼奪目, 但是假設(shè)你想把從編譯器中捕獲的輸出顯示到任何地方, 你會原樣地得到 ANSI 代碼, 就像 31m===[0mSORRY![31m===[0m
熙揍。
這很可怕, 但是幸運的是, 禁用顏色很簡單: 僅僅把 RAKUDO_ERROR_COLOR
這個環(huán)境變量的值設(shè)置為 0 就好了:
你也可以在程序中設(shè)置它届囚。你不得不足夠早地設(shè)置它, 所以在任何地方把它放置在程序的開頭并使用 BEGIN phaser 來設(shè)置它只要賦值被編譯完成:
BEGIN %*ENV<RAKUDO_ERROR_COLOR> = 0;
for ^5 {
say meow";
}
An Exceptional Failure
Perl 6 有一個特殊的異常 -- Failure -- 直到你把它用作變量它才會被激發(fā), 并且你甚至可以通過在布爾上下文中使用它來徹底地消除它意系。你可以通過調(diào)用 fail 子例程產(chǎn)生你自己的 Failures 并且 Perl 6 在核心中在盡可能合適的時候使用它蛔添。
這兒有一段代碼, 其中我們定義了一個前綴操作符用來計算對象的圓周長, 給定一個半徑迎瞧。如果半徑是負值, 它就調(diào)用 fail, 并返回一個 Failure 對象:
sub prefix:<?> (\??) {
?? < 0 and fail 'Your object warps the Universe a new one';
τ × ??;
}
say 'Calculating the circumference of the mystery object';
my $c? = ? ???;
say 'Calculating the circumference of the Earth';
my $c? = ? 6.3781 × 10?;
say 'Calculating the circumference of the Sun';
my $c? = ? 6.957 × 10?;
say "The circumference of the largest object is {max $c?, $c?, $c?} metres";
# OUTPUT:
# Calculating the circumference of the mystery object
# Calculating the circumference of the Earth
# Calculating the circumference of the Sun
# Your object warps the Universe a new one
# in sub prefix:<?> at test.p6 line 2
# in block <unit> at test.p6 line 7
#
# Actually thrown at:
# in block <unit> at test.p6 line 15
在第七行中我們正計算一個半徑為負值的圓的周長, 所以如果它僅僅是一個常規(guī)的異常, 那么我們的代碼會當(dāng)場掛掉夹攒。相反, 通過輸出, 我們能夠看到我們繼續(xù)計算了 Earth 和 Sun 的周長, 直到我們到達最后一行胁塞。
在那兒我們嘗試在 $c?
變量中使用 Failure 作為 max 程序的一個參數(shù)啸罢。因為我們在查詢真實的值, Failure 被激發(fā)并給了我們一個很好的反向追蹤扰才。上面的錯誤信息包含了我們的 Failure 爆發(fā)(第十五行)點, 還有我們的接收點(第七行)還有錯誤來自哪兒(第二行)蕾总。真甜!
結(jié)論
Perl 6 擁有令人驚嘆的 Errors!
大西瓜啊琅捏。