Tại sao thiết kế Go lại không tốt cho các lập trình viên thông minh

Trong những tháng qua, tôi đã sử dụng Go để triển khai. Bằng chứng của khái niệm (khoảng: mã để kiểm tra chức năng của một ý tưởng) trong thời gian rảnh rỗi, một phần để nghiên cứu ngôn ngữ lập trình. Bản thân các chương trình rất đơn giản và không phải là mục đích của bài viết này, nhưng bản thân trải nghiệm sử dụng Go xứng đáng nhận được vài lời về nó. Đi hứa sẽ được (khoảng: bài viết viết năm 2015) một ngôn ngữ phổ biến cho mã có khả năng mở rộng nghiêm trọng. Ngôn ngữ được tạo bởi Google và được sử dụng tích cực. Tóm lại, tôi thành thật nghĩ rằng thiết kế của ngôn ngữ Go không tốt cho các lập trình viên thông minh.

Được thiết kế cho các lập trình viên yếu?

Go rất dễ học, dễ đến mức phần giới thiệu khiến tôi mất một buổi tối, sau đó tôi đã có thể viết mã một cách hiệu quả. Cuốn sách tôi từng học cờ vây có tên là Giới thiệu về lập trình trong Go (dịch), nó có sẵn trực tuyến. Cuốn sách, giống như mã nguồn Go, rất dễ đọc, có các ví dụ về mã hay và chứa khoảng 150 trang có thể đọc được trong một lần. Sự đơn giản này ban đầu mang lại cảm giác mới mẻ, đặc biệt là trong thế giới lập trình chứa đầy công nghệ quá phức tạp. Nhưng cuối cùng, sớm hay muộn ý nghĩ này cũng nảy sinh: “Có thật như vậy không?”

Google tuyên bố tính đơn giản của Go là điểm bán hàng của nó và ngôn ngữ được thiết kế để mang lại năng suất tối đa trong các nhóm lớn, nhưng tôi nghi ngờ điều đó. Có những tính năng còn thiếu hoặc quá chi tiết. Và tất cả chỉ vì sự thiếu tin tưởng vào các nhà phát triển, với giả định rằng họ không thể làm đúng bất cứ điều gì. Mong muốn về sự đơn giản này là một quyết định có ý thức của các nhà thiết kế ngôn ngữ và để hiểu đầy đủ lý do tại sao nó lại cần thiết, chúng ta phải hiểu động lực của các nhà phát triển và những gì họ đang cố gắng đạt được trong Go.

Vậy tại sao nó lại được thực hiện đơn giản như vậy? Dưới đây là một vài trích dẫn Rob Pike (khoảng: một trong những người đồng sáng tạo ngôn ngữ Go):

Điểm mấu chốt ở đây là các lập trình viên của chúng tôi (khoảng: nhân viên Google) không phải là nhà nghiên cứu. Theo quy luật, họ còn khá trẻ, đến với chúng tôi sau khi học xong, có lẽ họ đã học Java, C/C++ hoặc Python. Họ không thể hiểu được một ngôn ngữ tuyệt vời, nhưng đồng thời chúng tôi muốn họ tạo ra phần mềm tốt. Đó là lý do tại sao ngôn ngữ của họ phải dễ hiểu và dễ học đối với họ.
 
Chắc anh này quen lắm, nói đại khái giống C. Các lập trình viên làm việc tại Google bắt đầu sự nghiệp của họ sớm và hầu hết đều quen thuộc với các ngôn ngữ thủ tục, đặc biệt là họ C. Yêu cầu về năng suất nhanh trong ngôn ngữ lập trình mới có nghĩa là ngôn ngữ đó không nên quá cấp tiến.

Cái gì? Vì vậy, Rob Pike về cơ bản đang nói rằng các nhà phát triển tại Google không giỏi đến thế, đó là lý do tại sao họ tạo ra một ngôn ngữ dành cho những kẻ ngốc (khoảng: câm lặng) để họ có thể làm điều gì đó. Cái nhìn kiêu ngạo nào về đồng nghiệp của bạn? Tôi luôn tin rằng các nhà phát triển của Google được lựa chọn cẩn thận từ những người thông minh nhất và giỏi nhất trên Trái đất. Chắc chắn họ có thể xử lý được việc gì đó khó khăn hơn?

Đồ tạo tác của sự đơn giản quá mức

Đơn giản là mục tiêu xứng đáng trong bất kỳ thiết kế nào và cố gắng làm một điều gì đó đơn giản là điều khó khăn. Tuy nhiên, khi cố gắng giải quyết (hoặc thậm chí diễn đạt) các vấn đề phức tạp, đôi khi cần có một công cụ phức tạp. Sự phức tạp và phức tạp không phải là tính năng tốt nhất của ngôn ngữ lập trình, nhưng có một nền tảng trung gian trong đó ngôn ngữ có thể tạo ra những khái niệm trừu tượng trang nhã, dễ hiểu và dễ sử dụng.

Không biểu cảm lắm

Do mong muốn sự đơn giản, Go thiếu các cấu trúc được coi là tự nhiên trong các ngôn ngữ khác. Điều này thoạt nghe có vẻ là một ý tưởng hay, nhưng trên thực tế, nó dẫn đến đoạn mã dài dòng. Lý do cho điều này rất rõ ràng - các nhà phát triển cần phải dễ dàng đọc mã của người khác, nhưng trên thực tế, những đơn giản hóa này chỉ gây hại cho khả năng đọc. Không có chữ viết tắt trong Go: rất nhiều hoặc không có gì.

Ví dụ: tiện ích bảng điều khiển đọc stdin hoặc tệp từ đối số dòng lệnh sẽ trông như thế này:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

        file, err := os.Open(flags[0])

        if err != nil {
            log.Fatal(err)
        }

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

    for scanner.Scan() {
        text += scanner.Text()
    }

    err = scanner.Err()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

Mặc dù mã này cũng cố gắng tổng quát nhất có thể, nhưng tính dài dòng bắt buộc của Go sẽ gây cản trở và kết quả là việc giải quyết một vấn đề đơn giản sẽ dẫn đến một lượng lớn mã.

Ví dụ, đây là một giải pháp cho cùng một vấn đề trong D:

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

Và ai là người dễ đọc hơn bây giờ? Tôi sẽ bỏ phiếu cho D. Mã của anh ấy dễ đọc hơn nhiều vì anh ấy mô tả các hành động rõ ràng hơn. D sử dụng các khái niệm phức tạp hơn nhiều (khoảng: cuộc gọi chức năng thay thế и mẫu) so với ví dụ về Go, nhưng thực sự không có gì phức tạp khi hiểu chúng.

Địa ngục sao chép

Một gợi ý phổ biến để cải thiện cờ vây là tính tổng quát. Điều này ít nhất sẽ giúp tránh việc sao chép mã không cần thiết để hỗ trợ tất cả các loại dữ liệu. Ví dụ, một hàm tính tổng một danh sách các số nguyên có thể được thực hiện không có cách nào khác ngoài việc sao chép và dán hàm cơ bản của nó cho từng loại số nguyên; không có cách nào khác:

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int8Sum(list []int8) (uint64) {
    var result int8 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list8  := []int8 {1, 2, 3, 4, 5}
    list16 := []int16{1, 2, 3, 4, 5}
    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

Và ví dụ này thậm chí không hoạt động đối với các loại đã ký. Cách tiếp cận này vi phạm hoàn toàn nguyên tắc không lặp lại chính mình (KHÔ), một trong những nguyên tắc nổi tiếng và rõ ràng nhất, bỏ qua nguyên tắc nào là nguồn gốc của nhiều sai sót. Tại sao Go làm điều này? Đây là một khía cạnh khủng khiếp của ngôn ngữ.

Ví dụ tương tự trên D:

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Đơn giản, thanh lịch và đi thẳng vào vấn đề. Hàm được sử dụng ở đây là reduce cho loại mẫu và vị ngữ. Đúng, điều này lại phức tạp hơn phiên bản Go, nhưng không quá khó hiểu đối với các lập trình viên thông minh. Ví dụ nào dễ bảo trì hơn và dễ đọc hơn?

Bỏ qua hệ thống loại đơn giản

Tôi tưởng tượng các lập trình viên Go đọc được điều này sẽ sùi bọt mép và hét lên, “Bạn đang làm sai rồi!” Chà, có một cách khác để tạo một hàm và kiểu chung, nhưng nó hoàn toàn phá vỡ hệ thống kiểu!

Hãy xem ví dụ sau về cách sửa lỗi ngôn ngữ ngu ngốc để giải quyết vấn đề:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

Việc thực hiện này Reduce được mượn từ bài viết Thành ngữ chung trong Go (khoảng: Tôi không thể tìm thấy bản dịch, tôi sẽ rất vui nếu bạn giúp đỡ việc này). Chà, nếu nó là thành ngữ, tôi ghét phải xem một ví dụ không thành ngữ. Cách sử dụng interface{} - một trò hề, và trong ngôn ngữ chỉ cần bỏ qua việc gõ. Đây là một giao diện trống và tất cả các loại đều triển khai nó, mang lại sự tự do hoàn toàn cho mọi người. Phong cách lập trình này cực kỳ xấu xí, và đó không phải là tất cả. Những màn nhào lộn như thế này đòi hỏi phải sử dụng phản ánh thời gian chạy. Ngay cả Rob Pike cũng không thích những cá nhân lạm dụng điều này, như anh ấy đã đề cập trong một báo cáo của mình.

Đây là một công cụ mạnh mẽ nên được sử dụng một cách thận trọng. Nên tránh trừ khi thực sự cần thiết.

Tôi sẽ lấy mẫu D thay vì điều vô nghĩa này. Làm sao có ai có thể nói như vậy interface{} dễ đọc hơn hoặc thậm chí gõ an toàn?

Tai ương của việc quản lý sự phụ thuộc

Go có một hệ thống phụ thuộc tích hợp được xây dựng dựa trên các nhà cung cấp dịch vụ lưu trữ phổ biến VCS. Các công cụ đi kèm với Go biết về các dịch vụ này và có thể tải xuống, xây dựng và cài đặt mã từ chúng chỉ trong một lần. Mặc dù điều này thật tuyệt vời nhưng có một lỗ hổng lớn trong việc lập phiên bản! Có, thực sự, bạn có thể lấy mã nguồn từ các dịch vụ như github hoặc bitbucket bằng công cụ Go, nhưng bạn không thể chỉ định phiên bản. Và một lần nữa sự đơn giản phải trả giá bằng sự hữu ích. Tôi không thể hiểu được tính logic của một quyết định như vậy.

Sau khi đặt câu hỏi về giải pháp cho vấn đề này, nhóm phát triển Go đã tạo ra chủ đề Diễn đàn, trong đó nêu rõ cách họ sẽ giải quyết vấn đề này. Khuyến nghị của họ là một ngày nào đó chỉ cần sao chép toàn bộ kho lưu trữ vào dự án của bạn và để nó “nguyên trạng”. họ đang nghĩ cái quái gì vậy? Chúng tôi có các hệ thống kiểm soát phiên bản tuyệt vời với khả năng gắn thẻ và hỗ trợ phiên bản tuyệt vời mà những người sáng tạo Go bỏ qua và chỉ sao chép mã nguồn.

Hành trang văn hóa từ Xi

Theo tôi, Go được phát triển bởi những người đã sử dụng C cả đời và bởi những người không muốn thử điều gì đó mới. Ngôn ngữ có thể được mô tả là C với các bánh xe phụ (nguồn gốc.: bánh xe đào tạo). Không có ý tưởng mới nào trong đó, ngoại trừ việc hỗ trợ tính song song (nhân tiện, điều này thật tuyệt vời) và điều này thật đáng tiếc. Bạn có khả năng song song tuyệt vời trong một ngôn ngữ khập khiễng, hầu như không thể sử dụng được.

Một vấn đề rắc rối khác là Go là một ngôn ngữ thủ tục (giống như nỗi kinh hoàng thầm lặng của C). Cuối cùng, bạn viết mã theo phong cách thủ tục có cảm giác cổ xưa và lỗi thời. Tôi biết lập trình hướng đối tượng không phải là một giải pháp dễ dàng, nhưng sẽ thật tuyệt nếu có thể trừu tượng hóa các chi tiết thành các loại và cung cấp khả năng đóng gói.

Đơn giản vì lợi ích của bạn

Cờ vây được thiết kế đơn giản và nó đã đạt được mục tiêu đó. Nó được viết cho những lập trình viên yếu, sử dụng ngôn ngữ cũ làm mẫu. Nó đi kèm với các công cụ đơn giản để làm những việc đơn giản. Nó rất dễ đọc và dễ sử dụng.

Nó cực kỳ dài dòng, không ấn tượng và không tốt cho các lập trình viên thông minh.

Cảm ơn mersinvald để chỉnh sửa

Nguồn: www.habr.com

Thêm một lời nhận xét