Mengapa Go Design Buruk bagi Pemrogram Cerdas

Selama beberapa bulan terakhir saya telah menggunakan Go untuk implementasi. Bukti dari konsep (kira-kira.: kode untuk menguji fungsionalitas suatu ide) di waktu luangnya, sebagian untuk mempelajari bahasa pemrograman itu sendiri. Programnya sendiri sangat sederhana dan bukan merupakan tujuan artikel ini, namun pengalaman menggunakan Go sendiri layak untuk dijelaskan lebih lanjut. Pergi berjanji untuk menjadi (kira-kira.: artikel yang ditulis pada tahun 2015) bahasa populer untuk kode skalabel yang serius. Bahasa ini dibuat oleh Google, dan digunakan secara aktif. Intinya, sejujurnya menurut saya desain bahasa Go buruk bagi programmer yang cerdas.

Dirancang untuk programmer yang lemah?

Go sangat mudah dipelajari, begitu mudahnya sehingga perkenalannya memakan waktu satu malam, setelah itu saya sudah bisa membuat kode secara produktif. Buku yang biasa saya pelajari Go berjudul Pengantar Pemrograman di Go (terjemahan), ini tersedia online. Buku ini, seperti kode sumber Go itu sendiri, mudah dibaca, memiliki contoh kode yang bagus, dan berisi sekitar 150 halaman yang dapat dibaca sekaligus. Kesederhanaan ini menyegarkan pada awalnya, terutama di dunia pemrograman yang dipenuhi dengan teknologi yang terlalu rumit. Namun pada akhirnya, cepat atau lambat muncul pemikiran: β€œBenarkah demikian?”

Google mengklaim kesederhanaan Go adalah nilai jualnya dan bahasanya dirancang untuk produktivitas maksimum dalam tim besar, tapi saya meragukannya. Ada fitur yang hilang atau terlalu detail. Dan semua itu karena kurangnya kepercayaan pada pengembang, dengan asumsi bahwa mereka tidak mampu melakukan sesuatu dengan benar. Keinginan akan kesederhanaan ini merupakan keputusan yang disengaja oleh para desainer bahasa tersebut, dan untuk memahami sepenuhnya mengapa hal itu diperlukan, kita harus memahami motivasi para pengembang dan apa yang ingin mereka capai di Go.

Lalu mengapa dibuat sesederhana itu? Berikut adalah beberapa kutipan Rob Pike (kira-kira.: salah satu pencipta bahasa Go):

Poin kuncinya di sini adalah programmer kami (kira-kira.: Karyawan Google) bukan peneliti. Mereka biasanya masih sangat muda, datang kepada kami setelah belajar, mungkin mereka mempelajari Java, atau C/C++, atau Python. Mereka tidak dapat memahami bahasa yang bagus, namun pada saat yang sama kami ingin mereka membuat perangkat lunak yang bagus. Itulah sebabnya bahasa mereka harus mudah dipahami dan dipelajari.
 
Dia seharusnya familiar, secara kasar mirip dengan C. Pemrogram yang bekerja di Google memulai karir mereka sejak dini dan sebagian besar akrab dengan bahasa prosedural, khususnya keluarga C. Persyaratan untuk produktivitas yang cepat dalam bahasa pemrograman baru berarti bahasa tersebut tidak boleh terlalu radikal.

Apa? Jadi Rob Pike pada dasarnya mengatakan bahwa pengembang di Google tidak begitu bagus, itu sebabnya mereka menciptakan bahasa untuk orang bodoh (kira-kira.: bodoh) sehingga mereka mampu melakukan sesuatu. Pandangan arogan seperti apa terhadap rekan kerja sendiri? Saya selalu percaya bahwa pengembang Google dipilih dari orang-orang paling cerdas dan terbaik di muka bumi. Tentunya mereka bisa mengatasi sesuatu yang lebih sulit?

Artefak kesederhanaan yang berlebihan

Menjadi sederhana adalah tujuan yang berharga dalam desain apa pun, dan mencoba membuat sesuatu yang sederhana itu sulit. Namun, ketika mencoba memecahkan (atau bahkan mengungkapkan) masalah yang kompleks, terkadang diperlukan alat yang kompleks. Kompleksitas dan kerumitan bukanlah fitur terbaik dari sebuah bahasa pemrograman, namun ada jalan tengah di mana bahasa tersebut dapat menciptakan abstraksi elegan yang mudah dipahami dan digunakan.

Tidak terlalu ekspresif

Karena komitmennya terhadap kesederhanaan, Go tidak memiliki konstruksi yang dianggap wajar dalam bahasa lain. Ini mungkin tampak seperti ide yang bagus pada awalnya, namun dalam praktiknya ini menghasilkan kode yang panjang. Alasannya jelas - pengembang harus mudah membaca kode orang lain, namun kenyataannya penyederhanaan ini hanya mengganggu keterbacaan. Tidak ada singkatan di Go: banyak atau tidak sama sekali.

Misalnya, utilitas konsol yang membaca stdin atau file dari argumen baris perintah akan terlihat seperti ini:

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

Meskipun kode ini juga mencoba untuk menjadi seumum mungkin, verbositas Go yang dipaksakan menghalanginya, dan sebagai hasilnya, penyelesaian masalah sederhana menghasilkan sejumlah besar kode.

Di sini, misalnya, adalah solusi untuk masalah yang sama 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);
    }
}

Dan siapa yang lebih mudah dibaca sekarang? Saya akan memberikan suara saya kepada D. Kodenya jauh lebih mudah dibaca karena dia menjelaskan tindakannya dengan lebih jelas. D menggunakan konsep yang jauh lebih kompleks (kira-kira.: panggilan fungsi alternatif ΠΈ Pola) dibandingkan contoh Go, namun sebenarnya tidak ada yang rumit dalam memahaminya.

Sungguh menyalin

Saran populer untuk meningkatkan Go adalah bersifat umum. Setidaknya ini akan membantu menghindari penyalinan kode yang tidak perlu untuk mendukung semua tipe data. Misalnya, fungsi untuk menjumlahkan daftar bilangan bulat dapat diimplementasikan hanya dengan menyalin-menempelkan fungsi dasarnya untuk setiap jenis bilangan bulat; tidak ada cara lain:

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

Dan contoh ini bahkan tidak berfungsi untuk tipe yang ditandatangani. Pendekatan ini sepenuhnya melanggar prinsip untuk tidak mengulangi diri sendiri (KERING), salah satu prinsip yang paling terkenal dan jelas, mengabaikan prinsip yang merupakan sumber banyak kesalahan. Mengapa Go melakukan ini? Ini adalah aspek bahasa yang buruk.

Contoh yang sama di D:

import std.stdio;
import std.algorithm;

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

Sederhana, elegan, dan langsung pada intinya. Fungsi yang digunakan disini adalah reduce untuk jenis template dan predikat. Ya, ini sekali lagi lebih rumit daripada versi Go, tetapi tidak terlalu sulit untuk dipahami oleh programmer yang cerdas. Contoh manakah yang lebih mudah dipelihara dan dibaca?

Bypass sistem tipe sederhana

Saya membayangkan programmer Go yang membaca ini akan mengeluarkan busa dari mulutnya dan berteriak, "Kamu salah melakukannya!" Ya, ada cara lain untuk membuat fungsi dan tipe generik, tetapi itu benar-benar merusak sistem tipe!

Lihatlah contoh perbaikan bahasa bodoh ini untuk mengatasi masalah tersebut:

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

Implementasi ini Reduce dipinjam dari artikel tersebut Obat generik idiomatik di Go (kira-kira.: Saya tidak dapat menemukan terjemahannya, saya akan senang jika Anda membantu dalam hal ini). Nah, jika itu idiomatis, saya tidak suka melihat contoh non-idiomatis. Penggunaan interface{} - sebuah lelucon, dan dalam bahasa itu hanya diperlukan untuk melewati pengetikan. Ini adalah antarmuka kosong dan semua tipe mengimplementasikannya, memungkinkan kebebasan penuh bagi semua orang. Gaya pemrograman ini sangat jelek, dan bukan itu saja. Prestasi akrobatik seperti ini memerlukan penggunaan refleksi runtime. Bahkan Rob Pike tidak menyukai individu yang menyalahgunakan hal ini, seperti yang dia sebutkan dalam salah satu laporannya.

Ini adalah alat yang ampuh yang harus digunakan dengan hati-hati. Hal ini harus dihindari kecuali benar-benar diperlukan.

Saya akan mengambil template D daripada omong kosong ini. Bagaimana orang bisa berkata seperti itu interface{} lebih mudah dibaca atau bahkan mengetik dengan aman?

Masalah Manajemen Ketergantungan

Go memiliki sistem ketergantungan bawaan yang dibangun di atas penyedia hosting populer VCS. Alat yang disertakan dengan Go mengetahui tentang layanan ini dan dapat mengunduh, membuat, dan menginstal kode dari layanan tersebut dalam satu gerakan. Meskipun ini bagus, ada kelemahan besar dalam pembuatan versi! Ya, memang benar Anda bisa mendapatkan kode sumber dari layanan seperti github atau bitbucket menggunakan alat Go, tetapi Anda tidak dapat menentukan versinya. Dan sekali lagi kesederhanaan dengan mengorbankan kegunaan. Saya tidak dapat memahami logika keputusan seperti itu.

Setelah mengajukan pertanyaan tentang solusi masalah ini, tim pengembangan Go membuat utas forum, yang menguraikan bagaimana mereka akan mengatasi masalah ini. Rekomendasi mereka adalah menyalin seluruh repositori ke dalam proyek Anda suatu hari nanti dan membiarkannya β€œsebagaimana adanya.” Apa yang mereka pikirkan? Kami memiliki sistem kontrol versi yang luar biasa dengan penandaan yang bagus dan dukungan versi yang diabaikan oleh pembuat Go dan hanya menyalin kode sumbernya.

Bagasi budaya dari Xi

Menurut pendapat saya, Go dikembangkan oleh orang-orang yang telah menggunakan C sepanjang hidup mereka dan oleh mereka yang tidak ingin mencoba sesuatu yang baru. Bahasanya dapat digambarkan sebagai C dengan roda tambahan (asal.: roda pelatihan). Tidak ada ide-ide baru di dalamnya, kecuali dukungan terhadap paralelisme (yang, omong-omong, luar biasa) dan ini memalukan. Anda memiliki paralelisme yang sangat baik dalam bahasa yang sulit digunakan dan membosankan.

Masalah lain yang muncul adalah bahwa Go adalah bahasa prosedural (seperti horor diam dari C). Anda akhirnya menulis kode dengan gaya prosedural yang terasa kuno dan ketinggalan jaman. Saya tahu pemrograman berorientasi objek bukanlah solusi terbaik, tetapi akan sangat bagus jika dapat mengabstraksikan detail ke dalam tipe dan menyediakan enkapsulasi.

Kesederhanaan untuk keuntungan Anda sendiri

Go dirancang untuk menjadi sederhana dan berhasil mencapai tujuan tersebut. Itu ditulis untuk pemrogram yang lemah, menggunakan bahasa lama sebagai templat. Muncul lengkap dengan alat sederhana untuk melakukan hal-hal sederhana. Sangat mudah dibaca dan mudah digunakan.

Ini sangat bertele-tele, tidak mengesankan, dan buruk bagi pemrogram cerdas.

Terima kasih mersinvald untuk pengeditan

Sumber: www.habr.com

Tambah komentar