Hvorfor Go Design er dårlig for smarte programmerere

I løpet av de siste månedene har jeg brukt Go for implementeringer. Proof of Concept (ca.: kode for å teste funksjonaliteten til en idé) på fritiden, dels for å studere selve programmeringsspråket. Selve programmene er veldig enkle og er ikke hensikten med denne artikkelen, men selve opplevelsen av å bruke Go fortjener noen ord om det. Go lover å bli (ca.: artikkel skrevet i 2015) et populært språk for seriøs skalerbar kode. Språket er laget av Google, der det brukes aktivt. Bunnlinjen, jeg tror ærlig talt at utformingen av Go-språket er dårlig for smarte programmerere.

Designet for svake programmerere?

Go er veldig lett å lære, så lett at introduksjonen tok meg en kveld, hvoretter jeg allerede kunne kode produktivt. Boken jeg pleide å lære Go heter En introduksjon til programmering i farten (oversettelse), er den tilgjengelig på nettet. Boken er, i likhet med selve Go-kildekoden, lettlest, har gode kodeeksempler, og inneholder ca 150 sider som kan leses på én gang. Denne enkelheten er forfriskende i begynnelsen, spesielt i en programmeringsverden fylt med overkomplisert teknologi. Men til slutt, før eller siden dukker tanken opp: "Er det virkelig slik?"

Google hevder Gos enkelhet er salgsargumentet og språket er designet for maksimal produktivitet i store team, men jeg tviler på det. Det er funksjoner som enten mangler eller er for detaljerte. Og alt på grunn av mangel på tillit til utviklere, med antagelsen om at de ikke er i stand til å gjøre noe riktig. Dette ønsket om enkelhet var en bevisst avgjørelse fra språkets designere, og for å fullt ut forstå hvorfor det var nødvendig, må vi forstå motivasjonen til utviklerne og hva de prøvde å oppnå i Go.

Så hvorfor ble det gjort så enkelt? Her er et par sitater Rob Pike (ca.: en av medskaperne av Go-språket):

Nøkkelpunktet her er at våre programmerere (ca.: Googlere) er ikke forskere. De er som regel ganske unge, kom til oss etter å ha studert, kanskje de studerte Java, eller C/C++, eller Python. De kan ikke forstå et godt språk, men samtidig vil vi at de skal lage god programvare. Det er grunnen til at språket deres skal være lett for dem å forstå og lære.
 
Han burde være kjent, omtrent lik C. Programmerere som jobber hos Google starter karrieren tidlig og er for det meste kjent med prosedyrespråk, spesielt C-familien. Kravet om rask produktivitet i et nytt programmeringsspråk gjør at språket ikke skal være for radikalt.

Hva? Så Rob Pike sier i grunnen at utviklerne hos Google ikke er så gode, det er derfor de har laget et språk for idioter (ca.: dumbed down) slik at de er i stand til å gjøre noe. Hva slags arrogant blikk på dine egne kolleger? Jeg har alltid trodd at Googles utviklere er håndplukket blant de smarteste og beste på jorden. De kan sikkert takle noe vanskeligere?

Artefakter av overdreven enkelhet

Å være enkel er et verdig mål i ethvert design, og å prøve å lage noe enkelt er vanskelig. Men når man prøver å løse (eller til og med uttrykke) komplekse problemer, er det noen ganger nødvendig med et komplekst verktøy. Kompleksitet og forviklinger er ikke de beste egenskapene til et programmeringsspråk, men det er en mellomting der språket kan skape elegante abstraksjoner som er enkle å forstå og bruke.

Ikke veldig uttrykksfull

På grunn av sin forpliktelse til enkelhet, mangler Go konstruksjoner som oppfattes som naturlige på andre språk. Dette kan virke som en god idé i begynnelsen, men i praksis resulterer det i en detaljert kode. Årsaken til dette burde være åpenbar – det må være enkelt for utviklere å lese andres kode, men faktisk skader disse forenklingene bare lesbarheten. Det er ingen forkortelser i Go: enten mye eller ingenting.

For eksempel vil et konsollverktøy som leser stdin eller en fil fra kommandolinjeargumenter se slik ut:

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

Selv om denne koden også prøver å være så generell som mulig, kommer Gos påtvungne ordlyd i veien, og som et resultat vil løsning av et enkelt problem resultere i en stor mengde kode.

Her er for eksempel en løsning på samme problem i 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);
    }
}

Og hvem er mest lesbar nå? Jeg gir min stemme til D. Koden hans er mye mer lesbar siden han beskriver handlingene tydeligere. D bruker mye mer komplekse konsepter (ca.: alternativ funksjonsanrop и maler) enn i Go-eksemplet, men det er egentlig ikke noe komplisert med å forstå dem.

Helvete med kopiering

Et populært forslag for å forbedre Go er generalitet. Dette vil i det minste bidra til å unngå unødvendig kopiering av kode for å støtte alle datatyper. For eksempel kan en funksjon for å summere en liste over heltall implementeres på ingen annen måte enn ved å kopiere og lime inn dens grunnleggende funksjon for hver heltallstype; det er ingen annen måte:

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

Og dette eksemplet fungerer ikke engang for signerte typer. Denne tilnærmingen bryter fullstendig med prinsippet om ikke å gjenta deg selv (TØRR), et av de mest kjente og åpenbare prinsippene, og ignorerer som er kilden til mange feil. Hvorfor gjør Go dette? Dette er et forferdelig aspekt ved språket.

Samme eksempel på D:

import std.stdio;
import std.algorithm;

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

Enkelt, elegant og rett på sak. Funksjonen som brukes her er reduce for maltype og predikat. Ja, dette er igjen mer komplisert enn Go-versjonen, men ikke så vanskelig for smarte programmerere å forstå. Hvilket eksempel er lettere å vedlikeholde og lettere å lese?

Enkel type system bypass

Jeg ser for meg at Go-programmerere som leser dette vil fråde om munnen og skrike: «Du gjør det feil!» Vel, det er en annen måte å lage en generisk funksjon og typer på, men den bryter typesystemet fullstendig!

Ta en titt på dette eksemplet på en dum språkløsning for å omgå problemet:

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

Denne implementeringen Reduce ble lånt fra artikkelen Idiomatisk generikk i Go (ca.: Jeg kunne ikke finne oversettelsen, jeg vil være glad hvis du hjelper med dette). Vel, hvis det er idiomatisk, ville jeg hate å se et ikke-idiomatisk eksempel. Bruk interface{} - en farse, og på språket trengs det bare for å omgå skriving. Dette er et tomt grensesnitt og alle typer implementerer det, og gir full frihet for alle. Denne programmeringsstilen er fryktelig stygg, og det er ikke alt. Akrobatiske bragder som disse krever bruk av kjøretidsrefleksjon. Selv Rob Pike liker ikke individer som misbruker dette, som han nevnte i en av rapportene sine.

Dette er et kraftig verktøy som bør brukes med forsiktighet. Det bør unngås med mindre det er strengt nødvendig.

Jeg ville tatt D-maler i stedet for dette tullet. Hvordan kan noen si det interface{} mer lesbar eller til og med typesikker?

Ulempene til avhengighetsstyring

Go har et innebygd avhengighetssystem bygget på toppen av populære vertsleverandører VCS. Verktøyene som følger med Go kjenner til disse tjenestene og kan laste ned, bygge og installere kode fra dem med ett slag. Selv om dette er flott, er det en stor feil med versjonering! Ja, det er sant at du kan få kildekoden fra tjenester som github eller bitbucket ved å bruke Go-verktøy, men du kan ikke spesifisere versjonen. Og igjen enkelhet på bekostning av nytten. Jeg er ikke i stand til å forstå logikken i en slik beslutning.

Etter å ha stilt spørsmål om en løsning på dette problemet, opprettet Go-utviklingsteamet forumtråd, som skisserte hvordan de skulle komme rundt dette problemet. Deres anbefaling var å ganske enkelt kopiere hele depotet inn i prosjektet ditt en dag og la det være "som det er." Hva i helvete tenker de på? Vi har fantastiske versjonskontrollsystemer med flott tagging og versjonsstøtte som Go-skaperne ignorerer og bare kopierer kildekoden.

Kulturbagasje fra Xi

Etter min mening ble Go utviklet av folk som hadde brukt C hele livet og av de som ikke ville prøve noe nytt. Språket kan beskrives som C med ekstra hjul(orig.: treningshjul). Det er ingen nye ideer i den, bortsett fra støtte for parallellisme (som forresten er fantastisk) og dette er synd. Du har utmerket parallellisme i et knapt brukbart, haltende språk.

Et annet knirkende problem er at Go er et prosessspråk (som den tause skrekk av C). Du ender opp med å skrive kode i en prosedyrestil som føles arkaisk og utdatert. Jeg vet at objektorientert programmering ikke er en sølvkule, men det ville vært flott å kunne abstrahere detaljene til typer og gi innkapsling.

Enkelhet til din egen fordel

Go ble designet for å være enkelt, og det lykkes med det målet. Den ble skrevet for svake programmerere, og brukte et gammelt språk som mal. Den leveres komplett med enkle verktøy for å gjøre enkle ting. Den er lett å lese og enkel å bruke.

Det er ekstremt detaljert, lite imponerende og dårlig for smarte programmerere.

Takk mersinvald for redigeringer

Kilde: www.habr.com

Legg til en kommentar