Por qué Go Design es malo para los programadores inteligentes

Durante los últimos meses he estado usando Go para implementaciones. Prueba de concepto (aprox.: código para probar la funcionalidad de una idea) en su tiempo libre, en parte para estudiar el propio lenguaje de programación. Los programas en sí son muy simples y no son el propósito de este artículo, pero la experiencia de usar Go en sí merece algunas palabras al respecto. Go promete ser (aprox.: artículo escrito en 2015) un lenguaje popular para código escalable serio. El idioma fue creado por Google, donde se utiliza activamente. En pocas palabras, honestamente creo que el diseño del lenguaje Go es malo para los programadores inteligentes.

¿Diseñado para programadores débiles?

Go es muy fácil de aprender, tan fácil que la introducción me llevó una noche, después de lo cual ya podía codificar productivamente. El libro que usé para aprender Go se llama Una introducción a la programación en Go (traducción), está disponible en línea. El libro, al igual que el código fuente de Go, es fácil de leer, tiene buenos ejemplos de código y contiene alrededor de 150 páginas que se pueden leer de una sola vez. Esta simplicidad resulta refrescante al principio, especialmente en un mundo de programación lleno de tecnología demasiado complicada. Pero al final, tarde o temprano surge el pensamiento: “¿Es esto realmente así?”

Google afirma que la simplicidad de Go es su punto de venta y que el lenguaje está diseñado para lograr la máxima productividad en equipos grandes, pero lo dudo. Hay características que faltan o están demasiado detalladas. Y todo por falta de confianza en los desarrolladores, bajo el supuesto de que no son capaces de hacer nada bien. Este deseo de simplicidad fue una decisión consciente de los diseñadores del lenguaje y, para comprender completamente por qué era necesario, debemos comprender la motivación de los desarrolladores y lo que intentaban lograr en Go.

Entonces, ¿por qué se hizo tan simple? Aquí hay un par de citas. Rob Pike (aprox.: uno de los cocreadores del lenguaje Go):

El punto clave aquí es que nuestros programadores (aprox.: Googlers) no son investigadores. Generalmente son bastante jóvenes, vienen a nosotros después de estudiar, tal vez hayan estudiado Java, C/C++ o Python. No pueden entender un gran idioma, pero al mismo tiempo queremos que creen un buen software. Es por eso que su idioma debe ser fácil de entender y aprender.
 
Debería resultarle familiar, en términos generales similar a C. Los programadores que trabajan en Google comienzan sus carreras temprano y en su mayoría están familiarizados con los lenguajes de procedimiento, en particular la familia C. El requisito de una productividad rápida en un nuevo lenguaje de programación significa que el lenguaje no debería ser demasiado radical.

¿Qué? Entonces, Rob Pike básicamente está diciendo que los desarrolladores de Google no son tan buenos, por eso crearon un lenguaje para idiotas (aprox.: simplificado) para que puedan hacer algo. ¿Qué clase de mirada arrogante hacia tus propios colegas? Siempre he creído que los desarrolladores de Google son seleccionados entre los mejores y más brillantes de la Tierra. ¿Seguramente podrán manejar algo más difícil?

Artefactos de excesiva sencillez

Ser simple es un objetivo valioso en cualquier diseño, y tratar de hacer algo simple es difícil. Sin embargo, cuando se intenta resolver (o incluso expresar) problemas complejos, a veces se necesita una herramienta compleja. La complejidad y la complejidad no son las mejores características de un lenguaje de programación, pero existe un término medio en el que el lenguaje puede crear abstracciones elegantes que son fáciles de entender y usar.

no muy expresivo

Debido a su compromiso con la simplicidad, Go carece de construcciones que se perciben como naturales en otros lenguajes. Esto puede parecer una buena idea al principio, pero en la práctica da como resultado un código detallado. La razón de esto debería ser obvia: los desarrolladores deben poder leer fácilmente el código de otras personas, pero en realidad estas simplificaciones sólo perjudican la legibilidad. En Go no hay abreviaturas: mucho o nada.

Por ejemplo, una utilidad de consola que lea la entrada estándar o un archivo desde los argumentos de la línea de comandos tendría este aspecto:

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

Aunque este código también intenta ser lo más general posible, la verbosidad forzada de Go se interpone y, como resultado, resolver un problema simple genera una gran cantidad de código.

Aquí, por ejemplo, hay una solución al mismo problema en 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);
    }
}

¿Y quién es más legible ahora? Le daré mi voto a D. Su código es mucho más legible porque describe las acciones con mayor claridad. D utiliza conceptos mucho más complejos (aprox.: llamada de función alternativa и Patrones) que en el ejemplo de Go, pero realmente no hay nada complicado en entenderlos.

infierno de copiar

Una sugerencia popular para mejorar Go es la generalidad. Esto al menos ayudará a evitar la copia innecesaria de código para admitir todos los tipos de datos. Por ejemplo, una función para sumar una lista de números enteros no se puede implementar de otra manera que copiando y pegando su función básica para cada tipo de número entero; no hay otra manera:

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

Y este ejemplo ni siquiera funciona para tipos con signo. Este enfoque viola completamente el principio de no repetirse (SECO), uno de los principios más famosos y obvios, ignorarlo es fuente de muchos errores. ¿Por qué Go hace esto? Este es un aspecto terrible del lenguaje.

Mismo ejemplo en D:

import std.stdio;
import std.algorithm;

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

Sencillo, elegante y directo al grano. La función utilizada aquí es reduce para el tipo de plantilla y el predicado. Sí, esto es nuevamente más complicado que la versión Go, pero no tan difícil de entender para los programadores inteligentes. ¿Qué ejemplo es más fácil de mantener y de leer?

Bypass del sistema de tipo simple

Me imagino que los programadores de Go que lean esto echarán espuma por la boca y gritarán: "¡Lo estás haciendo mal!". Bueno, hay otra forma de crear funciones y tipos genéricos, ¡pero rompe completamente el sistema de tipos!

Eche un vistazo a este ejemplo de una estúpida solución de lenguaje para solucionar el problema:

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

Esta implementación Reduce fue tomado prestado del artículo Genéricos idiomáticos en Go (aprox.: No pude encontrar la traducción, me alegraré si me ayudas con esto). Bueno, si es idiomático, odiaría ver un ejemplo que no sea idiomático. Uso interface{} - una farsa, y en el idioma solo es necesario evitar escribir. Esta es una interfaz vacía y todos los tipos la implementan, permitiendo total libertad para todos. Este estilo de programación es terriblemente feo y eso no es todo. Hazañas acrobáticas como estas requieren el uso de reflexión en tiempo de ejecución. Incluso a Rob Pike no le gustan las personas que abusan de esto, como mencionó en uno de sus informes.

Esta es una herramienta poderosa que debe usarse con precaución. Debe evitarse a menos que sea estrictamente necesario.

Tomaría plantillas D en lugar de estas tonterías. ¿Cómo puede alguien decir eso? interface{} ¿Más legible o incluso seguro para escribir?

Los problemas de la gestión de la dependencia

Go tiene un sistema de dependencia integrado creado sobre proveedores de alojamiento populares VCS. Las herramientas que vienen con Go conocen estos servicios y pueden descargar, compilar e instalar código de ellos de una sola vez. Si bien esto es genial, ¡hay un defecto importante en el control de versiones! Sí, es cierto que puedes obtener el código fuente de servicios como github o bitbucket usando las herramientas Go, pero no puedes especificar la versión. Y de nuevo sencillez a costa de utilidad. No puedo entender la lógica de tal decisión.

Después de hacer preguntas sobre una solución a este problema, el equipo de desarrollo de Go creó hilo del foro, que describía cómo iban a solucionar este problema. Su recomendación fue simplemente copiar todo el repositorio en su proyecto algún día y dejarlo "como está". ¿Qué diablos están pensando? Contamos con increíbles sistemas de control de versiones con excelente etiquetado y soporte de versiones, que los creadores de Go ignoran y simplemente copian el código fuente.

Bagaje cultural de Xi

En mi opinión, Go fue desarrollado por personas que habían usado C toda su vida y por aquellos que no querían probar algo nuevo. El lenguaje se puede describir como C con ruedas adicionales (orig.: ruedas de entrenamiento). No contiene ideas nuevas, excepto el apoyo al paralelismo (que, por cierto, es maravilloso) y es una pena. Tiene un paralelismo excelente en un lenguaje poco convincente y poco utilizable.

Otro problema chirriante es que Go es un lenguaje procedimental (como el horror silencioso de C). Terminas escribiendo código en un estilo procedimental que parece arcaico y anticuado. Sé que la programación orientada a objetos no es una solución milagrosa, pero sería fantástico poder abstraer los detalles en tipos y proporcionar encapsulación.

Simplicidad para tu propio beneficio

Go fue diseñado para ser simple y logra ese objetivo. Fue escrito para programadores débiles, utilizando un lenguaje antiguo como plantilla. Viene completo con herramientas simples para hacer cosas simples. Es fácil de leer y fácil de usar.

Es extremadamente detallado, poco impresionante y malo para los programadores inteligentes.

Gracias mersinvaldo para ediciones

Fuente: habr.com

Añadir un comentario