Perché il Go Design è dannoso per i programmatori intelligenti

Negli ultimi mesi ho utilizzato Go per le implementazioni. Verifica teorica (ca.: codice per testare la funzionalità di un'idea) nel tempo libero, anche per studiare il linguaggio di programmazione stesso. I programmi stessi sono molto semplici e non sono lo scopo di questo articolo, ma l'esperienza di utilizzo di Go merita qualche parola a riguardo. Go promette di essere (ca.: articolo scritto nel 2015) un linguaggio popolare per codice serio e scalabile. La lingua è stata creata da Google, dove viene utilizzata attivamente. In conclusione, onestamente penso che il design del linguaggio Go sia dannoso per i programmatori intelligenti.

Progettato per programmatori deboli?

Go è molto facile da imparare, così facile che l'introduzione mi ha richiesto una sera, dopodiché potevo già programmare in modo produttivo. Il libro che ho usato per imparare il Go si intitola Un'introduzione alla programmazione in Go (traduzione), è disponibile online. Il libro, come lo stesso codice sorgente di Go, è facile da leggere, contiene buoni esempi di codice e contiene circa 150 pagine che possono essere lette in una sola seduta. Questa semplicità all'inizio è rinfrescante, specialmente in un mondo di programmazione pieno di tecnologia eccessivamente complicata. Ma alla fine, prima o poi sorge il pensiero: "È davvero così?"

Google afferma che la semplicità di Go è il suo punto di forza e che il linguaggio è progettato per la massima produttività in team di grandi dimensioni, ma ne dubito. Ci sono funzionalità che mancano o sono eccessivamente dettagliate. E tutto a causa della mancanza di fiducia negli sviluppatori, partendo dal presupposto che non siano in grado di fare nulla di giusto. Questo desiderio di semplicità è stata una decisione consapevole da parte dei progettisti del linguaggio e, per comprendere appieno il motivo per cui era necessario, dobbiamo comprendere la motivazione degli sviluppatori e ciò che stavano cercando di ottenere in Go.

Allora perché è stato reso così semplice? Ecco un paio di citazioni Rob Pike (ca.: uno dei co-creatori della lingua Go):

Il punto chiave qui è che i nostri programmatori (ca.: Googler) non sono ricercatori. Di solito sono piuttosto giovani, vengono da noi dopo aver studiato, forse hanno studiato Java, o C/C++, o Python. Non riescono a capire una grande lingua, ma allo stesso tempo vogliamo che creino un buon software. Ecco perché la loro lingua dovrebbe essere facile da capire e da imparare.
 
Dovrebbe essere familiare, grosso modo simile a C. I programmatori che lavorano in Google iniziano presto la loro carriera e hanno per lo più familiarità con i linguaggi procedurali, in particolare la famiglia C. L'esigenza di una rapida produttività in un nuovo linguaggio di programmazione significa che il linguaggio non dovrebbe essere troppo radicale.

Che cosa? Quindi Rob Pike sta sostanzialmente dicendo che gli sviluppatori di Google non sono così bravi, ecco perché hanno creato un linguaggio per idioti (ca.: ammutolito) in modo che siano in grado di fare qualcosa. Che tipo di sguardo arrogante verso i tuoi colleghi? Ho sempre creduto che gli sviluppatori di Google siano selezionati con cura tra i più brillanti e migliori sulla Terra. Sicuramente possono gestire qualcosa di più difficile?

Artefatti di eccessiva semplicità

Essere semplici è un obiettivo degno di qualsiasi progetto e cercare di realizzare qualcosa di semplice è difficile. Tuttavia, quando si cerca di risolvere (o addirittura esprimere) problemi complessi, a volte è necessario uno strumento complesso. La complessità e la complessità non sono le caratteristiche migliori di un linguaggio di programmazione, ma esiste una via di mezzo in cui il linguaggio può creare astrazioni eleganti facili da comprendere e utilizzare.

Non molto espressivo

A causa del suo impegno per la semplicità, Go manca di costrutti percepiti come naturali in altre lingue. All'inizio può sembrare una buona idea, ma in pratica il risultato è un codice dettagliato. La ragione di ciò dovrebbe essere ovvia: deve essere facile per gli sviluppatori leggere il codice di altre persone, ma in realtà queste semplificazioni danneggiano solo la leggibilità. Non ci sono abbreviazioni in Go: o molto o niente.

Ad esempio, un'utilità della console che legge stdin o un file dagli argomenti della riga di comando sarebbe simile a questa:

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

Sebbene questo codice cerchi di essere il più generale possibile, la verbosità forzata di Go si intromette e, di conseguenza, la risoluzione di un problema semplice risulta in una grande quantità di codice.

Ecco, ad esempio, una soluzione allo stesso problema in 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);
    }
}

E chi è più leggibile adesso? Darò il mio voto a D. Il suo codice è molto più leggibile poiché descrive le azioni in modo più chiaro. D utilizza concetti molto più complessi (ca.: chiamata di funzione alternativa и Modelli) rispetto all'esempio Go, ma non c'è davvero nulla di complicato nel capirli.

L'inferno della copia

Un suggerimento popolare per migliorare Go è la generalità. Ciò aiuterà almeno a evitare la copia non necessaria del codice per supportare tutti i tipi di dati. Ad esempio, una funzione per sommare un elenco di numeri interi non può essere implementata altrimenti che copiando e incollando la sua funzione di base per ciascun tipo di numero intero; non esiste altro modo:

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

E questo esempio non funziona nemmeno per i tipi firmati. Questo approccio viola completamente il principio di non ripetersi (ASCIUTTO), uno dei principi più famosi ed evidenti, il cui mancato rispetto è fonte di numerosi errori. Perché Go fa questo? Questo è un aspetto terribile del linguaggio.

Stesso esempio su D:

import std.stdio;
import std.algorithm;

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

Semplice, elegante e dritto al punto. La funzione utilizzata qui è reduce per il tipo di modello e il predicato. Sì, anche questo è più complicato della versione Go, ma non così difficile da capire per i programmatori intelligenti. Quale esempio è più facile da mantenere e più facile da leggere?

Bypass del sistema di tipo semplice

Immagino che i programmatori Go che leggeranno questo avranno la bava alla bocca e urleranno: "Stai sbagliando!" Bene, c'è un altro modo per creare una funzione e dei tipi generici, ma rompe completamente il sistema dei tipi!

Dai un'occhiata a questo esempio di una stupida correzione del linguaggio per aggirare il 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)
}

Questa implementazione Reduce è stato preso in prestito dall'articolo Generici idiomatici in Go (ca.: Non sono riuscito a trovare la traduzione, sarò felice se mi aiutate). Beh, se è idiomatico, detesterei vedere un esempio non idiomatico. Utilizzo interface{} - una farsa, e nella lingua è necessario solo evitare la digitazione. Questa è un'interfaccia vuota e tutti i tipi la implementano, consentendo completa libertà a tutti. Questo stile di programmazione è terribilmente brutto e non è tutto. Azioni acrobatiche come queste richiedono l'uso della riflessione in fase di esecuzione. Anche a Rob Pike non piacciono le persone che ne abusano, come ha menzionato in uno dei suoi rapporti.

Questo è uno strumento potente che dovrebbe essere usato con cautela. Dovrebbe essere evitato se non strettamente necessario.

Prenderei i modelli D invece di queste sciocchezze. Come si può dire una cosa del genere? interface{} più leggibile o addirittura sicuro da scrivere?

I problemi della gestione delle dipendenze

Go ha un sistema di dipendenze integrato basato sui provider di hosting più diffusi VCS. Gli strumenti forniti con Go conoscono questi servizi e possono scaricare, creare e installare il codice da essi in un colpo solo. Anche se questo è fantastico, c'è un grosso difetto con il controllo delle versioni! Sì, è vero che puoi ottenere il codice sorgente da servizi come github o bitbucket utilizzando gli strumenti Go, ma non puoi specificare la versione. E ancora la semplicità a scapito dell'utilità. Non riesco a comprendere la logica di una simile decisione.

Dopo aver posto domande su una soluzione a questo problema, il team di sviluppo di Go ha creato discussione del forum, che delineava come avrebbero aggirato questo problema. Il loro consiglio era semplicemente di copiare un giorno l’intero repository nel tuo progetto e lasciarlo “così com’è”. A che diavolo stanno pensando? Disponiamo di straordinari sistemi di controllo della versione con ottimi tag e supporto della versione che i creatori di Go ignorano e copiano semplicemente il codice sorgente.

Bagaglio culturale di Xi

Secondo me, Go è stato sviluppato da persone che avevano usato il C per tutta la vita e da coloro che non volevano provare qualcosa di nuovo. Il linguaggio può essere descritto come C con ruote extra(orig.: ruote di formazione). Non ci sono nuove idee, ad eccezione del supporto per il parallelismo (che, tra l'altro, è meraviglioso) e questo è un peccato. Hai un eccellente parallelismo in un linguaggio poco utilizzabile e noioso.

Un altro problema scricchiolante è che Go è un linguaggio procedurale (come l'orrore silenzioso del C). Finisci per scrivere codice in uno stile procedurale che sembra arcaico e obsoleto. So che la programmazione orientata agli oggetti non è una soluzione miracolosa, ma sarebbe fantastico poter astrarre i dettagli in tipi e fornire l'incapsulamento.

La semplicità a tuo vantaggio

Go è stato progettato per essere semplice e riesce a raggiungere questo obiettivo. È stato scritto per programmatori deboli, utilizzando un vecchio linguaggio come modello. Viene fornito completo di strumenti semplici per fare cose semplici. È facile da leggere e facile da usare.

È estremamente prolisso, insignificante e dannoso per i programmatori intelligenti.

Grazie Mersinvald per le modifiche

Fonte: habr.com

Aggiungi un commento