Prečo je Go Design zlý pre inteligentných programátorov

Počas posledných mesiacov som na implementácie používal Go. Proof of Concept (približne.: kód na testovanie funkčnosti nápadu) vo voľnom čase, čiastočne na štúdium samotného programovacieho jazyka. Samotné programy sú veľmi jednoduché a nie sú cieľom tohto článku, no samotná skúsenosť s používaním Go si zaslúži pár slov. Go sľubuje, že bude (približne.: článok napísaný v roku 2015) populárny jazyk pre seriózny škálovateľný kód. Jazyk bol vytvorený spoločnosťou Google, kde sa aktívne používa. Zrátané a podčiarknuté, úprimne si myslím, že dizajn jazyka Go je pre šikovných programátorov zlý.

Určené pre slabých programátorov?

Go sa učí veľmi ľahko, tak ľahko, že úvod mi trval jeden večer, po ktorom som už mohol produktívne kódovať. Kniha, ktorú som sa naučil Go, sa volá Úvod do programovania v Go (preklad), je k dispozícii online. Kniha, rovnako ako samotný zdrojový kód Go, sa ľahko číta, má dobré príklady kódu a obsahuje približne 150 strán, ktoré sa dajú prečítať jedným ťahom. Táto jednoduchosť je spočiatku osviežujúca, najmä vo svete programovania, ktorý je plný príliš komplikovaných technológií. Ale nakoniec sa skôr či neskôr objaví myšlienka: „Je to naozaj tak?

Google tvrdí, že jednoduchosť Go je jeho predajnou stránkou a jazyk je navrhnutý pre maximálnu produktivitu vo veľkých tímoch, ale pochybujem o tom. Existujú funkcie, ktoré buď chýbajú, alebo sú príliš podrobné. A to všetko kvôli nedostatku dôvery vo vývojárov, s predpokladom, že nie sú schopní urobiť nič správne. Táto túžba po jednoduchosti bola vedomým rozhodnutím dizajnérov jazyka, a aby sme plne pochopili, prečo to bolo potrebné, musíme pochopiť motiváciu vývojárov a to, čo sa v Go snažili dosiahnuť.

Prečo to teda bolo také jednoduché? Tu je pár citátov Rob Pike (približne.: jeden zo spolutvorcov jazyka Go):

Kľúčovým bodom je, že naši programátori (približne.: zamestnanci spoločnosti Google) nie sú výskumníci. Sú spravidla dosť mladí, prichádzajú k nám po štúdiu, možno študovali Javu, C/C++ alebo Python. Nerozumejú skvelému jazyku, no zároveň chceme, aby vytvorili dobrý softvér. Preto by ich jazyk mal byť pre nich ľahko zrozumiteľný a učený.
 
Mal by byť známy, zhruba povedané podobne ako C. Programátori pracujúci v Google začínajú svoju kariéru skoro a väčšinou poznajú procedurálne jazyky, najmä rodinu C. Požiadavka rýchlej produktivity v novom programovacom jazyku znamená, že jazyk by nemal byť príliš radikálny.

Čo? Takže Rob Pike v podstate hovorí, že vývojári v Google nie sú takí dobrí, preto vytvorili jazyk pre idiotov (približne.: hlúpy), aby boli schopní niečo urobiť. Aký arogantný pohľad na vlastných kolegov? Vždy som veril, že vývojári Google sú vyberaní z tých najchytrejších a najlepších na Zemi. Určite zvládnu aj niečo náročnejšie?

Artefakty nadmernej jednoduchosti

Byť jednoduchý je hodný cieľ v akomkoľvek dizajne a snažiť sa urobiť niečo jednoduché je ťažké. Keď sa však pokúšame vyriešiť (alebo dokonca vyjadriť) zložité problémy, niekedy je potrebný komplexný nástroj. Zložitosť a zložitosť nie sú najlepšie črty programovacieho jazyka, ale existuje šťastné médium, v ktorom jazyk dokáže vytvárať elegantné abstrakcie, ktoré sú ľahko pochopiteľné a použiteľné.

Nie veľmi expresívne

Vďaka svojmu záväzku k jednoduchosti nemá Go konštrukty, ktoré sú v iných jazykoch vnímané ako prirodzené. Na prvý pohľad sa to môže zdať ako dobrý nápad, ale v praxi to vedie k podrobnému kódu. Dôvod by mal byť zrejmý – pre vývojárov musí byť ľahké čítať kód iných ľudí, ale v skutočnosti tieto zjednodušenia len poškodzujú čitateľnosť. V Go nie sú žiadne skratky: buď veľa, alebo nič.

Napríklad pomôcka konzoly, ktorá číta stdin alebo súbor z argumentov príkazového riadku, by vyzerala takto:

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

Aj keď sa tento kód tiež snaží byť čo najvšeobecnejší, vynútená mnohorečnosť Go prekáža a výsledkom je, že vyriešenie jednoduchého problému má za následok veľké množstvo kódu.

Tu je napríklad riešenie rovnakého problému v 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);
    }
}

A kto je teraz čitateľnejší? Dám svoj hlas D. Jeho kód je oveľa čitateľnejší, pretože jasnejšie popisuje úkony. D používa oveľa zložitejšie koncepty (približne.: volanie alternatívnej funkcie и Šablóny), ako v príklade Go, ale na ich porozumení v skutočnosti nie je nič zložité.

Pekelné kopírovanie

Obľúbeným návrhom na zlepšenie Go je všeobecnosť. To aspoň pomôže vyhnúť sa zbytočnému kopírovaniu kódu na podporu všetkých typov údajov. Napríklad funkciu na sčítanie zoznamu celých čísel nemožno implementovať iným spôsobom, ako kopírovaním a prilepením jej základnej funkcie pre každý typ celého čísla; neexistuje žiadny iný spôsob:

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

A tento príklad nefunguje ani pre podpísané typy. Tento prístup úplne porušuje zásadu neopakovať sa (DRY), jeden z najznámejších a najzrejmejších princípov, ktorého ignorovanie je zdrojom mnohých chýb. Prečo to Go robí? Toto je hrozný aspekt jazyka.

Rovnaký príklad na D:

import std.stdio;
import std.algorithm;

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

Jednoduché, elegantné a priamo k veci. Tu je použitá funkcia reduce pre typ šablóny a predikát. Áno, toto je opäť zložitejšie ako verzia Go, ale pre inteligentných programátorov to nie je také ťažké na pochopenie. Ktorý príklad sa ľahšie udržiava a ľahšie číta?

Jednoduchý typ systémového bypassu

Predstavujem si, že programátori Go, ktorí to čítajú, budú mať penu z úst a budú kričať: "Robíš to zle!" Existuje aj iný spôsob, ako vytvoriť generickú funkciu a typy, ale úplne rozbije systém typov!

Pozrite si tento príklad hlúpej jazykovej opravy, aby ste problém vyriešili:

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

Táto implementácia Reduce bol požičaný z článku Idiomatické generiká v Go (približne.: Preklad som nenašiel, budem rád, ak mi s tým pomôžete). No, ak je to idiomatické, nerád by som videl neidiomatický príklad. Použitie interface{} - fraška a v jazyku je potrebná iba na obídenie písania. Toto je prázdne rozhranie a implementujú ho všetky typy, čo umožňuje úplnú slobodu pre každého. Tento štýl programovania je strašne škaredý a to nie je všetko. Akrobatické výkony, ako sú tieto, vyžadujú použitie odrazu behu. Ani Rob Pike nemá rád jedincov, ktorí toto zneužívajú, ako spomenul v jednej zo svojich reportáží.

Toto je výkonný nástroj, ktorý by sa mal používať opatrne. Je potrebné sa mu vyhnúť, pokiaľ to nie je nevyhnutne potrebné.

Namiesto týchto nezmyslov by som bral D šablóny. Ako to môže niekto povedať interface{} čitateľnejšie alebo dokonca bezpečné pre typ?

Útrapy manažmentu závislostí

Go má vstavaný systém závislosti postavený na populárnych poskytovateľoch hostingu VCS. Nástroje, ktoré sú súčasťou Go, vedia o týchto službách a dokážu z nich jedným ťahom stiahnuť, zostaviť a nainštalovať kód. Aj keď je to skvelé, existuje veľká chyba pri vytváraní verzií! Áno, je pravda, že zdrojový kód môžete získať zo služieb ako github alebo bitbucket pomocou nástrojov Go, ale nemôžete určiť verziu. A opäť jednoduchosť na úkor užitočnosti. Nie som schopný pochopiť logiku takéhoto rozhodnutia.

Po položení otázok o riešení tohto problému vytvoril vývojový tím Go vlákno na fóre, ktorá načrtla, ako chcú tento problém obísť. Ich odporúčaním bolo jedného dňa jednoducho skopírovať celé úložisko do vášho projektu a nechať ho „tak, ako je“. Čo si do pekla myslia? Máme úžasné systémy na správu verzií so skvelým značkovaním a podporou verzií, ktoré tvorcovia Go ignorujú a len skopírujú zdrojový kód.

Kultúrna batožina od Xi

Podľa mňa Go vyvinuli ľudia, ktorí celý život používali C a tí, ktorí nechceli skúšať niečo nové. Jazyk možno opísať ako C s extra kolieskami (orig.: tréningové kolieska). Nie sú v ňom žiadne nové nápady, okrem podpory paralelizmu (ktorý je mimochodom úžasný) a to je škoda. Máte vynikajúci paralelizmus v sotva použiteľnom, chabom jazyku.

Ďalším škrípajúcim problémom je, že Go je procedurálny jazyk (ako tichý horor C). Skončíte pri písaní kódu v procedurálnom štýle, ktorý vám pripadá archaický a zastaraný. Viem, že objektovo orientované programovanie nie je strieborná guľka, ale bolo by skvelé vedieť abstrahovať detaily do typov a poskytnúť zapuzdrenie.

Jednoduchosť vo váš vlastný prospech

Go bol navrhnutý tak, aby bol jednoduchý a podarilo sa mu dosiahnuť tento cieľ. Bol napísaný pre slabých programátorov s použitím starého jazyka ako šablóny. Dodáva sa s jednoduchými nástrojmi na jednoduché veci. Ľahko sa číta a ľahko sa používa.

Je extrémne podrobný, nevýrazný a nevhodný pre inteligentných programátorov.

Vďaka mersinvald pre úpravy

Zdroj: hab.com

Pridať komentár