Categories
程式開發

編程語言中的6個有趣特性


Java是一門不斷發展的語言,這是一件好事。然而,其他語言的一些特性也是值得研究的。語言的結構是人們思考問題的方式,也是人們設計解決方案的方式。學習或至少熟悉其他語言是藉鑑其設計的好方法。

Java是我學習的第一門語言並且是我專業使用的語言。它是我大約十五年以來的主要謀生手段。然而,它並不是我多年來學習和使用的唯一語言:例如,很久以前,我必須開發JavaScript代碼來實現動態用戶界面。當時,它被稱為DHTML ……幾年前,我還自學了Kotlin,並且從未停止過使用它。去年,在一家新公司工作時,我嘗試了Clojure,但沒有成功。

在上述所有場景中,Java仍然是我學習和評判其他語言的基準。以下是一些有趣的語言特性,我認為這些特性對於來自Java背景的人都頗具思想挑戰性。

JavaScript:原型

JavaScript是我和Java一起使用的第一種語言。儘管JavaScript已經發展這麼多年了,但它有一個實現起來非常奇怪的常見特性:新對象的實例化。

在Java中,首先創建要一個

public class Person {

  private final String name;
  private final LocalDate birthdate;

  public Person(String name, LocalDate birthdate) {
    this.name = name;
    this.birthdate = birthdate;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }
}

然後,就可以繼續創建該類的實例了:

var person1 = new Person("John Doe", LocalDate.now());
var person2 = new Person("Jane Doe", LocalDate.now());

JavaScript與Java的語法非常相似:

class Person {
  constructor(name, birthdate) {
    this.name = name;
    this.birthdate = birthdate;
  }
}

let person1 = new Person("John Doe", Date.now());
let person2 = new Person("Jane Doe", Date.now());

相似之處到此為止。由於JavaScript具有動態特性,所以可以向現有實例中添加屬性和函數。

person1.debug = function() {
  console.debug(this);
}

person1.debug();

但是,這些只能添加到某個實例中。其他實例會缺少這些補充屬性或函數:

person2.debug();  // Throws TypeError: person2.debug is not a function

要將函數(或屬性)添加到所有實例(無論是現在的還是將來的)中,都需要利用原型的概念:

Person.prototype.debug = function() {
  console.debug(this);
}

person1.debug();
person2.debug();

let person3 = new Person("Nicolas", Date.now());

person3.debug();

Kotlin:擴展函數/屬性

幾年前,我開始嘗試著自學Android。我發現這種體驗對開發人員來說不太友好:當然,我了解它其中一個目標是盡可能減少內存佔用,但這是以非常簡潔的API為代價的。

我記得當時我必須調用帶有很多參數的方法,其中大多數參數為null。在嘗試尋找到一種方法來解決這個問題時,找到了Kotlin的擴展屬性:帶有默認參數。我後來停止了Android的學習,但仍繼續使用Kotlin。

我喜歡Kotlin。很多人都稱讚Kotlin的null安全性(null-safety)實現。但對我來說,我喜歡它,並不是因為它是null安全的,而是因為別的。

假設我們經常需要將字符串首字母改成大寫。在Java中實現這一目的的方法是使用靜態方法創建一個類:

public class StringUtils {

  public static String capitalize(String string) {
    var character = string.substring(0, 1).toUpperCase();
    var rest = string.substring(1, string.length() - 1).toLowerCase();
    return character + rest;
  }
}

在早期,每個項目幾乎都具有StringUtils和DateUtils類。幸運的是,現有的庫提供了最常用的功能,例如Apache Commons LangGuava。然而,它們仍遵循相同的設計原則,即遵循基於靜態方法的設計原則。這很糟糕,因為Java被認為是一種面向對象語言。不幸的是,靜態方法不是面向對象的。

擴展函數和屬性的幫助下,Kotlin允許將行為、狀態分別添加到現有的類中。語法非常簡單,並且與面向對象的方法完全兼容:

fun String.capitalize(): String {
  val character = substring(0, 1).toUpperCase()
  val rest = substring(1, length - 1).toLowerCase()
  return character + rest
}

在編寫Kotlin代碼時,我經常使用這個。

在底層,Kotlin編譯器生成與Java代碼類似的字節碼。這僅僅是語法糖,但是從設計的角度來看,與Java代碼相比,它是一個巨大的改進!

Go:隱式接口實現

在大多數面向對象語言(Java、Scala、Kotlin等)中,類可以實現一個契約(也稱為接口)。這樣,客戶端代碼可以引用該接口,而無需關心任何特定的實現。

public interface Shape {

  float area();
  float perimeter();

  default void display() {
    System.out.println(this);
    System.out.println(perimeter());
    System.out.println(area());
  }
}
public class Rectangle implements Shape {

  public final float width;
  public final float height;

  public Rectangle(float width, float height) {
    this.width = width;
    this.height = height;
  }
  
  @Override
  public float area() {
    return width * height;           //(1)
  }

  @Override
  public float perimeter() {
    return 2 * width + 2 * height;   //(1)
  }

  public static void main(String... args) {
    var rect = new Rectangle(2.0f, 3.0f);
    rect.display();
  }
}

(1)處為了精確起見,應該使用 BigDecimal ,但這不是重點

重點是:由於 Rectangle 實現了 Shape,所以可以在 Rectangle 的任何實例上調用在 Shape 上定義的 display() 方法。

Go不是一種面向對象語言:它沒有類的概念。它提供了結構體,並且函數可以與這種結構體相關聯。它還提供了接口,該接口可以使用結構體來實現。

然而,Java實現接口的方式是顯式的:Rectangle 類聲明它實現了Shape。相反,Go的方式是隱式的。實現接口所有函數的結構體隱式地實現了該接口。

這可以轉換為如下代碼:

package main

import (
 "fmt"
)

type shape interface {        //(1)               
 area() float32
 perimeter() float32
}

type rectangle struct {         //(2)             
 width float32
 height float32
}

func (rect rectangle) area() float32 {     //(3)   
 return rect.width * rect.height
}

func (rect rectangle) perimeter() float32 {    //(3)
 return 2 * rect.width + 2 * rect.height
}
func display(shape shape)  {           //(4)      
 fmt.Println(shape)
 fmt.Println(shape.perimeter())
 fmt.Println(shape.area())
}

func main() {
 rect := rectangle{width: 2, height: 3}
 display(rect)                         //(5)      
}

(1)定義 shape 接口

(2)定義 rectangle 結構體

(3)將兩個 shape 函數添加到 rectangle 中

(4)display() 方法只接收一個 shape 參數

(5)因為 rectangle 實現了shape的所有函數,並且由於是隱式實現的,所以 rect 也是一個 shape。因此,調用display()方法並將rect作為參數進行傳遞是完全合法的

Clojure:“依賴類型”

我之前的公司對Clojure投入了大量的資金。正因為如此,我努力學習過這門語言,甚至還寫了幾篇文章來總結我對它的理解。

Clojure深受LISP的啟發。因此,表達式用圓括號括起來,首先執行位於圓括號內部的方法。此外,Clojure是一種動態類型語言:它們雖然有類型,但沒有聲明。

另一方面,該語言提供了基於契約的編程。可以指定前置條件和後置條件:它們在運行時計算。這些條件可以進行類型檢查,例如,檢查參數是字符串還是布爾值等?甚至可以進行更進一步地檢查,類似於_dependent類型:

在計算機科學和邏輯學中,依賴類型是其定義依賴於某個值的類型。 “整數對”是一種類型。由於對值的依賴,“第二個大於第一個的整數對”也是依賴類型。

— 維基百科

https://en.wikipedia.org/wiki/Dependent_type

它在運行時強制執行,因此它不能被真正稱為依賴類型。然而,這是我所接觸過的語言中最接近依賴類型的一種了。

之前,我曾詳細寫過一篇關於依賴類型和基於契約編程的文章

Elixir :模式匹配

一些語言吹噓自己提供了模式匹配的特性。通常,模式匹配可用於計算變量,例如,在Kotlin中:

var statusCode: Int
val errorMessage = when(statusCode) {
  401 -> "Unauthorized"
  403 -> "Forbidden"
  500 -> "Internal Server Error"
  else -> "Unrecognized Status Code"
}

這個用法是類固醇上(steroids)的switch語句。然而,一般來說,模式匹配的應用要廣泛得多。在下面的代碼片段中,首先檢查常規HTTP狀態錯誤碼,如果沒有找到,則默認設成更通用的錯誤信息:

val errorMessage = when {
  statusCode == 401 -> "Unauthorized"
  statusCode == 403 -> "Forbidden"
  statusCode - 400  "Client Error"
  statusCode == 500 -> "Internal Server Error"
  statusCode - 500  "Server Error"
  else -> "Unrecognized Status Code"
}

不過,它是有限制的。

Elixir是一種在Erlang OTP上運行的動態類型語言,它將模式匹配提升到了一個全新的水平。 Elixir的模式匹配可用於簡單的變量析構:

{a, b, c} = {:hello, "world", 42}

a 將被賦值成 :hello,b 被賦值成 “world”,c 被賦值成 42。

它還可以對集合進行更高級的析構:

(head | tail) = (1, 2, 3)

head 被賦值成 1,tail 被賦值成 (2, 3)。

然而,對於函數重載來說,它甚至更是如此。作為一種函數式語言,Elixir沒有用於循環的關鍵字(for 或 while),循環需要使用遞歸來實現。

舉個例子,我們使用遞歸來計算 List 的大小。在Java中,這是很容易的,因為有一個size()方法,但是 Elixir API 沒有提供這樣的功能。讓我們用如下的偽代碼來實現該功能,Elixir 也是採用這種遞歸的方法。

public int lengthOf(List item) {
  return lengthOf(0, items);
}

private int lengthOf(int size, List items) {
  if (items.isEmpty()) {
    return size;
  } else {
    return lengthOf(size + 1, items.remove(0));
  }
}

幾乎可以將它逐行的轉換成 Elixir:

def length_of(list), do: length_of(0, list)

defp length_of(size, list) do
  if () == list do
    size
  else
    (_ | tail) = list           //(1)
    length_of(size + 1, tail)
  end
end

(1)變量析構的模式匹配。表頭的值被賦值給 _ 變量,這意味著以後就無法引用它了,因為它沒有用處了。

然而,如前所述,Elixir模式匹配也適用於函數重載。因此,Elixir的命名方式將是:

def list_len(list), do: list_len(0, list)

defp list_len(size, ()), do: size      //(1)  
defp list_len(size, list) do        //(2)     
  (_ | tail) = list
  list_len(size + 1, tail)
end

(1)如果列表為空,則調用此方法

(2)否則調用此函數

注意,模式是按照聲明的順序進行評估的:在上面的代碼段中, Elixir首先評估具有空列表的函數,如果不匹配,才評估第二個函數,即列表不為空。如果要以相反的順序聲明函數,則每次都會對非空列表進行匹配操作。

Python:for推導式

Python是一種動態類型語言。與Java一樣,Python通過 for 關鍵字提供循環功能。下面的代碼片段循環遍歷集合中的所有項,並逐個打印它們。

for n in (1, 2, 3, 4, 5):
  print(n)

要在新集合中收集所有項,可以先創建一個空集合,然後在循環中添加每個項到空集合中:

numbers = ()
for n in (1, 2, 3, 4, 5):
  numbers.append(n)
print(numbers)

然而,可以使用一個精美的Python特性:for推導式(for comprehensions)。雖然它與標準循環使用相同的 for 關鍵字,但是for推導式是一個能獲得相同結果的函數式構造器。

numbers = (n for n in (1, 2, 3, 4, 5))
print(numbers)

上面片段的輸出是 (1, 2, 3, 4, 5) 。

也可以轉換每個項。例如,下面的代碼段將計算每個項的平方:

numbers = (n ** 2 for n in (1, 2, 3, 4, 5))
print(numbers)

輸出是 (1, 4, 9, 16, 25)。

for推導式的一個好處是能夠使用條件語句。例如,下面的代碼片段將只過濾偶數項,然後將其平方:

numbers = (n ** 2 for n in (1, 2, 3, 4, 5) if n % 2 == 0)
print(numbers)

輸出是 (4, 16)。

最後,for推導式允許使用笛卡爾積。

numbers = (a:n for n in (1, 2, 3) for a in ('a', 'b'))
print(numbers)

它將會輸出(('a', 1), ('b', 1), ('a', 2), ('b', 2), ('a', 3), ('b', 3))。

以上的for推導式也被稱為列表推導式(list comprehensions),因為它們是為了創建新的列表而設計的。Map推導式(Map comprehension)也是非常相似的,目的是為了創造map。

原文鏈接:

https://blog.frankel.ch/six-interesting-features-programming-languages/