4 puntos por GN⁺ 2025-08-20 | 1 comentarios | Compartir por WhatsApp
  • La forma de programar de izquierda a derecha mantiene el programa en un estado válido desde el momento en que se escribe el código, lo que maximiza el soporte de herramientas como el autocompletado del editor
  • Las list comprehensions de Python dificultan el autocompletado por el uso de variables no declaradas y la ausencia de inferencia de tipos
  • Rust y JavaScript permiten construir programas de forma natural de izquierda a derecha, por lo que el uso de variables y la exploración de métodos resultan más intuitivos
  • El estilo funcional en C y Python perjudica una experiencia de codificación eficiente por la baja capacidad de descubrimiento de nombres de funciones o estructuras
  • En una lógica de alta complejidad, el código que se desarrolla de izquierda a derecha es más fácil de leer y ofrece mejor mantenimiento y escalabilidad

Programar de izquierda a derecha

El código debe ser válido desde el momento en que se escribe


Limitaciones de las list comprehensions de Python

  • La sintaxis de list comprehensions de Python, words_on_lines = [line.split() for line in text.splitlines()], genera el problema de que el editor no puede ofrecer correctamente autocompletado ni inferencia de tipos, porque necesita acceder a una variable no declarada (line)
  • Durante el proceso de escribir el código de forma parcial
    • si se escribe words_on_lines = [line.sp, el editor no puede saber el tipo de line, así que no puede sugerir métodos
    • también se vuelve difícil detectar errores potenciales, como un typo en el nombre de la variable (lime, por ejemplo)
  • Para recibir sugerencias correctas, hay que escribir código incompleto, y ese proceso resulta poco intuitivo e incómodo

Composición de izquierda a derecha en Rust

  • En el ejemplo de Rust, let words_on_lines = text.lines().map(|line| line.split_whitespace());
    • como la variable (line) se considera declarada en el momento en que aparece por primera vez junto con la función anónima, se vuelve posible obtener de inmediato autocompletado y sugerencias de métodos
    • de hecho, el método split_whitespace también fue fácil de encontrar gracias a la sugerencia automática
  • Este enfoque mantiene el programa siempre en un estado válido al menos de forma parcial, por lo que el IDE o editor puede asistir la escritura de código en tiempo real

Divulgación progresiva (Progressive Disclosure) y usabilidad de APIs

  • La divulgación progresiva (Progressive Disclosure) es un principio de diseño en el que el usuario solo experimenta la complejidad que necesita, y también puede aplicarse a la programación
    • ejemplo: es similar a la UX de un procesador de texto donde las opciones relacionadas solo aparecen al insertar una imagen
  • El lenguaje C carece de este tipo de apoyo
    • como no se pueden explorar todas las funciones relacionadas con FILE *file mediante file., hay que memorizar patrones en los nombres de funciones (fread, fclose, etc.), y resulta difícil descubrir funcionalidades
    • en cambio, en un lenguaje ideal, sería posible descubrir progresivamente funciones relacionadas mediante sugerencias de métodos a través de file.

Diferencia en la capacidad de descubrimiento de funciones y métodos

  • Comparación entre los ejemplos map(len, text.split()) de Python y text.split(" ").map(word => word.length) de JavaScript
    • en Python, como no es predecible cuál será el nombre de la función (len, length, size, etc.), hay que probar varias opciones para saber cuál funciona realmente
    • en JavaScript, basta con escribir .l después de word. para que el editor sugiera métodos como length, lo que da una alta capacidad de descubrimiento
    • incluso con funciones de orden superior como map, el valor de retorno real y el tipo de dato se vuelven evidentes de inmediato

Cuanto más compleja es la lógica, mayores son las ventajas de escribir con estructura

  • En el caso de una lógica compleja (código largo de Python con filter y lambda anidados)
    • hay que revisar repetidamente el inicio y el final del código, y aparecen problemas de legibilidad y de comprensión con expresiones condicionales o emparejamiento de paréntesis
  • En la versión equivalente en JavaScript, el código puede leerse y entenderse secuencialmente de arriba hacia abajo y de izquierda a derecha

Principio clave

El código debe ser válido en cada momento en que se escribe

  • Incluso al escribir solo text, el programa permanece en un estado válido
  • Aunque se escriba hasta text.split(" "), y luego se continúe con .map(word => word.length), el estado intermedio completo siempre sigue siendo válido
  • Este patrón de escritura de código incrementa la posibilidad de soporte en tiempo real por parte del editor, y en un entorno REPL incluso permite verificar resultados al instante

Conclusión

  • El diseño de APIs y lenguajes debe permitir escribir código de forma natural de izquierda a derecha y crear un programa válido en cada etapa intermedia
  • Un buen diseño de APIs es la clave para mejorar esta experiencia de codificación

1 comentarios

 
GN⁺ 2025-08-20
Comentarios de Hacker News
  • Uno de los problemas de SQL es que la consulta empieza con SELECT y no con FROM, así que es difícil ver de inmediato qué entidad (tabla) se está manejando, y eso también estorba a la hora de que los editores inteligentes ayuden a escribir consultas de forma más eficiente; ir en el orden FROM -> SELECT -> WHERE se siente más natural, sobre todo porque en la cláusula SELECT se definen nombres de columnas y luego se hace referencia a ellos en WHERE; de hecho, creo que en vez de SELECT * FROM table podría bastar con escribir solo FROM table y omitir la cláusula SELECT; sé que por quejarme de esto sueno como un viejo regañón, pero es solo una añoranza personal
    • PSQL y PRQL de hecho usan un orden de consulta donde FROM va primero; BigQuery también agregó recientemente sintaxis de pipe/flecha, y hay extensiones comunitarias para DuckDB, así que las recomiendo: DuckDB - PSQL, DuckDB - PRQL
    • SQL se escribe así porque en los fundamentos del álgebra relacional la proyección (Projection) siempre se escribe primero; por eso, según el estándar, no se pueden usar alias de columnas en WHERE, porque la selection (WHERE) ocurre antes que la projection (SELECT); por cierto, en MySQL 8 también existe la sintaxis TABLE <table>, que vale la pena revisar
    • De hecho, en la mayoría de los motores SQL el orden interno de procesamiento es FROM -> WHERE -> SELECT; por eso los alias de columna definidos en SELECT pueden usarse en GROUP BY, HAVING y ORDER BY, pero no en WHERE
    • En C# también hay un DSL que compila a SQL (LINQ-to-SQL) con una estructura donde FROM va primero; además, al escribir otras cláusulas en el IDE, el autocompletado puede sugerir campos de inmediato, y por eso me parece una buena estructura
    • Kusto, el lenguaje de consultas para análisis de datos de Azure, también tiene una forma parecida usando pipes: Introducción a las consultas Kusto; el estilo LINQ de .NET es igual; sinceramente, creo que en SQL deberían introducirse de forma más decidida variantes que empiecen con FROM, y no me parece algo difícil de lograr; siento que faltan intentos por mejorar la usabilidad
  • Me cuesta entender por qué Python es tan querido; cuando trabajan dos o más personas, el lenguaje se vuelve infinitamente doloroso; lo que señala el autor es apenas la punta del iceberg
    • Creo que es parecido a por qué la gente no se vuelca masivamente a los lenguajes de la familia Lisp; el rigor matemático no equivale automáticamente a legibilidad; las comprehensions de list/dict/set en Python son como los bucles for que ya determinan el tipo; todo el mundo se preocupa porque los tipos en Python son flojos, pero es raro que el único constructo que deja claro el tipo de retorno (la list comprehension) termine siendo el objetivo; además, en la mayoría de los otros lenguajes, incluido Rust, el orden no es "from iter as var"; también es divertido comparar la sintaxis de llamadas a funciones entre lenguajes (igual que en Python existe functools.map)
    • No creo que el hecho de no entender algo lo convierta en una virtud; claramente hay algo por lo que Python es tan querido; por supuesto que tiene defectos, pero eso por sí solo no significa nada; hay que comparar de forma integral ventajas y desventajas, y hacer lo mismo con otros lenguajes
    • A mí también me gusta Python (siempre que hablemos de equipos pequeños y programas cortos, de vida útil breve); al no tener tipos estáticos se implementa rápido, pero gracias a su sistema de tipos fuerte no se rompe por completo; creo que por eso es tan popular en ciencia de datos, porque es muy cómodo para explorar; en cambio, sí tiene desventajas claras en programas grandes o mantenidos a largo plazo por varios equipos; al final no existe un lenguaje universal, y como mínimo hacen falta dos tipos: uno “suave” para probar cosas e ir rápido, y otro “duro” para mantener durante mucho tiempo
    • Yo antes estaba completamente de acuerdo con esa opinión, pero las anotaciones de tipos y la verificación de tipos han hecho mucho más fácil colaborar en código Python escrito por otras personas; sigo sin verlo para proyectos enormes, pero con tipos Python se convirtió en mi lenguaje de scripting favorito
    • Yo también evito cosas como las list comprehensions en codebases compartidos y prefiero un estilo de Python muy simple; se le conoce como un lenguaje donde “debería haber una sola forma”, pero en la práctica conviven demasiadas; personalmente las list comprehensions me parecen divertidas y satisfactorias, pero si todos tuviéramos que ir por un solo camino, esta sintaxis no debería existir
  • Simpatizo con la idea de que “un programa debería ser válido en cuanto lo estás escribiendo”, pero en la realidad no siempre se escribe código de izquierda a derecha y línea por línea; muchas veces primero escribes otra parte o declaras variables después; por ejemplo, a veces usas una variable y la declaras mucho más adelante
    • Dado que el código se escribe una vez y se lee decenas o cientos de veces, creo que el código que puede leerse secuencialmente es mucho más cómodo que el que obliga a estar saltando
    • En realidad esta discusión se desvía un poco del punto central del artículo, pero es una perspectiva interesante
    • Totalmente de acuerdo; solo escribo código en orden desde el principio cuando creo un archivo nuevo; si agrego un campo, no voy primero a la definición de la clase, sino que escribo antes el código que usa ese campo; y al mejorar una condición, muchas veces el código pasa por un estado temporalmente inválido (con errores)
    • También estoy de acuerdo con esto, pero un principio importante relacionado es que una estructura que “ni siquiera te deja compilar porque todavía no terminas de programar” es excesiva; los errores deberían ser no bloqueantes, pero algunos lenguajes bloquean por completo el código incompleto (por ejemplo, variables no usadas, return faltante, etc.)
    • A veces me incomoda sentir que el IDE no entiende bien el orden en que realmente estoy escribiendo el código
  • En algunos IDE hay funciones de plantillas de código donde escribes una abreviatura y se expande a una estructura de código, y luego vas llenando cada placeholder con Tab; en esos casos, el orden de navegación con Tab no tiene por qué ser estrictamente de izquierda a derecha, así que algo como {3} for {2} in {1} también es posible; estas herramientas ofrecen un punto medio entre una “sintaxis fácil de leer” y una “sintaxis fácil de teclear”; yo me inclino por priorizar la sintaxis legible, aunque sea apoyándose en tooling; no creo que haya que aferrarse necesariamente a la estructura “for-in”
  • Últimamente en Hacker News parece haber cierto consenso en que a Python le faltó un operador pipe; yo entendí rápido el valor del pipe cuando pasé de Mathematica a R; al escribir código de transformación de datos por etapas en ciencia de datos, se siente muy intuitivo y fácil de leer; Python se usa en muchos ámbitos, pero me pregunto si el pipe también tendría ventajas en otros contextos más allá del análisis de datos; estoy tratando de entender por qué Python no lo adoptó
    • Yendo un paso más allá del operador pipe, creo que también valdría la pena probar la asignación inversa; en vez de asignar el resultado a una variable como en 'let foo = ...', me gustaría probar algo como '... =: foo'
    • El operador pipe de R (especialmente en tidyverse R) es para mí la “killer app” más importante; no conozco otro lenguaje donde trabajar con datos sea tan fácil y agradable; por ejemplo, en vez de ir anidando una receta de galletas como bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12), con pipe se vuelve mucho más sencillo y legible: mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12)
    • En pandas de Python, usando la sintaxis de pipe, queda así:
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      En R sería:
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      Ambos se leen bien, pero la ventaja de R es que el pipe funciona mejor también fuera de los dataframes
  • Esta discusión se acerca casi a una guerra religiosa, como la discusión de FP (funcional) vs OOP (orientado a objetos), o la de vim contra emacs; en vim el operador va primero, y en emacs primero va el orden de selección; los lenguajes que “se leen como inglés” suelen tener una estructura con el verbo al principio (como Lisp/Scheme), mientras que lenguas como el alemán o el tamil, donde el verbo va al final, encajan mejor con un estilo OOP (sustantivo primero); por ejemplo, en tamil el orden sería “water drink”, mientras que en inglés es “drink water”; por eso puede haber gente a la que vim le resulte más natural; no creo que un estilo sea mejor que otro, sino que a veces se adaptan mejor a ciertas herramientas o preferencias personales, y hoy en día con modelos de lenguaje casi cualquier cosa es posible
    • Sobre si “si se diseña para leerse como inglés entonces el verbo debería ir primero”, diría que sí en un lenguaje imperativo, pero en uno declarativo, si quieres que se lea como inglés, el sujeto va primero
    • Sobre si “en alemán el verbo siempre va al final”, en realidad en oraciones simples el verbo va en segundo lugar (“I drink water” → “Ich trinke Wasser”), no siempre completamente al final
    • Sobre la idea de que en vim el operador va primero, en realidad Kakoune funciona al revés, y me parece mucho más lógico: Explicación sobre Kakoune
  • Por otro lado, la sintaxis de Python from some_library import child_module es muy intuitiva; en JS una estructura como import { asYetUnknownModule } from SomeLibrary se siente bastante menos intuitiva
    • En JS, si se usa un namespace import de esta forma:
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      en la práctica el autocompletado del IDE funciona muy bien, y eso me parece una ventaja: Explicación de MDN sobre namespace import
    • Me parece raro obsesionarse con la palabra clave "from"; simplemente podría hacerse algo así:
      import SomeLibrary {
        asYetUnknownModule
      }
      
  • ReScript cambió justamente por esto su API de data-last a data-first; gracias a su excelente inferencia de tipos, casi siempre ofrece autocompletado correcto y acorde al tipo, y la experiencia de desarrollo es muy buena; claro, si declaras una función sin referencias previas (y por tanto sin poder conocer el tipo), el problema sigue ahí, pero se resuelve agregando tipos o llamándola primero; también recomiendo esta entrada del blog: Comparación entre data-first y data-last
  • Llevo tiempo defendiendo esta forma de verlo, y también conecta con por qué Ruby siempre me ha parecido mucho más fácil; especialmente porque yo nunca he usado ni Python ni Ruby de forma profunda a nivel de producción, pero aun así me cuesta entender por qué Python terminó tan instalado y tan usado; Ruby tampoco está libre de defectos, pero no parece que mucha gente que hace scripting se tope con el mismo nivel de cambios complejos que tuvo Python; al menos Ruby no ha tenido en los últimos 10 años un gran choque por incompatibilidades de versión
  • En general estoy completamente de acuerdo con lo que señala el artículo: una estructura donde el contexto aparece primero y se lee de izquierda a derecha probablemente se adapte mejor a los LLM y al autocompletado; aun así, en vez de escribir el código del ejemplo como len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))), me parece mucho mejor aprovechar un array de NumPy, porque no hace falta crear listas nuevas en memoria y además permite operar sobre toda la línea de una vez; por ejemplo:
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    esto refleja mucho mejor la idea de “izquierda a derecha”
    • La versión con numpy sigue siendo algo críptica ("line > 0" está bien, pero las reglas de broadcasting pueden complicarse), y las APIs de colecciones de lenguajes con tipos más estrictos, como el ejemplo en JavaScript del autor o C#, Java y Scala, me parecen más limpias; mi preferencia es Kotlin, porque se puede escribir así:
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }