De ce Go Design este rău pentru programatorii inteligenți

În ultimele luni, am folosit Go pentru implementări. Dovada de concept (aproximativ: cod pentru a testa funcționalitatea unei idei) în timpul liber, parțial pentru a studia limbajul de programare în sine. Programele în sine sunt foarte simple și nu reprezintă scopul acestui articol, dar experiența utilizării Go în sine merită câteva cuvinte despre asta. Du-te promite să fie (aproximativ: articol scris în 2015) un limbaj popular pentru cod scalabil serios. Limbajul a fost creat de Google, unde este folosit în mod activ. În concluzie, cred sincer că designul limbajului Go este rău pentru programatorii inteligenți.

Proiectat pentru programatori slabi?

Go este foarte ușor de învățat, atât de ușor încât introducerea mi-a luat într-o seară, după care puteam deja să codificam productiv. Cartea pe care obișnuiam să învăț Go se numește O introducere în programare în Go (traducere), este disponibil online. Cartea, ca și codul sursă Go în sine, este ușor de citit, are exemple bune de cod și conține aproximativ 150 de pagini care pot fi citite dintr-o singură ședință. Această simplitate este revigorantă la început, mai ales într-o lume a programării plină de tehnologie prea complicată. Dar în cele din urmă, mai devreme sau mai târziu apare gândul: „Este chiar așa?”

Google susține că simplitatea lui Go este punctul său de vânzare, iar limbajul este conceput pentru productivitate maximă în echipe mari, dar mă îndoiesc. Există caracteristici care fie lipsesc, fie sunt excesiv de detaliate. Și totul din cauza lipsei de încredere în dezvoltatori, cu presupunerea că aceștia nu sunt capabili să facă nimic corect. Această dorință de simplitate a fost o decizie conștientă a designerilor limbajului și, pentru a înțelege pe deplin de ce a fost nevoie, trebuie să înțelegem motivația dezvoltatorilor și ce încercau să realizeze în Go.

Deci de ce a fost făcut atât de simplu? Iată câteva citate Rob Pike (aproximativ: unul dintre co-creatorii limbajului Go):

Punctul cheie aici este că programatorii noștri (aproximativ: Googlers) nu sunt cercetători. Sunt, de regulă, destul de tineri, vin la noi după ce au studiat, poate au studiat Java, sau C/C++, sau Python. Ei nu pot înțelege un limbaj grozav, dar în același timp dorim ca ei să creeze un software bun. De aceea, limba lor ar trebui să fie ușor de înțeles și de învățat pentru ei.
 
Ar trebui să fie familiar, aproximativ similar cu C. Programatorii care lucrează la Google își încep cariera devreme și sunt în mare parte familiarizați cu limbajele procedurale, în special cu familia C. Cerința de productivitate rapidă într-un nou limbaj de programare înseamnă că limbajul nu ar trebui să fie prea radical.

Ce? Deci Rob Pike spune practic că dezvoltatorii de la Google nu sunt atât de buni, de aceea au creat un limbaj pentru idioți (aproximativ: amuțit) pentru ca ei să poată face ceva. Ce fel de privire arogantă asupra propriilor colegi? Întotdeauna am crezut că dezvoltatorii Google sunt aleși cu atenție dintre cei mai strălucitori și cei mai buni de pe Pământ. Sigur se pot descurca cu ceva mai dificil?

Artefacte de o simplitate excesivă

A fi simplu este un obiectiv demn în orice design, iar încercarea de a face ceva simplu este dificilă. Cu toate acestea, atunci când încercați să rezolvați (sau chiar să exprimați) probleme complexe, uneori este nevoie de un instrument complex. Complexitatea și complexitatea nu sunt cele mai bune caracteristici ale unui limbaj de programare, dar există o cale de mijloc în care limbajul poate crea abstracții elegante, ușor de înțeles și de utilizat.

Nu foarte expresiv

Datorită angajamentului său față de simplitate, Go nu are construcții care sunt percepute ca naturale în alte limbi. Aceasta poate părea o idee bună la început, dar în practică are ca rezultat un cod prolios. Motivul pentru aceasta ar trebui să fie evident - trebuie să fie ușor pentru dezvoltatori să citească codul altor persoane, dar, de fapt, aceste simplificări dăunează doar lizibilității. Nu există abrevieri în Go: fie mult, fie nimic.

De exemplu, un utilitar de consolă care citește stdin sau un fișier din argumentele liniei de comandă ar arăta astfel:

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

Deși și acest cod încearcă să fie cât mai general posibil, verbozitatea forțată a lui Go iese în cale și, ca urmare, rezolvarea unei probleme simple are ca rezultat o cantitate mare de cod.

Iată, de exemplu, o soluție la aceeași problemă în 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);
    }
}

Și cine este mai ușor de citit acum? Îi voi da votul meu lui D. Codul lui este mult mai lizibil pentru că el descrie acțiunile mai clar. D folosește concepte mult mai complexe (aproximativ: apel de funcție alternativă и Șabloane) decât în ​​exemplul Go, dar nu este nimic complicat în a le înțelege.

Iadul de copiere

O sugestie populară pentru îmbunătățirea Go este generalitatea. Acest lucru va ajuta cel puțin la evitarea copierii inutile a codului pentru a accepta toate tipurile de date. De exemplu, o funcție pentru însumarea unei liste de numere întregi nu poate fi implementată în nici un alt mod decât prin copierea și lipirea funcției sale de bază pentru fiecare tip de întreg; nu există altă cale:

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

Și acest exemplu nici măcar nu funcționează pentru tipurile semnate. Această abordare încalcă complet principiul de a nu te repeta (uSCAT), unul dintre cele mai cunoscute și evidente principii, ignorând care este sursa multor erori. De ce face Go asta? Acesta este un aspect teribil al limbajului.

Același exemplu pe D:

import std.stdio;
import std.algorithm;

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

Simplu, elegant și direct la obiect. Funcția folosită aici este reduce pentru tipul de șablon și predicat. Da, acest lucru este din nou mai complicat decât versiunea Go, dar nu atât de greu de înțeles pentru programatorii inteligenți. Care exemplu este mai ușor de întreținut și mai ușor de citit?

Ocolire a sistemului de tip simplu

Îmi imaginez că programatorii Go care citesc asta vor face spumă la gură și vor țipa: „Ai greșit!” Ei bine, există o altă modalitate de a face o funcție și tipuri generice, dar rupe complet sistemul de tip!

Aruncă o privire la acest exemplu de remediere stupidă a limbajului pentru a rezolva problema:

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

Această implementare Reduce a fost împrumutat din articol Generice idiomatice în Go (aproximativ: Nu am putut găsi traducerea, mă bucur dacă mă ajutați cu asta). Ei bine, dacă este idiomatic, mi-ar plăcea să văd un exemplu non-idiomatic. Utilizare interface{} - o farsă, iar în limbaj este nevoie doar pentru a ocoli tastarea. Aceasta este o interfață goală și toate tipurile o implementează, permițând libertate completă pentru toată lumea. Acest stil de programare este teribil de urât și asta nu este tot. Operațiunile acrobatice ca acestea necesită utilizarea reflecției în timpul execuției. Nici măcar lui Rob Pike nu îi plac persoanele care abuzează de asta, așa cum a menționat într-unul dintre rapoartele sale.

Acesta este un instrument puternic care trebuie folosit cu prudență. Ar trebui evitată dacă nu este strict necesar.

Aș lua șabloanele D în loc de prostia asta. Cum poate cineva să spună asta interface{} mai lizibil sau chiar tip safe?

Necazurile managementului dependenței

Go are un sistem de dependență încorporat construit pe lângă furnizorii de găzduire populari VCS. Instrumentele care vin cu Go cunosc aceste servicii și pot descărca, construi și instala codul din ele dintr-o singură lovitură. Deși acest lucru este grozav, există un defect major în versiunea! Da, este adevărat că puteți obține codul sursă de la servicii precum github sau bitbucket folosind instrumentele Go, dar nu puteți specifica versiunea. Și din nou simplitatea în detrimentul utilității. Nu sunt în stare să înțeleg logica unei astfel de decizii.

După ce a pus întrebări despre o soluție la această problemă, echipa de dezvoltare Go a creat fir de forum, care a subliniat modul în care urmau să rezolve această problemă. Recomandarea lor a fost să copiați pur și simplu întregul depozit în proiectul dvs. într-o zi și să îl lăsați „ca atare”. La ce naiba se gândesc? Avem sisteme uimitoare de control al versiunilor cu etichetare excelentă și suport pentru versiuni pe care creatorii Go le ignoră și doar copiază codul sursă.

Bagaj cultural de la Xi

După părerea mea, Go a fost dezvoltat de oameni care au folosit C toată viața și de cei care nu au vrut să încerce ceva nou. Limbajul poate fi descris ca C cu roți suplimentare (orig.: roți de antrenament). Nu există idei noi în ea, cu excepția suportului pentru paralelism (care, apropo, este minunat) și asta este păcat. Ai un paralelism excelent într-un limbaj abia utilizabil, șchioapă.

O altă problemă care scârțâie este că Go este un limbaj procedural (cum ar fi groaza tăcută a lui C). Ajungi prin a scrie cod într-un stil procedural care se simte arhaic și depășit. Știu că programarea orientată pe obiecte nu este un glonț de argint, dar ar fi grozav să poți abstrage detaliile în tipuri și să oferi încapsulare.

Simplitate în beneficiul tău

Go a fost conceput pentru a fi simplu și reușește acest obiectiv. A fost scris pentru programatori slabi, folosind un limbaj vechi ca șablon. Vine complet cu instrumente simple pentru a face lucruri simple. Este ușor de citit și ușor de utilizat.

Este extrem de verbos, neimpresionant și rău pentru programatorii inteligenți.

mulțumesc mersinvald pentru editări

Sursa: www.habr.com

Adauga un comentariu