Чому дизайн 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))
}

І цей приклад навіть не працює для знакових типів. Такий підхід повністю порушує принцип не повторювати себе.СУХИЙ), один з найбільш відомих та очевидних принципів, ігнорування якого є джерелом багатьох помилок. Навіщо 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

Додати коментар або відгук