เหตุใดการออกแบบ Go จึงไม่ดีสำหรับโปรแกรมเมอร์อัจฉริยะ

ในช่วงหลายเดือนที่ผ่านมา ฉันใช้ Go เพื่อนำไปใช้งาน Proof of Concept (ประมาณ: รหัสเพื่อทดสอบการทำงานของไอเดีย) ในเวลาว่าง ส่วนหนึ่งเพื่อศึกษาภาษาการเขียนโปรแกรมนั่นเอง ตัวโปรแกรมเองนั้นเรียบง่ายมากและไม่ใช่จุดประสงค์ของบทความนี้ แต่ประสบการณ์การใช้งาน Go นั้นสมควรที่จะพูดอะไรสักสองสามคำเกี่ยวกับมัน ไปสัญญาว่าจะเป็น (ประมาณ: บทความที่เขียนในปี 2015) ภาษายอดนิยมสำหรับโค้ดที่ปรับขนาดได้อย่างจริงจัง ภาษานี้สร้างขึ้นโดย Google ซึ่งมีการใช้งานอย่างแข็งขัน สรุปแล้ว ฉันคิดว่าการออกแบบภาษา Go นั้นไม่ดีสำหรับโปรแกรมเมอร์ที่ชาญฉลาด

ออกแบบมาสำหรับโปรแกรมเมอร์ที่อ่อนแอใช่ไหม?

Go นั้นง่ายต่อการเรียนรู้ ง่ายมากจนฉันต้องใช้เวลาแนะนำตัวในเย็นวันหนึ่ง หลังจากนั้นฉันก็สามารถเขียนโค้ดได้อย่างมีประสิทธิผลแล้ว หนังสือที่ผมเคยเรียนโกะมีชื่อว่า ความรู้เบื้องต้นเกี่ยวกับการเขียนโปรแกรมใน Go (การแปล) มีจำหน่ายทางออนไลน์ หนังสือก็เหมือนกับซอร์สโค้ด Go ตรงที่อ่านง่าย มีตัวอย่างโค้ดที่ดี และมีประมาณ 150 หน้าที่สามารถอ่านได้ในคราวเดียว ความเรียบง่ายนี้ทำให้รู้สึกสดชื่นในช่วงแรก โดยเฉพาะอย่างยิ่งในโลกการเขียนโปรแกรมที่เต็มไปด้วยเทคโนโลยีที่ซับซ้อนมากเกินไป แต่ท้ายที่สุดแล้ว ไม่ช้าก็เร็ว ความคิดก็เกิดขึ้น: “เป็นเช่นนั้นจริงหรือ?”

Google อ้างว่าความเรียบง่ายของ Go คือจุดขาย และภาษาได้รับการออกแบบมาเพื่อประสิทธิภาพสูงสุดในทีมขนาดใหญ่ แต่ฉันสงสัย มีคุณสมบัติที่ขาดหายไปหรือมีรายละเอียดมากเกินไป และทั้งหมดเป็นเพราะขาดความไว้วางใจในตัวนักพัฒนาโดยมีข้อสันนิษฐานว่าพวกเขาไม่สามารถทำอะไรถูกต้องได้ ความปรารถนาในความเรียบง่ายนี้เป็นการตัดสินใจอย่างมีสติของนักออกแบบภาษา และเพื่อที่จะเข้าใจอย่างถ่องแท้ว่าทำไมจึงจำเป็น เราต้องเข้าใจแรงจูงใจของนักพัฒนาและสิ่งที่พวกเขาพยายามทำให้สำเร็จใน Go

แล้วทำไมมันถึงทำง่ายขนาดนี้ล่ะ? นี่คือคำพูดสองสามข้อ ร็อบ ไพค์ (ประมาณ: หนึ่งในผู้ร่วมสร้างภาษา Go):

ประเด็นสำคัญที่นี่คือโปรแกรมเมอร์ของเรา (ประมาณ: ชาว Google) ไม่ใช่นักวิจัย ตามกฎแล้วพวกเขายังอายุน้อยจึงมาหาเราหลังจากเรียนจบ บางทีพวกเขาอาจจะเรียน Java หรือ C/C++ หรือ Python พวกเขาไม่เข้าใจภาษาที่ยอดเยี่ยม แต่ในขณะเดียวกัน เราก็ต้องการให้พวกเขาสร้างซอฟต์แวร์ที่ดี นั่นคือเหตุผลที่ภาษาของพวกเขาควรจะง่ายสำหรับพวกเขาที่จะเข้าใจและเรียนรู้
 
เขาน่าจะคุ้นเคยประมาณพูดคล้ายกับซี โปรแกรมเมอร์ที่ทำงานที่ Google เริ่มต้นอาชีพตั้งแต่เนิ่นๆ และส่วนใหญ่คุ้นเคยกับภาษาขั้นตอน โดยเฉพาะตระกูล C ข้อกำหนดสำหรับประสิทธิภาพการทำงานที่รวดเร็วในภาษาการเขียนโปรแกรมใหม่หมายความว่าภาษาไม่ควรรุนแรงเกินไป

อะไร โดยพื้นฐานแล้ว Rob Pike กำลังบอกว่านักพัฒนาซอฟต์แวร์ของ Google ไม่ค่อยดีนัก นั่นคือเหตุผลที่พวกเขาสร้างภาษาสำหรับคนโง่ (ประมาณ: ใบ้ลง) เพื่อให้พวกเขาสามารถทำอะไรบางอย่างได้ เพื่อนร่วมงานของคุณดูหยิ่งแบบไหน? ฉันเชื่อมาโดยตลอดว่านักพัฒนาซอฟต์แวร์ของ Google ได้รับการคัดเลือกจากผู้ที่ฉลาดที่สุดและดีที่สุดในโลก แน่นอนว่าพวกเขาสามารถรับมือกับสิ่งที่ยากกว่านี้ได้ใช่ไหม?

สิ่งประดิษฐ์แห่งความเรียบง่ายเกินบรรยาย

ความเรียบง่ายเป็นเป้าหมายที่คุ้มค่าในการออกแบบใดๆ และการพยายามทำอะไรที่เรียบง่ายก็เป็นเรื่องยาก อย่างไรก็ตาม เมื่อพยายามแก้ไข (หรือแสดง) ปัญหาที่ซับซ้อน บางครั้งจำเป็นต้องใช้เครื่องมือที่ซับซ้อน ความซับซ้อนและความซับซ้อนไม่ใช่คุณสมบัติที่ดีที่สุดของภาษาการเขียนโปรแกรม แต่มีจุดกึ่งกลางที่ภาษาสามารถสร้างนามธรรมที่สวยงามซึ่งง่ายต่อการเข้าใจและใช้งาน

ไม่ค่อยแสดงออก

เนื่องจากความมุ่งมั่นในความเรียบง่าย Go จึงขาดโครงสร้างที่มองว่าเป็นธรรมชาติในภาษาอื่น นี่อาจดูเหมือนเป็นความคิดที่ดีในตอนแรก แต่ในทางปฏิบัติผลลัพธ์ที่ได้คือโค้ดแบบละเอียด เหตุผลนี้ควรชัดเจน - นักพัฒนาจะต้องอ่านโค้ดของผู้อื่นได้ง่าย แต่ในความเป็นจริงแล้ว การลดความซับซ้อนเหล่านี้ส่งผลเสียต่อความสามารถในการอ่านเท่านั้น Go ไม่มีคำย่อ: มากหรือไม่มีเลย

ตัวอย่างเช่น ยูทิลิตี้คอนโซลที่อ่าน stdin หรือไฟล์จากอาร์กิวเมนต์บรรทัดคำสั่งจะมีลักษณะดังนี้:

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)
}

แม้ว่าโค้ดนี้จะพยายามทำให้เป็นแบบทั่วไปที่สุดเท่าที่จะเป็นไปได้ แต่การใช้คำฟุ่มเฟือยแบบบังคับของ Go ก็เข้ามาขวางทาง และด้วยเหตุนี้ การแก้ปัญหาง่ายๆ จึงส่งผลให้มีโค้ดจำนวนมาก

ตัวอย่างเช่นนี่คือวิธีแก้ไขปัญหาเดียวกันค่ะ 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);
    }
}

แล้วตอนนี้ใครน่าอ่านกว่ากัน? ฉันจะลงคะแนนให้ D. รหัสของเขาอ่านง่ายขึ้นมากเนื่องจากเขาอธิบายการกระทำได้ชัดเจนยิ่งขึ้น D ใช้แนวคิดที่ซับซ้อนมากขึ้น (ประมาณ: การเรียกใช้ฟังก์ชันทางเลือก и รูปแบบ) มากกว่าในตัวอย่าง Go แต่ไม่มีอะไรซับซ้อนจริงๆ เกี่ยวกับการทำความเข้าใจมัน

นรกแห่งการคัดลอก

คำแนะนำยอดนิยมสำหรับการปรับปรุง Go คือเรื่องทั่วไป อย่างน้อยก็จะช่วยหลีกเลี่ยงการคัดลอกโค้ดโดยไม่จำเป็นเพื่อรองรับข้อมูลทุกประเภท ตัวอย่างเช่น ฟังก์ชันสำหรับการรวมรายการจำนวนเต็มสามารถนำไปใช้ในลักษณะอื่นไม่ได้นอกจากการคัดลอกและวางฟังก์ชันพื้นฐานสำหรับจำนวนเต็มแต่ละประเภท ไม่มีทางอื่น:

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))
}

และตัวอย่างนี้ใช้ไม่ได้กับประเภทที่เซ็นชื่อด้วยซ้ำ วิธีการนี้ฝ่าฝืนหลักการที่จะไม่ทำซ้ำตัวเองโดยสิ้นเชิง (แห้ง) หนึ่งในหลักการที่มีชื่อเสียงและชัดเจนที่สุด โดยไม่สนใจซึ่งเป็นต้นตอของข้อผิดพลาดมากมาย ทำไมโกทำแบบนี้? นี่เป็นแง่มุมที่แย่มากของภาษา

ตัวอย่างเดียวกันกับ D:

import std.stdio;
import std.algorithm;

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

เรียบง่าย หรูหรา และตรงประเด็น ฟังก์ชั่นที่ใช้ในที่นี้คือ reduce สำหรับประเภทเทมเพลตและภาคแสดง ใช่ สิ่งนี้ซับซ้อนกว่าเวอร์ชัน Go อีกครั้ง แต่ก็ไม่ใช่เรื่องยากที่โปรแกรมเมอร์อัจฉริยะจะเข้าใจ ตัวอย่างใดดูแลรักษาง่ายกว่าและอ่านง่ายกว่า

บายพาสระบบชนิดธรรมดา

ฉันจินตนาการว่าโปรแกรมเมอร์ Go ที่อ่านข้อความนี้จะต้องน้ำลายฟูมปากและกรีดร้องว่า “คุณทำผิด!” มีวิธีอื่นในการสร้างฟังก์ชันและประเภททั่วไป แต่มันทำลายระบบประเภทโดยสิ้นเชิง!

ดูตัวอย่างการแก้ไขภาษาโง่ๆ นี้เพื่อแก้ไขปัญหา:

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)
}

การดำเนินการนี้ Reduce ยืมมาจากบทความ ชื่อสามัญสำนวนใน Go (ประมาณ: ฉันหาคำแปลไม่เจอ ฉันยินดีถ้าคุณช่วยเรื่องนี้) ถ้ามันเป็นสำนวน ฉันคงไม่อยากเห็นตัวอย่างที่ไม่ใช่สำนวน การใช้งาน interface{} - เรื่องตลกและในภาษานั้นจำเป็นเท่านั้นที่จะเลี่ยงการพิมพ์ นี่คืออินเทอร์เฟซที่ว่างเปล่าและทุกประเภทนำไปใช้งาน เพื่อให้ทุกคนมีอิสระอย่างสมบูรณ์ การเขียนโปรแกรมรูปแบบนี้น่าเกลียดมาก และนั่นไม่ใช่ทั้งหมด การแสดงกายกรรมเช่นนี้จำเป็นต้องใช้การสะท้อนรันไทม์ แม้แต่ Rob Pike ก็ไม่ชอบบุคคลที่ละเมิดสิ่งนี้ ดังที่เขากล่าวไว้ในรายงานฉบับหนึ่งของเขา

นี่เป็นเครื่องมืออันทรงพลังที่ควรใช้ด้วยความระมัดระวัง ควรหลีกเลี่ยงเว้นแต่มีความจำเป็นอย่างยิ่ง

ฉันจะใช้เทมเพลต D แทนเรื่องไร้สาระนี้ ใครๆก็พูดแบบนั้นได้ยังไง. interface{} อ่านง่ายขึ้นหรือพิมพ์ได้อย่างปลอดภัย?

ปัญหาของการจัดการการพึ่งพา

Go มีระบบการพึ่งพาในตัวที่สร้างจากผู้ให้บริการโฮสติ้งยอดนิยม VCS. เครื่องมือที่มาพร้อมกับ Go รู้จักบริการเหล่านี้และสามารถดาวน์โหลด สร้าง และติดตั้งโค้ดจากบริการเหล่านี้ได้ในคราวเดียว แม้ว่าสิ่งนี้จะดี แต่ก็มีข้อบกพร่องที่สำคัญในการกำหนดเวอร์ชัน! ใช่ เป็นเรื่องจริงที่คุณสามารถรับซอร์สโค้ดจากบริการต่างๆ เช่น github หรือ bitbucket โดยใช้เครื่องมือ Go แต่คุณไม่สามารถระบุเวอร์ชันได้ และความเรียบง่ายอีกครั้งโดยเสียประโยชน์ ฉันไม่สามารถเข้าใจตรรกะของการตัดสินใจดังกล่าวได้

หลังจากถามคำถามเกี่ยวกับวิธีแก้ไขปัญหานี้แล้ว ทีมพัฒนา Go ก็ได้สร้างขึ้น กระทู้ฟอรั่มซึ่งสรุปว่าพวกเขาจะแก้ไขปัญหานี้อย่างไร คำแนะนำของพวกเขาคือเพียงคัดลอกพื้นที่เก็บข้อมูลทั้งหมดลงในโปรเจ็กต์ของคุณสักวันหนึ่งและปล่อยทิ้งไว้ "ตามที่เป็น" พวกเขากำลังคิดอะไรบ้า? เรามีระบบควบคุมเวอร์ชันที่น่าทึ่งพร้อมการแท็กและการสนับสนุนเวอร์ชันที่ยอดเยี่ยมซึ่งผู้สร้าง Go เพิกเฉยและเพียงคัดลอกซอร์สโค้ด

สัมภาระทางวัฒนธรรมจาก Xi

ในความคิดของฉัน Go ได้รับการพัฒนาโดยผู้ที่ใช้ C มาตลอดชีวิตและโดยผู้ที่ไม่ต้องการลองอะไรใหม่ๆ ภาษาสามารถอธิบายได้ว่าเป็น C พร้อมล้อเสริม (ต้นฉบับ: ล้อฝึก). ไม่มีแนวคิดใหม่ ๆ ในนั้นยกเว้นการสนับสนุนความเท่าเทียม (ซึ่งโดยวิธีการนั้นยอดเยี่ยมมาก) และนี่ก็เป็นเรื่องน่าละอาย คุณมีความเท่าเทียมที่ยอดเยี่ยมในภาษาง่อยและแทบจะไม่มีประโยชน์

ปัญหาที่ดังเอี๊ยดอีกประการหนึ่งคือ Go เป็นภาษาขั้นตอน (เช่นหนังสยองขวัญเงียบ ๆ ของ C) คุณจะลงเอยด้วยการเขียนโค้ดในรูปแบบขั้นตอนที่ให้ความรู้สึกโบราณและล้าสมัย ฉันรู้ว่าการเขียนโปรแกรมเชิงวัตถุไม่ใช่เป้าหมายหลัก แต่จะเป็นการดีหากสามารถสรุปรายละเอียดออกเป็นประเภทต่างๆ และจัดให้มีการห่อหุ้ม

ความเรียบง่ายเพื่อผลประโยชน์ของคุณเอง

Go ได้รับการออกแบบให้เรียบง่ายและประสบความสำเร็จตามเป้าหมายนั้น มันถูกเขียนขึ้นสำหรับโปรแกรมเมอร์ที่อ่อนแอโดยใช้ภาษาเก่าเป็นเทมเพลต มาพร้อมเครื่องมือง่ายๆ ในการทำสิ่งง่ายๆ อ่านง่ายและใช้งานง่าย

มันละเอียดมาก ไม่น่าประทับใจ และไม่ดีสำหรับโปรแกรมเมอร์ที่ชาญฉลาด

ขอบคุณ เมอร์ซินวาลด์ สำหรับการแก้ไข

ที่มา: will.com

เพิ่มความคิดเห็น