1. 程式人生 > >程式語言特性:函式

程式語言特性:函式

程式語言特性是我一個新的系列,某一大膽想法的實體,我很願意投入大量精力去試錯這個主題。該系列將用來闡述一門程式語言應該具有什麼模樣,它無關任何一門具體語言,但是卻從眾多已知程式語言提煉而出。每一個特性,它必須是嚴格經過市場檢驗合格,而我也會持續觀察,從眾多的複雜性中裁剪定製,試圖設計一門新的烏托邦式程式設計概念語言。

1. 前言

標題為啥用函式不用方法?方法和函式的區別在我看來,無非是方法多了一個引數,該引數指代方法的宿主,所以方法是函式的延伸,一個特例。程式語言的函式原型來自於數學,源遠流長,在廣大人群中已經根深蒂固,而且它的表意也是非常明確的,有未知數,視同輸入,有函式值,視同輸出。總而言之,我偏好函式。

2. 建模

函式本身是一個虛擬的抽象概念,而我們現在做的事就是在抽象中進一步抽象,也就是抽象的抽象。在我看來,抽象是簡化的同義詞,都是試圖提取重要的共性,忽略不重要的細節。抽象的產物的我們一般用模型指代,所以整個流程我稱之為建模。

2.1 函式的特性

2.1.1 都具有輸入輸出

這個特性已經被程式設計師爛熟於心,以至於在這裡顯得有些多餘,正是因為他的普適,而更加說明這是不可或缺的一個重要組成元素。

輸入是已知的,而輸出是未知的,是我們需要求索的答案。函式經常被看做是一個黑盒子,我們把輸入放進去,輸出就會自動出來,而黑盒子裡是什麼,如果你只是一個使用者,則無需關心。這種方式極大的簡化函式呼叫者的開發難度。

graph LR
輸入-->函式
函式-->輸出
複製程式碼

這裡,可能有人說了,有些函式沒有輸出,“這裡沒有輸出”意指函式沒有返回值,有些程式設計語稱也把這些沒有返回值的函式稱為過程,但是我這裡的輸出再次重申為不僅限於函式返回值的輸出,而包括其他形式的輸出,比如函式副作用,產生對外部狀態的改變的行為也統稱為輸出,如果一個函式封閉式的內部改變,只進不出,雖不能否定其存在,但是從實用角度來講毫無意義。輸入同理,也不應該理解為狹義的程式語言函式的引數列表,而應該理解為外部狀態的輸入。

通常輸入的具體實現是一個引數列表,該引數列表至少包含每個引數的變數名稱和型別,如果是弱型別語言,型別也可省略;或者再不濟,函式內部也至少提供訪問到引數的路徑,這樣連整個引數列表都可以省略,比如 js

中函式內部內建變數 arguments 就可以訪問到呼叫者所有實際引數,而不需要任何形式引數的宣告;再或者這個函式沒有引數列表,但這不代表,它沒有輸入,它有隱式輸入,例如函式程式碼可以訪問到某些外部狀態,比如獲取當前時間,我們也認為這是輸入的一種其他形式 。

而輸出不需要任何名稱標識,但不是絕對的,比如 go 語言提供了返回值名稱宣告,方便了在函式多返回值時內部直接賦值,所以只能說返回標識不是必要的。強型別語言可能會需要提供返回型別宣告,而弱型別語言連返回型別都可以省略。

接下來,我們來看看不同程式語言的一個函式應該長啥樣。

go

package main

import "fmt"

func main() {
    result := foo("medivh")
    fmt.Println("result: " + result);
}

func foo(name string) string {
    fmt.Println("hello " + name)
    return name
}
複製程式碼

python

def foo(name):
    print("hello " + name)
    return name;

result = foo("medivh");
print("result: " + result)
複製程式碼

javascript

function foo(name) {
    console.log('hello ' + name);
    return name;
}

var result = foo("medivh")
console.log("result:", result);
複製程式碼

java

class Main {
    public static void main(String[] args) {
        String result = foo("medivh");
        System.out.println("result: " + result);
    }

    static String foo(String name) {
        System.out.println("hello " + name);
        return name;
    }
}
複製程式碼

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        string result = Foo("medivh");
        Console.WriteLine("result: " + result);
    }

    static string Foo(string name)
    {
        Console.WriteLine("hello " + name);
        return name;
    }
}
複製程式碼

以上程式碼都是絕對可執行的,本人親測!同時安利一個好用的工具,可以在本地搭建服務執行多種程式語言程式碼,它的名字是 nodebook 。我這裡的程式碼即是使用該工具測試執行完成。

我們可以觀察 foo 函式的定義,基本上所有程式語言語法都大同小異,都有引數列表和函式名稱構成函式簽名,強型別語言普遍有返回型別宣告,而弱型別語言普遍沒有返回型別宣告,因為多此一舉。那麼這麼多的表現形式,我們該怎麼分析?記住下面這句話:

記劍意,不要記劍招。

劍意是其本質,是種子,可以誕生無窮無盡的劍招。所以這裡的劍意就是一個函式一定要有訪問輸入和產生輸出的機制。

2.1.2 過載

事實證明過載不是必要的,例如 go 就沒有過載,我不知道是 go 沒來得及實現,還是設計構想中就沒有過載這項,不過現在使用 go 語言程式設計的人也沒有說因為缺少這一特性而而導致嚴重的問題出現,頂多是不習慣,不順手。

何為過載?過載就是複用相同的函式名稱,但是引數列表不同,進而函式簽名不同,從而提供可靠的差異性。要知道一句名言,我說的,如下:

程式設計最難的事是命名,幾乎佔程式設計 80% 的時間。

為啥命名這麼難,因為一個名字與其使用環境有著強關聯,名字就是背後複雜邏輯簡化的代名詞,你能想出好的名字,也就意味著你理解背後的邏輯。所以名字只是一個表徵,反映了你對程式的理解程度。我相信對於一個你十分熟悉的程式編寫,命名的效率應該會很快。但是名字也是稀缺資源,同一個名稱空間下,名字是用一個少一個,好用的名字不多,你只能在幾乎有限的名字池中篩選一個最合適的。而對於同一功能不同版本,命名更是難上加難,過載非常完美的解決這個問題,只要引數列表不同,重用相同的函式名稱是可能的。

過載的缺點是啥?是由引數列表的微小差異帶來的近似混淆。由於引數列表差異微小,致使呼叫者稍不留神,就編寫錯誤的呼叫程式碼。還有就是相容型別的處理,特別強調隱式轉換帶來的坑。

還有,弱型別語言大多數是沒有過載的,因為引數列表沒有強型別製造差異,導致程式執行時無法識別具體呼叫的目標是誰,當然也有意外,python3 通過特有的機制去實現過載,比如使用裝飾器 functools.singledispatch。而強型別語言過載基本上是標配。具體程式語言過載示例如下所示:

java

class Main {
    public static void main(String[] args) {
        foo();
        foo("medivh");
    }

    static void foo() {
        System.out.println("hello world");
    }

    static void foo(String name) {
        System.out.println("hello " + name);
    }
}
複製程式碼

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        Foo();
        Foo("medivh");
        Console.ReadKey();
    }

    static void Foo()
    {
        Console.WriteLine("hello world");
    }

    static void Foo(string name)
    {
        Console.WriteLine("hello " + name);
    }
}
複製程式碼

2.1.3 閉包

閉包是什麼?閉包是一種機制,一種行為,是函式使用方式的進一步發明與創造,它可以讓函式攜帶狀態。

我們知道,影響函式行為有兩大因數,一是函式的引數列表,二是外部狀態的訪問,例如函訪問全域性作用域變數。但是函式本身是沒有狀態的,所以它只能依據前面提的兩大因數的而產生行為變化。

那閉包做了什麼,它可以讓一個函式內建狀態,也就是說閉包讓函式攜帶了狀態,依據這些狀態,它的行為很可能相應發生變化,這種攜帶狀態的函式可以稱之為執行時函式。

閉包只能發生在函式內部,所謂的“閉”是指封閉在某一函式內部而且不外洩的意思,一般情況下是函式返回一個被構建好的函式,這個被構建出來的函式攜帶了構建函式產生的狀態資訊,即使是構建函式返回值之後,這些狀態依然被保留下來,可謂是如影隨行,除非函式引用為零,這些狀態資訊所佔用的記憶體才會被釋放掉。

閉包程式碼如下所示:

javascript

function fooConstructor(status){
    return function foo() {
        console.log("status: " + status);
    };
}

var foo = fooConstructor(47)
foo()
複製程式碼

python3

def fooConstructor(status):
    def foo():
        print("status: " + str(status))
    return foo

foo = fooConstructor(47)
foo()
複製程式碼

2.1.4 位置引數、可變引數、預設引數和可選引數

關於引數列表這裡還有很多花樣可以折騰,雖然增加了理解的複雜度,但是方便了函式呼叫者的使用。下面一一介紹。

位置引數是按照位置來傳參的函式引數宣告,它是必傳引數,也就是嚴格按照順序一一傳入,少一個也不行,這種情況想必大家都一清二楚,是最常見的引數形式。示例程式碼如下:

go

package main

import "fmt"

func main() {
    foo("a", "b")
}

func foo(arg1 string, arg2 string) {
    fmt.Printf("argument list: arg1 = %s, arg2 = %s\n", arg1, arg2)
}
複製程式碼

可變引數顧名思義,也就是可以有不確定數量的引數宣告,為了不產生二義性,它只能放在固定引數後面,通常在函式內部,以類陣列的方式去訪問。示例程式碼如下:

go

package main

import "fmt"

func main() {
    foo("a", "b", "c")
}

func foo(arg1 string, someArgs ... string) {
    fmt.Printf("argument list: arg1 = %s, arg2 = %s\n", arg1, someArgs)
}
複製程式碼

預設引數意為在函式呼叫之前會有預設值賦給引數變數,如果呼叫者,沒傳這個引數,則內部訪問到就是預設引數。示例程式碼如下:

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        Foo("medivh");
        Console.ReadKey();
    }

    static void Foo(string name, int age = 18)
    {
        Console.WriteLine("My name is " + name + ", and my age is " + age + " years old.");
    }
}
複製程式碼

可選引數意指可以傳遞也可以不傳遞的引數宣告,細分可以劃分為兩種型別,一種稱之為可變命名引數,也就是呼叫時可以通過指定名字賦值的引數宣告,它可以在固定引數後以任意順序指定,另一種稱之為可選位置引數,它是以固定順序傳參的引數宣告,它可以省略末尾的,但是不能省略中間或者排在前面的可選位置引數。可選引數通常會和預設引數配合使用,示例程式碼將以 Dart 舉例,因為他的語法在我看來比較優雅,程式碼如下:

dart

void main() {
    foo("medivh", age:24);
    foo2("medivh", "female", 17);
}

// 可選命名引數
void foo(String name, {String sex = "male", int age, String hobby="code"}) {
    print("name: " + name + ", sex: " + sex + ", age: " + age.toString() + ", hobby: " + hobby);
}

// 可選位置引數
void foo2(String name, [String sex = "male", int age, String hobby="code"]) {
    print("name: " + name + ", sex: " + sex + ", age: " + age.toString() + ", hobby: " + hobby);
}
複製程式碼

3 總結

函式是程式語言不可或缺的模組,使用它通常是為了封裝程式碼以達到複用的目的。這裡為函式建造出的模型覆蓋了多種程式語言函式的共有特性,這些特性是值得反覆推敲,也可思考替代某一種特性的其他實現方式,只不過,沉澱至今,更好的替代方案會越來越難找尋了。