Чаму дызайн Go дрэнны для разумных праграмістаў

На працягу апошніх месяцаў я выкарыстоўваю Go для імплементацый Доказ канцэпцыі (заўв.: код для праверкі працаздольнасці ідэі) у вольны час, збольшага для вывучэння самой мовы праграмавання. Праграмы самі па сабе вельмі простыя і не з'яўляюцца мэтай напісання артыкула, але сам досвед выкарыстання Go заслугоўвае таго, каб сказаць пра яго пару слоў. Go абяцае быць (заўв.: артыкул напісаны ў 2015) масавай мовай для сур'ёзнага маштабуемага кода. Мова створаны ў Google, у якім актыўна ім карыстаюцца. Падвядучы рысу, я шчыра лічу, што дызайн мовы Go дрэнны для разумных праграмістаў.

Створаны для слабых праграмістаў?

Go вельмі проста навучыцца, настолькі проста, што ўвядзенне заняло ў мяне адзін вечар, пасля чаго ўжо мог прадуктыўна пісаць код. Кніга па якой я вывучаў Go называецца An Introduction to Programming in Go (пераклад), яна даступная ў сетцы. Кнігу, як і сам зыходны код на Go, лёгка чытаць, у ёй ёсць добрыя прыклады кода, яна змяшчае каля 150 старонак, якія можна прачытаць за раз. Спачатку гэтая прастата дзейнічае асвяжальна, асабліва ў свеце праграмавання, поўнага пераўскладненых тэхналогій. Але ў выніку рана ці позна ўзнікае думка: "Ці так гэта насамрэч?"

Google сцвярджае, што прастата Go - гэта подкупающая рыса, і мова прызначаны для максімальнай прадуктыўнасці ў вялікіх камандах, але я сумняваюся ў гэтым. Ёсць фічы, якіх альбо бракуе, альбо яны празмеру падрабязныя. А ўсё з-за адсутнасці даверу да распрацоўшчыкаў, з здагадкай, што яны не ў стане зрабіць штосьці правільна. Гэтае імкненне да прастаты было свядомым рашэннем распрацоўнікаў мовы і, для таго, каб цалкам зразумець для чаго гэта было патрэбна, мы павінны зразумець матывацыю распрацоўнікаў і чаго яны дамагаліся ў Go.

Дык для чаго ж ён быў створаны такім простым? Вось пара цытат Роба Пайка (заўв.: адзін з суаўтараў мовы Go):

Ключавы момант тут, што нашы праграмісты (заўв.: гуглеры) не даследчыкі. Яны, як правіла, вельмі маладыя, ідуць да нас пасля вучобы, магчыма вывучалі Java, ці C/C++, ці Python. Яны не ў стане зразумець выбітную мову, але ў той жа час мы жадаем, каб яны стваралі добрае ПЗ. Менавіта таму іх мова павінна простай ёй для разумення і вывучэння.
 
Ён павінен быць знаёмым, груба кажучы падобным да Сі. Праграмісты якія працуюць у Google рана пачынаюць сваю кар'еру і ў большасці сваёй знаёмыя з працэдурнымі мовамі, у прыватнасці сямейства Сі. Патрабаванне ў хуткай прадуктыўнасці на новай мове праграмавання азначае, што мова не павінна быць занадта радыкальнай.

Што? Так Роб Пайк у сутнасці кажа, што распрацоўнікі ў Google не гэтак добрыя, таму яны і стварылі мову для ідыётаў (заўв.: dumbed down), так каб яны былі ў стане нешта зрабіць. Што за пагардлівы погляд на ўласных калегаў? Я заўсёды лічыў, што распрацоўшчыкі Google адабраны з самых яркіх і лепшых на Зямлі. Вядома яны могуць справіцца з нечым больш складаным?

Артэфакты празмернай прастаты

Быць простым - гэта годнае імкненне ў любым дызайне, а паспрабаваць зрабіць нешта простым цяжка. Аднак пры спробе вырашыць (ці нават выказаць) складаныя задачы, сітавінай неабходзен складаная прылада. Складанасць і заблытанасць не лепшыя рысы мовы праграмавання, але існуе залатая сярэдзіна, пры якой у мове магчыма стварэнне элегантных абстракцый, простых у разуменні і выкарыстанні.

Не вельмі выразны

З-за імкнення да прастаты ў Go адсутнічаюць канструкцыі, якія ў астатніх мовах успрымаюцца як нешта натуральнае. Спачатку гэта можа здацца добрай ідэяй, але на практыцы выходзіць шматслоўны код. Чыннік гэтаму павінна быць відавочная - неабходна, каб распрацоўнікам было проста чытаць чужы код, але насамрэч гэтыя спрашчэнні толькі шкодзяць чытальнасці. Скарачэнні ў Go адсутнічае: альбо шмат, альбо нічога.

Напрыклад, кансольная ўтыліта, якая чытае stdin або файл з аргументаў каманднага радка, будзе выглядаць наступным чынам:

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

Хоць і гэты код спрабуе быць як мага больш агульным, прымусовая шматслоўнасць Go мяшае, і ў выніку рашэнне простай задачы выліваецца ў вялікі аб'ём кода.

Вось, да прыкладу, рашэнне той жа задачы на 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);
    }
}

І хто зараз больш чытэльны? Я аддам свой голас D. Яго код куды больш чытэльны, бо ён больш відавочна апісвае дзеянні. У D выкарыстоўваюцца канцэпцыі куды складаней (заўв.: альтэрнатыўны выклік функцый и шаблоны), чым у прыкладзе з Go, але на самой справе няма нічога складанага ў тым, каб разабрацца ў іх.

Пекла капіявання

Папулярная прапанова для паляпшэння Go - гэта абагульненасць. Гэта хаця б дапаможа пазбегнуць непатрэбнага капіявання кода для падтрымкі ўсіх тыпаў дадзеных. Да прыкладу, функцыю для падсумоўвання спісу цэлых лікаў можна рэалізаваць ніяк інакш, акрамя як яе копіпастой яе базавай функцыі для кожнага цэлага тыпу, іншага спосабу няма:

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

І гэты прыклад нават не працуе для знакавых тыпаў. Такі падыход цалкам парушае прынцып не паўтараць сябе (DRY), адзін з найбольш вядомых і відавочных прынцыпаў, ігнараванне якога з'яўляецца крыніцай многіх памылак. Навошта Go гэта робіць? Гэта жахлівы аспект мовы.

Той жа прыклад на D:

import std.stdio;
import std.algorithm;

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

Просты, элегантны і прама ў кропку. Тут выкарыстоўваецца функцыя reduce для шаблоннага тыпу і прэдыката. Так, гэта ізноў жа складаней варыянту з Go, але не гэтак ужо складана для разумення разумнымі праграмістамі. Які з прыкладаў прасцей падтрымліваць і лягчэй чытаць?

Просты абыход сістэмы тыпаў

Я мяркую, чытаючы гэта, праграмісты Go будуць з пенай у роце крычаць: "Ты робіш гэта не так!". Што ж, ёсць яшчэ адзін спосаб зрабіць абагульненую функцыю і тыпы, але гэта цалкам разбурае сістэму тыпаў!

Зірніце на гэты прыклад дурнога выпраўлення мовы для абыходу праблемы:

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

Гэтая імплементацыя Reduce была запазычаная з артыкула Idiomatic generics in Go (заўв.: пераклад не знайшоў, буду рады, калі дапаможаце з гэтым). Што ж, калі гэта ідыяматычна, я б не хацеў убачыць не ідыяматычны прыклад. Выкарыстанне interface{} - фарс, і ў мове ён патрэбен толькі для абыходу тыпізацыі. Гэта пусты інтэрфейс і ўсе тыпы яго рэалізуюць, дазваляючы поўную свабоду для ўсіх. Гэты стыль праграмавання да жаху пачварны, і гэта яшчэ не ўсё. Для падобных акрабатычных трукаў патрабуецца выкарыстоўваць рэфлексію часу выкананні. Нават Робу Пайку не падабаюцца індывіды, якія злоўжываюць гэтым, пра што ён згадваў у адным са сваіх дакладаў.

Гэта магутная прылада, які павінен быць скарыстаны з асцярожнасцю. Яго варта пазбягаць пакуль у ім няма строгай неабходнасці.

Я б узяў шаблоны D замест гэтага глупства. Як нехта можа сказаць, што interface{} больш чытэльны ці нават тыпабяспечны?

Гора кіравання залежнасцямі

У Go ёсць убудаваная сістэма залежнасцяў, пабудаваная па-над папулярнымі хостынгамі VCS. Якія пастаўляюцца з Go прылады ведаюць аб гэтым сэрвісах і могуць спампоўваць, збіраць і ўсталёўваць з іх код адным махам. Хоць гэта і выдатна, ёсць буйны промах з версіяваннем! Ды сапраўды, можна атрымаць зыходны код з сэрвісаў накшталт github ці bitbucket з дапамогай прылад Go, але нельга паказаць версію. І зноў прастата ва ўрон карыснасці. Я не ў стане зразумець логіку такога рашэння.

Пасля пытанняў аб рашэнні гэтай праблемы, каманда распрацоўкі Go стварыла галіну форума, у якой выказалі, як яны збіраюцца абыйсці гэтае пытанне. Іх рэкамендацыя была проста аднойчы скапіяваць увесь рэпазітар сабе ў праект і пакінуць "як ёсць". Якога чорта яны думаюць? У нас ёсць цудоўныя сістэмы кантролю версій з выдатным теггірованіем і падтрымкай версій, якія стваральнікі Go ігнаруюць і проста капіююць зыходныя тэксты.

Культурны багаж з Сі

На маю думку, Go быў распрацаваны людзьмі, якія выкарыстоўвалі Сі ўсё сваё жыццё і тымі, хто не хацеў паспрабаваць выкарыстоўваць нешта новае. Мова можна апісаць як Сі з дадатковымі колцамі(арыг.: навучальныя колы). У ім няма новых ідэй, акрамя падтрымкі паралелізму (які, дарэчы, выдатны) і гэта крыўдна. У вас ёсць выдатная паралельнасць у ці ледзь прыдатным да ўжывання, кульгальнай мове.

Яшчэ адна рыпучая праблема ў тым, што Go – гэта працэдурная мова (падобна ціхаму жаху Сі). У выніку пачынаеш пісаць код у працэдурным стылі, які адчуваецца архаічным і састарэлым. Я ведаю, што аб'ектна-арыентаванае праграмаванне - гэта не сярэбраная куля, але было б выдатна мець магчымасць абстрагаваць дэталі ў тыпы і забяспечыць інкапсуляцыю.

Прастата для ўласнай выгады

Go быў распрацаваны, каб быць простым і ён атрымаў поспех у гэтай мэты. Ён быў напісаны для слабых праграмістаў, выкарыстоўваючы ў якасці нарыхтоўкі старую мову. Пастаўляецца ён у камплекце з простымі прыладамі для выканання простых рэчаў. Яго проста чытаць і проста выкарыстоўваць.

Ён вельмі шматслоўны, невыразны і дрэнны для разумных праграмістаў.

Дзякуй mersinvald за праўкі

Крыніца: habr.com

Дадаць каментар