Syntax

本篇將講述常見程式語言的解構方式,以常見的程式語言作為範例。建議先閱讀 Regular Expression

閱讀此篇有助於在不同語言之間切換,並有助於理解新的語言;無論為結構語言還是程式語言。

Lark Parser

Python 套件 lark-parser/lark 可以自訂程式語言的規則並解析它們。

Lark 基於 EBNF,並使用 Earley Parser,本篇將解說電腦如何閱讀「語言」。

基本元素:

"A" "ABC" "(" ")"  // 字串與符號
"ABC"i  // 不分大小寫的字串
/regular_expression/flag  // Regular Expression
"a".."z" "1".."9"  // 範圍
expression ~ n  // 前一個子句必須出現 n 次
expression ~ n..m  // 前一個子句必須出現 n 到 m 次
expression?  // 前一個子句可以不存在或出現一次
expression*  // 前一個子句可以不存在,或可以無限重複
expression+  // 前一個子句至少出現一次,並可以無限重複
(expression expression ...)  // 提升優先權
[expression expression ...]  // 可選,同於 (expression expression ...)?

定義句:

子句一: "1".."9"
子句二: 子句一 "+" 子句一

上面的 子句二 可以表示 1 + 1

基本上,為了排版,電腦使用的語言會忽略空白:

%ignore /\s+/

上面的空白用 RE 表示,意味著一個或重複的不可視字元(包含 Tab)。 忽略後,除非顯式表示,否則解析器不會計算。 例如:1 + 11+1 是等效的;A (B) CA(B)C 是等效的。 但是文字通常有連續性,除非用符號分離,否則一定要至少隔一個空白。

而文字也是定義的一部份,因此當變數名稱訂為:

variable_character: "a".."z" | "A".."Z" | "_"
variable: variable_character+

符號 _ 也會被當成是變數可用的字元,並保持連續性。 一些程式語言也許有例外,如 JavaScript 允許 $ 作為變數名稱,而 Matlab 不允許 _ 作為變數的第一個字元。

子句的組成會影響解析的順序,進而影響程式中的優先權 (Priority)。

digit: "0".."9"
int: digit+
plus: int "+" int
strong: "(" (int | plus_statement) ")"
base: int | strong | plus
plus_statement: base ("+" base)+

如上所示,plus_statement 可以表示 (1 + 20) + 7(5 + 6)4 + (6 + 9)2 + 777 等語句。

而以下文法:

digit: "0".."9"
int: digit+
assignment: plus_statement "=" int

對於 2 + 3 = 5 會先解析 2 + 3,而非 3 = 5

Commonly Used Syntax

可簡單歸類程式語言中的語法構成。

其中 op1, op2, ... 為使用的符號,可為任何字元或字串。

以下將舉例各式程式語言的部分文法:

Comment

comment: op1 /[^\n]*/
multiline_comment: op1 /[\s\S]*/ op2

註解是程式語言中很重要的一部份,因為他們不會影響執行內容。 不過這些說明內容可以被提取出來做為說明文件等用途。

  • C / C++ / Java / JavaScript / C# / ...

    // 單行註解
    /* 區段註解 */
    
  • Python

    # 單行註解
    """區段註解"""  # 其實是多行字串
    
  • Matlab

    % 單行註解
    %% 段落註解,以下的區塊可以被折疊。
    
  • Lua

    -- 單行註解
    --[[
    區段註解
    --]]
    

Line Break

某些語言是會忽略換行記號的,因此可以併成一行也不會影響解析,有助於在只有一行的情況下輸入,如 Bash 中。

%ignore /\r?\n/+

要注意有些程式碼並不是結尾有 ; 註記就是忽略換行記號, 如 Matlab 只是為了隱藏輸出訊息;Lua 沒有結尾記號但是會忽略換行。 而像 JavaScript 會在換行處自動檢查並補上,但是官方仍建議開發者必須打上。 至於 Python 則是加上 ; 可以併做一行,可是無法加上縮排。

忽略換行記號的語言如 C / C++ 可以這樣寫:

a = (1 + 2)
    * (2 + 3)
    * (3 + 4);

可是如 JavaSript 有自動插入的機制,如下會導致強制段行(returnbreakthrow, continue 結尾):

return
    1 + 3;

而在意換行記號的語言因為閱讀需要,會有換行的策略,如:

  • Python:可以在括弧 ()[]{} 中換行。但是同時因為縮排規定,其中的縮排不能比原始行小。

    a = (
        "aaa",
        "bbb",
        "ccc",
    )
    
  • Bash:必須使用 \ 符號在換行結尾。

    rm -f file_a.md \
    file_b.md ${FLAG}
    
  • Matlab:可以在 ; 後換行,除此之外必須使用 ... 符號在換行結尾,而且其之後的內容會被視為註解。

    a = (1 + 2)...
        * (2 + 3)...
        * (3 + 4);
    a = [... 這裡要加
        1, 2, 3;
        4, 5, 6;
        7, 8, 9]
    
  • Markdown:段落之間必須空一行,否則視為上一行,至於其他如列舉或標題等語法不受限。

    aaa,
    bbb.
    
    ccc:
    1. ddd
        with ddd
    1. eee
    1. fff
    

Indentation

縮排通常做為排版使用,不過仍有語言作為程式碼區塊的判定,如 Python。

indentation: " " ~ 4 | /\t/
block: indentation+ code_block

若非使用縮排,程式區塊會使用符號或語句作為標記。

  • C / C++ / Java / JavaScript / C# / ...:使用大括弧 {}

    if (a < b) { return 0; }
    
  • Lua:不需要換行。有對應記號。

    if a < b then return 0 end
    while a < b do a = a + 1 end
    
  • Latex:有對應記號。另外括充模組也能夠提供巨集 (Macro) 讓語法比較好看。

    \if \else \fi  % 判斷式
    \loop \if \repeat  % While 迴圈,\repeat 有 \fi 的功能。
    
  • Matlab:需要換行。通常搭配 end 關鍵字結尾。

    if a < b
        return
    end
    

Binary Operator

binary: statement op1 statement

為兩項運算,如一般的數學式。前後的 statement 可以不相同。 在計算的情況,通常可以連續操作如 a + b + ca = b = c; 但為避免讀錯,如 a += b 的縮寫不會允許連續的語句。

  • Common

    a + b
    a < b
    a = b
    a += b
    a.b
    
  • Python

    a and b  # Logical AND
    a in b  # Contain check
    a is b  # Same memory position check
    a := b  # Assignment, but repersent "a" itself (in expression)
    
  • C / C++

    a && b  // Logical AND
    a->b  // Pointer member
    a::b  // Namespace member
    a, b  // Run "a" and "b" but repersent "b" itself (in expression)
    
  • JavaScript

    a === b  // Same memory position check
    
  • C#

    a ?? b // Repersents "b" if "a" is "null", otherwise repersent "a"
    a?.b  // Repersents "null" if "a" is "null", otherwise repersent the member "b"
    
  • R

    a <= b  # Assignment
    

Unary Operator

leading_unary: op1 statement
trailing_unary: statement op1

分成前置或尾置,為了相容二元運算,通常優先權會最大。 但是因為 C++ 的前車之鑑,發現它們極易破壞可閱讀性,因此新的程式語言就較沒有出現。

  • C / C++

    !a  // Logical not
    ~a  // Byte inverse
    *a  // Pointer get value
    &a  // Get position to pointer
    ++a  // Plus 1 to "a", then represent "a"
    a++  // Represents "a", then plus 1 to "a"
    
  • Python

    not a  # Logical not
    yield a  # Generator get input value (in expression)
    
  • C#

    a?  // Type "a" can be "null"
    
  • Haskell:函數型程式語言,語法與數學式相仿,函式優先權最高。由 f x1 x2 ... 表示函式 f 和其參數 x1x2 等。

    f a = sin a * 10  -- Same as "(sin a) * 10", "f" and "sin" is function.
    

Ternary Operator

ternary: statement op1 statement op2 statement

較長的句型,由兩個運算子組成。

  • C / C++ / Java / JavaScript / C#

    a ? b : c  // If "a" is true, repersents "b", otherwise repersents "c"
    
  • Python

    b if a else c  # If "a" is true, repersents "b", otherwise repersents "c"
    a for b in c  # An iterator generate "a", extract "b" from "c" during iterating
    a for b in c if d  # Same as above, skip if "d" is "False"
    

Other Statements

至於其他就比較多元了。因此,基礎概念必須先熟悉,如型別標示就會很大程度的影響文法, 但是只要知道該概念會需要哪些參數、選項,自然便會容易理解。

如單純的 while loop 在哪種語言都沒什麼變化:

  • C / C++ / Java / JavaScript / C# / ...

    while (check_statement) {
        run_statements
    }
    
    // Check after run once
    do {
        run_statements
    } while (check_statement)
    
  • Python

    while check_statement:
        run_statements
    
  • Rust

    
    #![allow(unused)]
    fn main() {
    while check_statement {
        run_statements
    }
    }
    

而物件導向有些卻有可見性,有些會聲明物件本身為 "self":

  • C++

    public:
    decorator return_type func_name(arg1_type arg1, arg2_type arg2, ...) {
        run_statements
    }
    
  • Python

    def func_name(self, arg1, arg2, ...):
        """Doc string"""
        run_statements
    

最後舉例 Function 的表示百百種:

  • C / C++:有 Closure 語法。

    // Normal Function
    decorator return_type func_name(arg1_type arg1, arg2_type arg2, ...) {
        run_statements
    }
    
    // Lambda Fuction (Closures, only in C++)
    [refer_mode] (arg1_type arg1, arg2_type arg2, ...) -> return_type {
        run_statements
    }
    
    // Generic Fuction (only in C++)
    template<template_option>
    decorator return_type func_name(arg1_type arg1, arg2_type arg2, ...) {
        run_statements
    }
    
  • Python:沒有 Closure 語法,自動處理。

    # Simple Function
    def func_name(arg1, arg2, ...):
        """Doc string"""
        run_statements
    
    # Lambda Function
    lambda arg1, arg2, ...: return_statement
    
    # Typed decorated Function
    @decorator
    def func_name(arg1: arg1_type, arg2: arg2_type, ..., *, kward1: kward1_type, ..., **, kward2: kward2_type, ...) -> return_type:
        """Doc string"""
        run_statements
    
  • JavaScript:沒有 Closure 語法,自動處理。

    // Simple Function
    function func_name(arg1, arg2, ...) {
        run_statements
    }
    
    // Lambda Function
    function (arg1, arg2, ...) {
        run_statements
    }
    
    // Lambda Function
    (arg1, arg2, ...) => {
        run_statements
    }
    
    // Lambda Function
    arg1 => return_statement
    
  • Rust:有 Closure 語法。

    
    #![allow(unused)]
    fn main() {
    // Simple Function
    fn func_name(arg1: arg1_type, arg2: arg2_type, ...) -> return_type {
        run_statements
        return_statement
    }
    
    // Generic Fuction
    fn func_name<template_option>(arg1: arg1_type, arg2: arg2_type, ...) -> return_type {
        run_statements
        return_statement
    }
    
    // Closures
    |arg1: arg1_type, arg2: arg1_type, ...| -> return_type {
        run_statements
    }
    }
    
  • Go:沒有 Closure 語法,自動處理。

    func func_name(arg1 arg1_type, arg2 arg2_type, ...) return_type {
        run_statements
    }