Dlaczego projektowanie jest złe dla inteligentnych programistów

Przez ostatnie miesiące korzystałem z Go do wdrożeń. Proof of Concept (około.: kod do testowania funkcjonalności pomysłu) w wolnym czasie, częściowo w celu nauki samego języka programowania. Same programy są bardzo proste i nie są celem tego artykułu, ale wrażenia z korzystania z samego Go zasługują na kilka słów na ten temat. Idź obiecuje być (około.: artykuł napisany w 2015 r.) popularny język dla poważnego, skalowalnego kodu. Język został stworzony przez Google i jest aktywnie używany. Podsumowując, szczerze uważam, że projekt języka Go jest zły dla inteligentnych programistów.

Zaprojektowany dla słabych programistów?

Go jest bardzo łatwy do nauczenia, tak łatwy, że wprowadzenie zajęło mi jeden wieczór, po którym mogłem już produktywnie kodować. Książka, z której uczyłam się Go, nazywa się Wprowadzenie do programowania w Go (tłumaczenie), jest dostępny w Internecie. Książka, podobnie jak sam kod źródłowy Go, jest łatwa do odczytania, zawiera dobre przykłady kodu i zawiera około 150 stron, które można przeczytać za jednym razem. Ta prostota na pierwszy rzut oka działa odświeżająco, szczególnie w świecie programowania wypełnionym nadmiernie skomplikowaną technologią. Ale w końcu prędzej czy później pojawia się myśl: „Czy tak jest naprawdę?”

Google twierdzi, że prostota Go jest jego zaletą, a język został zaprojektowany z myślą o maksymalnej produktywności w dużych zespołach, ale wątpię w to. Brakuje niektórych funkcji lub są one zbyt szczegółowe. A wszystko przez brak zaufania do deweloperów, przy założeniu, że nie są w stanie zrobić nic dobrze. To pragnienie prostoty było świadomą decyzją projektantów języka i aby w pełni zrozumieć, dlaczego była ona potrzebna, musimy zrozumieć motywację programistów i to, co próbowali osiągnąć w Go.

Dlaczego więc zrobiono to tak prosto? Oto kilka cytatów Roba Pike’a (około.: jeden ze współtwórców języka Go):

Kluczową kwestią jest to, że nasi programiści (około.: Googlerzy) nie są badaczami. Są to z reguły dość młodzi ludzie, przychodzą do nas po studiach, być może uczyli się Javy, C/C++ lub Pythona. Nie rozumieją świetnego języka, ale jednocześnie chcemy, żeby tworzyli dobre oprogramowanie. Dlatego ich język powinien być dla nich łatwy do zrozumienia i nauki.
 
Powinien być znajomy, z grubsza podobny do C. Programiści pracujący w Google wcześnie rozpoczynają karierę i znają głównie języki proceduralne, w szczególności rodzinę C. Wymóg szybkiej produktywności w nowym języku programowania oznacza, że ​​język ten nie powinien być zbyt radykalny.

Co? Więc Rob Pike w zasadzie mówi, że programiści w Google nie są aż tak dobrzy, dlatego stworzyli język dla idiotów (około.:głupi), żeby mogli coś zrobić. Jakie aroganckie spojrzenie na własnych kolegów? Zawsze wierzyłem, że programiści Google są wybierani spośród najzdolniejszych i najlepszych na Ziemi. Na pewno poradzą sobie z czymś trudniejszym?

Artefakty o nadmiernej prostocie

Prostota jest szczytnym celem każdego projektu, a próba stworzenia czegoś prostego jest trudna. Jednak próbując rozwiązać (a nawet wyrazić) złożone problemy, czasami potrzebne jest złożone narzędzie. Złożoność i zawiłość nie są najlepszymi cechami języka programowania, ale istnieje złoty środek, dzięki któremu język może tworzyć eleganckie abstrakcje, łatwe do zrozumienia i użycia.

Mało wyraziste

Ze względu na swoje przywiązanie do prostoty w Go brakuje konstrukcji, które w innych językach są postrzegane jako naturalne. Na początku może się to wydawać dobrym pomysłem, ale w praktyce skutkuje pełnym kodem. Powód powinien być oczywisty - programiści muszą mieć możliwość łatwego odczytania kodu innych osób, ale tak naprawdę te uproszczenia tylko pogarszają czytelność. W Go nie ma skrótów: albo dużo, albo nic.

Na przykład narzędzie konsoli odczytujące stdin lub plik z argumentów wiersza poleceń wyglądałoby następująco:

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

Chociaż ten kod również stara się być jak najbardziej ogólny, wymuszona gadatliwość Go przeszkadza, w wyniku czego rozwiązanie prostego problemu skutkuje powstaniem dużej ilości kodu.

Tutaj na przykład jest rozwiązanie tego samego problemu w 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 jest teraz bardziej czytelny? Oddam swój głos D. Jego kod jest znacznie bardziej czytelny, ponieważ jaśniej opisuje działania. D używa znacznie bardziej złożonych koncepcji (około.: alternatywne wywołanie funkcji и Patterns) niż w przykładzie Go, ale naprawdę nie ma nic skomplikowanego w ich zrozumieniu.

Piekło kopiowania

Popularną sugestią dotyczącą ulepszenia Go jest ogólność. Pomoże to przynajmniej uniknąć niepotrzebnego kopiowania kodu obsługującego wszystkie typy danych. Na przykład funkcję sumowania listy liczb całkowitych można zaimplementować jedynie poprzez skopiowanie i wklejenie jej podstawowej funkcji dla każdego typu liczb całkowitych; nie ma innego sposobu:

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

Ten przykład nie działa nawet w przypadku typów podpisanych. Takie podejście całkowicie narusza zasadę nie powtarzania się (DRY), jedna z najbardziej znanych i oczywistych zasad, której ignorowanie jest źródłem wielu błędów. Dlaczego Go to robi? To straszny aspekt języka.

Ten sam przykład na D:

import std.stdio;
import std.algorithm;

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

Prosto, elegancko i od razu na temat. Zastosowana tutaj funkcja to reduce dla typu szablonu i predykatu. Tak, to znowu jest bardziej skomplikowane niż wersja Go, ale nie jest tak trudne do zrozumienia dla inteligentnych programistów. Który przykład jest łatwiejszy w utrzymaniu i łatwiejszy do odczytania?

Proste obejście systemu

Wyobrażam sobie, że programiści Go, którzy to przeczytają, będą mieli pianę z ust i będą krzyczeć: „Robisz to źle!” Cóż, istnieje inny sposób na utworzenie ogólnej funkcji i typów, ale całkowicie psuje to system typów!

Spójrz na przykład głupiej poprawki językowej, aby obejść problem:

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

Ta realizacja Reduce zostało zapożyczone z artykułu Idiomatyczne rodzaje generyczne w Go (około.: Nie mogłem znaleźć tłumaczenia, będzie mi miło, jeśli w tym pomożesz). Cóż, jeśli jest to idiomatyczne, nie chciałbym widzieć nieidiomatycznego przykładu. Stosowanie interface{} - farsa, a w języku wystarczy jedynie ominąć pisanie. Jest to pusty interfejs i implementują go wszystkie typy, zapewniając każdemu pełną swobodę. Ten styl programowania jest strasznie brzydki, ale to nie wszystko. Tego typu akrobatyczne wyczyny wymagają użycia refleksji w czasie wykonywania. Nawet Rob Pike nie lubi osób, które to wykorzystują, jak wspomniał w jednym ze swoich raportów.

To potężne narzędzie, którego należy używać ostrożnie. Należy tego unikać, jeśli nie jest to absolutnie konieczne.

Zamiast tych bzdur wziąłbym szablony D. Jak ktoś może tak powiedzieć interface{} bardziej czytelny, a nawet bezpieczny dla typu?

Nieszczęścia zarządzania zależnościami

Go ma wbudowany system zależności oparty na popularnych dostawcach usług hostingowych VCS. Narzędzia dostarczane z Go wiedzą o tych usługach i mogą za jednym zamachem pobrać, skompilować i zainstalować z nich kod. Chociaż jest to świetne rozwiązanie, istnieje poważna wada wersjonowania! Tak, to prawda, że ​​możesz pobrać kod źródłowy z serwisów takich jak github czy bitbucket za pomocą narzędzi Go, ale nie możesz określić wersji. I znowu prostota kosztem użyteczności. Nie jestem w stanie zrozumieć logiki takiej decyzji.

Po zadaniu pytań dotyczących rozwiązania tego problemu, zespół programistów Go stworzył wątek forumowy, w którym opisano, w jaki sposób zamierzają obejść tę kwestię. Zalecono im, aby pewnego dnia po prostu skopiować całe repozytorium do projektu i pozostawić je „tak jak jest”. Co oni do cholery myślą? Mamy niesamowite systemy kontroli wersji ze świetnym tagowaniem i obsługą wersji, które twórcy Go ignorują i po prostu kopiują kod źródłowy.

Bagaż kulturowy Xi

Moim zdaniem Go stworzyli ludzie, którzy przez całe życie używali C i ci, którzy nie chcieli próbować czegoś nowego. Język można opisać jako C z dodatkowymi kołami (oryg.: koła treningowe). Nie ma w nim żadnych nowych pomysłów, poza wsparciem dla paralelizmu (co swoją drogą jest cudowne) i to jest wstyd. Masz doskonałą paralelę w ledwo użytecznym, kiepskim języku.

Kolejnym trzeszczącym problemem jest to, że Go jest językiem proceduralnym (jak cichy horror C). Kończy się na pisaniu kodu w stylu proceduralnym, który wydaje się archaiczny i przestarzały. Wiem, że programowanie obiektowe nie jest złotym rozwiązaniem, ale byłoby wspaniale móc streścić szczegóły w typach i zapewnić enkapsulację.

Prostota dla własnej korzyści

Go został zaprojektowany tak, aby był prosty i udało mu się osiągnąć ten cel. Został napisany dla słabych programistów, wykorzystując jako szablon stary język. Zawiera proste narzędzia do wykonywania prostych czynności. Jest łatwy do odczytania i łatwy w użyciu.

Jest niezwykle gadatliwy, niezbyt imponujący i szkodliwy dla inteligentnych programistów.

Dzięki Mersinvald do edycji

Źródło: www.habr.com

Dodaj komentarz