Sintaxis vs. Semántica
Todos los lenguajes de programación comparten dos componentes esenciales, la sintaxis y la semántica. La sintaxis es el conjunto de reglas que definen cómo organizar los símbolos y palabras claves de un lenguaje para formar sentencias y expresiones válidas . Por otro lado, la semántica es cómo se deben interpretar esas expresiones. Esto se puede formalizar de distintas maneras, una de ellas es la semántica operacional, que describe el comportamiento de un programa en términos de cómo se ejecuta paso a paso sus instrucciones. Esta semántica se divide en dos ramas, la semántica small step y la semántica big step.
La semántica small step describe la ejecución de los programas dividiéndolos en pasos pequeños, es decir, evaluando cada instrucción de forma secuencial. Para ello, define una relación binaria que conecta cada estado del programa antes y después de realizar una instrucción. Esta semántica es útil para conocer cuál es el estado del programa en un momento dado.
Por otro lado, la semántica big step, describe los resultados finales de la computación, sin preocuparse por los estados intermedios. El objetivo de esta semántica es llegar directamente al resultado final sin detenerse en cada paso, aplicando una estrategia de divide y vencerás.
Existen otras semánticas, como la axiomática que describe el significado de los programas mediante pre y post-condiciones, o la semántica denotacional que describe el comportamiento de los programas haciendo uso de objetos matemáticos.
Aunque estas semánticas sirven para estudiar el poder expresivo de un lenguaje, no suelen utilizarse explícitamente al momento de programar. Por esta razón, vamos a introducir una nueva noción de semántica: la semántica en lenguaje natural, que describe el programa según lo que el desarrollador pretende que el código haga. Esta semántica es imposible de definir formalmente porque la misma es subjetiva al programador que escribió el código, pero es importante darle entidad a su existencia, pues la misma puede ser más o menos evidente, para los demás, según la calidad del código que se escribe.
Para explicar mejor la idea de semántica en lenguaje natural, vamos a decir que un código bonito es un código que sigue buenas prácticas de programación. Entre esas prácticas que vamos a ver en este trabajo, está la elección de buenos nombres de funciones: un buen nombre hace explicito lo qué hace la función. Para ilustrar esta idea, consideremos un ejemplo clásico, una función que calcula la secuencia de Fibonacci
def fibonacci (n: int) -> int:if n <= 1:return nreturn fibonacci(n-1) + fibonacci(n-2)
Ahora, analicemos las siguientes funciones que hacen uso de esta secuencia.
def rabbit_population_growth (n_months: int) -> int:"""Computes the number of rabbit pairs after a given number of months.Params:n_months (int): The number of months to calculate.Returns:int: The total number of rabbit pairs after n_months."""return fibonacci(n_months)
def count_drone_ancestors (n_generations: int) -> int:"""Computes the number of ancestors of a drone bee after a given number of generations.Params:n_generations (int): The number of generations to trace back.Returns:int: The total number of ancestors in n_generations."""return fibonacci(n_generations)
Aunque rabbit_population_growth
y count_drone_ancestors
compartan implementación y semántica formal, su semántica en lenguaje natural difiere ya que realizan tareas distintas. La primer función comunica una historia sobre la reproducción de los conejos, mientras que la segunda se centra en los ancestros de los zánganos de una colmena. Esta diferencia nos permite entender la intención del desarrollador, porque con un simple vistazo al nombre de la función comprenderemos un poco mas sobre el contexto del código en general.
Si un sistema de software es lo suficientemente complejo, estará compuesto por una gran cantidad de funciones que interactúan entre sí. Si el código no es organizado correctamente, no sólo se hará muy difícil de entender, sino que también de extender y mantener. Para evitar estos problemas, es muy importante adoptar un enfoque de trabajo claro y estructurado, como el enfoque top-down.
Cuando hablamos de enfoque top-down decimos que debemos ir del nivel más general al más específico, descomponiendo el problema en partes más pequeñas y manejables. En otras palabras, el código que escribe un desarrollador debe contar una historia: la función principal o main
debe actuar a modo de índice o resumen, presentando los “capítulos” que no son más que las funciones dentro del programa. Cada función detalla una parte específica de la historia, mientras que los componentes como variables y controladores de flujo dentro de las funciones desarrollan el contenido. Observemos el siguiente código:
def nuclear_reactor_controller () -> None:for control in CONTROL_LIST:control_result = execute_control(control)if control_result.failed():trigger_alarm(control, control_result)execute_emergency_plan(control, control_result)
La mayoría de los lectores probablemente no entienda los detalles técnicos sobre reactores nucleares, pero este fragmento de código cuenta una historia lo suficientemente clara como para comprender a grandes rasgos qué es lo que ocurre. Describe como un reactor realiza una serie de controles rutinarios, y, si alguno de ellos falla, se activa una alarma y se ejecuta el plan de emergencia. Si bien hay muchos detalles que no conocemos, como cuáles son los controles o como se activan las alarmas, el diseño facilita explorar las funcionalidades internas y comprender la lógica detrás de estas acciones.
Entonces, para escribir una buena historia en código primero debemos tener en cuenta conceptos fundamentales, como el uso adecuado de nombres de variables y funciones, escribir comentarios de docstring claros, seguir convenciones del lenguaje que se está utilizando y ser consistentes con el idioma a lo largo de todo el código. Estos son los pilares para escribir un código bonito que comunique apropiadamente la intención del desarrollador, logrando que un proyecto complejo tenga una semántica en lenguaje natural evidente.
El arte de nombrar variables
Todo código esta compuesto por funciones y variables. Las funciones permiten abstraernos de un bloque de sentencias y reutilizarlo a lo largo de todo el programa. Mientras que las variables nos permiten almacenar y manipular datos. Tanto las funciones como variables tienen nombres que nos permiten identificarlas y utilizarlas. A nivel sintáctico, algunos lenguajes imponen restricciones, como exigir que los nombres de las clases comiencen con mayúsculas o prohibir comenzar con un número. Pero por fuera de esas reglas, el desarrollador es completamente libre de escoger cualquier nombre. El problema es que con frecuencia se utilizan nombres vagos, confusos o ambiguos, lo que dificulta la comprensión del código. Un nombre estará bien elegido si hace inequívoca su semántica en lenguaje natural.
Al nombrar incorrectamente una función, generamos malinterpretaciones, ya que otro desarrollador podría pensar que la función realiza acciones que realmente no ejecuta, o por el contrario, ocultamos funcionalidades que no se reflejan en el nombre. Lo mismo ocurre con las variables, nombres poco claros pueden dificultar comprender el tipo de dato que éstas almacenan o cómo es que ese dato está siendo utilizado en el sistema. Por ello es que debemos elegir cuidadosamente palabras específicas que describan con precisión el propósito de nuestros elementos, evitando términos genéricos o vacíos que puedan causar ambigüedad.
Supongamos una función llamada processData()
, ¿qué es lo que pretende el desarrollador qué esta función haga? Comprender esto tan solo mirando el nombre se vuelve una tarea casi imposible. ¿Suma distintos valores? ¿Filtra elementos según alguna regla específica? En definitiva, no es algo claro. Por otro lado, nombres como calculateTotalWithTaxes()
o filterValidatedUsers()
brindan mucha más información sobre la finalidad de la función. Lo mismo ocurre con las variables, un caso recurrente es llamarlas data
o value
. Estos nombres no ofrecen nada de información sobre su propósito o contenido. Incluso, en lenguajes no tipados como Python o JavaScript, ni siquiera se tiene información sobre el tipo de dato que contiene.
💡 Lineamiento: Las funciones y variables deben tener nombres descriptivos que ayuden a comprender su significado.
¿Cómo podemos elegir un buen nombre para nuestras funciones y variables? La clave está en usar palabras adecuadas para describir claramente lo que pretendemos con ellas. Una regla esencial es utilizar nombres fáciles de buscar y pronunciar. Proyectos grandes suelen contener múltiples archivos y carpetas, que a su vez poseen gran cantidad de variables y funciones, por lo que nombres descriptivos y fáciles de buscar mejoran la legibilidad y ahorran tiempo. A esto se le suma la importancia de los nombres pronunciables, nombres fáciles de decir, serán fáciles de recordar y encontrar. Un nombre complejo que solo el desarrollador original comprende entorpece la comunicación y colaboración en el código.
Lineamientos para nombrar funciones
Al momento de nombrar funciones, es fundamental utilizar verbos. Dado que las funciones realizan acciones, que mejor que utilizar verbos que son perfectos para ello. Elegir el verbo correcto puede marcar una gran diferencia entre un nombre claro y uno ambiguo. Por ejemplo, usar distribute
en lugar de send
, o identify
en lugar de find
puede dar lugar a nombres mucho más precisos e informativos. Si no somos capaces de encontrar un verbo que describa precisamente la intención de nuestro código, entonces puede que la función en cuestión realice más de una acción y deba modularizarse. Asegurar que una función realice una única tarea es muy importante y por eso trataremos este tema en los siguientes capítulos.
Además, como nos enseñan desde los primeros años de escuela, los verbos suelen estar acompañados por otras palabras que brindan más contexto sobre la acción. En las funciones, esto es igual de importante. Necesitamos términos específicos que describan con claridad el alcance de la función. En nuestro ejemplo anterior calculateTotalWithTaxes()
, no solo se nos indica que se está calculando un valor total, sino que además, se agrega que se están considerando impuestos.
ℹ️ Una práctica común al trabajar con clases es nombrar a los métodos que acceden o modifican valores internos con prefijos get y set. Esto indica automáticamente si el método devuelve el valor de una propiedad interna o por el contrario lo modifica.
Lineamientos para nombrar variables
Así como podemos dar nombres descriptivos a las funciones, existen algunas buenas prácticas al nombrar variables que hacen que sea más fácil entender el propósito del código. En este caso, el uso de sustantivos es ideal para las variables, ya que representan entidades dentro del programa. No obstante, el tipo de la variable también influye en cómo debería nombrarse. Para variables de tipo bool
, es recomendado utilizar prefijos como is
, has
o can
. Dado que estas palabras suelen iniciar las preguntas en inglés, nombres como isVisible
o hasAccess
resultan intuitivos y ayudan a comprender el significado de su valor en un momento dado. Es importante, sin embargo, evitar nombres con este formato que incluyan una negación, como isNotOpen
, ya que, aunque se entiende su objetivo, puede generarse confusión al momento de su uso.
En el caso de arreglos, listas o conjuntos de valores, los nombres en plural son buena práctica, como adminCommands
o validUsers
para reflejar la multiplicidad de elementos. Para variables numéricas, prefijos como max
, min
o total
añaden contexto valioso si el valor implica algún tipo de rango o límite. Asimismo, si la variable representa alguna unidad de medida (como tiempo, distancia o dinero), incluir una referencia a la unidad en el nombre aporta claridad y reduce posibles errores de conversión innecesarios.
Otra buena práctica al nombrar constantes o variables es aprovechar el nombre de la función con la que las inicializamos. Si la función tiene un nombre adecuado, es decir, es descriptivo y no genera confusión, podemos usarlo como referencia para nombrar nuestra variable de manera coherente. Veamos un ejemplo:
new_warehouse = self._get_warehouse_basic_info_obj(warehouse)
El nombre new_warehouse
sugiere que la variable almacena un objeto de una clase, pero si observamos el nombre de la función, vemos que en realidad devuelve la información básica de un depósito. Un nombre más preciso y alineado con su contenido sería:
warehouse_basic_info = self._get_warehouse_basic_info_obj(warehouse)
Longitud de los nombres
Muchas veces, al intentar ser específicos con nuestros nombres, surge un nuevo problema, la longitud de estos. Entonces ¿Cuál es la longitud perfecta para un nombre? En general, nombres demasiado largos pueden ser difíciles de recordar y ocupan mucho espacio en pantalla, pero por otro lado nombres cortos no ofrecen tanta información. La clave, como siempre, es encontrar un equilibrio, pero también existen algunas recomendaciones que podemos seguir:
- Si el alcance de la función o variable es pequeño, por ejemplo, una función que solo se utiliza en el mismo archivo en la cual se define o una variable con vida util de unas pocas lineas, está bien optar por nombres cortos. Por ejemplo si estamos realizando cálculos matemáticos y tenemos una función local auxiliar para calcular la magnitud o norma de un vector, podemos nombrar a nuestra función como
norm()
en lugar decalculateVectorMagnitude()
- Intentaremos evitar el uso de acrónimos y abreviaciones siempre que sea posible. Los nuevos desarrolladores o aquellos con poco conocimiento del código, podrían tener dificultades para comprender su significado. Por ejemplo en lugar de
calcTtl()
, usarcalculateTotalPrice()
. - Elimina palabras que no aportan información relevante. Por ejemplo en lugar de
ConvertToString()
, usarToString()
.
Siguiendo estas recomendaciones, lograremos nombres más claros y concisos que aportarán legibilidad y facilitarán la comprensión de las funciones.
Tipado en el código
Tipos de dato, tipos de función y su importancia
Un tipo de dato (o simplemente tipo) define el conjunto de valores que una variable puede almacenar y las operaciones que se pueden realizar sobre esos valores. De forma similar, las funciones también poseen un tipo, conocido como tipo de una función, que describe el tipo de sus parámetros y su valor de retorno.
En la mayoría de los lenguajes de programación, los tipos de datos se pueden clasificar en tres categorías
- Primitivos: Tipos básicos proporcionados por el lenguaje, como
int
,float
,char
,boolean
. - Compuestos: Estructuras que agrupan múltiples valores, como
array
,tuple
,struct
. - Personalizados: Tipos definidos por el usuario a partir de tipos primitivos o compuestos. Estos se utilizan para representar entidades específicas.
Cada tipo de dato requiere distinta cantidad de memoria y permite realizar ciertas operaciones. Por ejemplo, una variable booleana solo puede almacenar los valores true
o false
, lo que generalmente ocupa un solo byte en memoria. Por otro lado, los tipos numéricos pueden representar un rango mucho más amplio de valores, por lo que su tamaño en memoria es ser mayor.
Un caso reciente que demuestra la importancia de elegir los tipos adecuados se vio con la publicación del modelo de lenguaje de la empresa china DeepSeek. A diferencia de sus competidores, los desarrolladores de DeepSeek optaron por utilizar menos bits para sus variables numéricas. Esta decisión permitió que su modelo ocupara significativamente menos memoria, logrando así un sistema más eficiente.
Tipado estático vs tipado dinámico
Si bien todos los lenguajes de programación cuentan con algún sistema de tipos, no todos lo manejan de la misma manera. En algunos, el sistema de tipos es explícito y obligatorio, pero en otros, existe de forma implícita y solo se verifica durante la ejecución. Estas diferencias nos llevan a dos enfoques principales:
Tipado estático
En los lenguajes con tipado estático como C
, Java
o Rust
, es necesario especificar el tipo de cada variable declarada. Una vez definido, ese tipo no puede cambiar a lo largo del programa. El compilador se encarga de verificar que todas las operaciones y funciones respeten estos tipos, lo que permite detectar errores incluso antes de ejecutar el código.
Tipado dinámico
Lenguajes como JavaScript
y Python
utilizan tipado dinámico. En ellos, el tipo de una variable se determina durante la ejecución del programa, e incluso puede contener distintos tipos de datos en diferentes momentos. Esta flexibilidad puede agilizar el desarrollo en un comienzo, pero también incrementa el riesgo de cometer errores si no se tiene suficiente cuidado.
¿Por qué queremos tipar?
Algunos desarrolladores consideran que la flexibilidad de tipos en el tipado dinámico es una de las principales virtudes de ciertos lenguajes, pero la verdad es que tipar el código va más allá de una simple formalidad. El tipado es una herramienta clave que mejora la calidad del código. En proyectos pequeños o funciones simples, puede parecer innecesario o incluso una pérdida de tiempo, pero adquirir el hábito de tipar desde el principio es beneficioso. En sistemas más complejos, los tipos permiten comprender rápidamente el propósito de las funciones con un simple vistazo, ya que definen claramente los tipos de entrada y salida. Además, reducen errores y facilitan la mantenibilidad. Cuando combinamos un tipado explícito con buenos nombres de variables y funciones, obtenemos un código claro y fácil de entender.
💡 Lineamiento: Tipar siempre las variables y las funciones.
Tipado en JavaScript y Python
Si bien JavaScript y Python utilizan tipado dinámico por defecto, originalmente no contaban con un sistema de tipos formal. Con el tiempo, a medida que los proyectos crecieron en complejidad, se hizo evidente la necesidad de incorporar herramientas de tipado para mejorar la claridad. Esto llevó al desarrollo de soluciones como TypeScript para JavaScript y anotaciones de tipos en Python, permitiendo un mayor control sobre los tipos sin perder la flexibilidad característica de estos lenguajes.
Las anotaciones de tipo de Python en el módulo typing
, son ayudas visuales que se incluyen en las variables, parámetros y funciones. Estas anotaciones no interfieren de ninguna manera con la ejecución del código pero sirven de guía tanto al desarrollador como a herramientas externas. En el siguiente fragmento de código, tenemos una función que devuelve un bool
con un parámetro de tipo List[int]
.
Por otro lado, para JavaScript se desarrolló TypeScript, un superconjunto del lenguaje que agrega tipado estático opcional entre otras mejoras. A diferencia de Python, TypeScript si detecta errores de tipos, esto lo hace al momento de transpilar el código a JavaScript, ya que TypeScript no se ejecuta directamente, sino que es convertido a un archivo .js
. En el siguiente fragmento de código podremos observar una implementación en TypeScript.
Recomendaciones al tipar
Terminamos esta sección con algunas recomendaciones para trabajar con tipo en los lenguajes que estamos utilizando. Estas recomendaciones deberían adaptarse siempre que sean posibles al lenguaje de programación que se esté utilizando.
Si bien el tipado es una herramienta muy útil con la que podemos contar, existe malas prácticas que muchos desarrolladores suelen cometer.
- Abusar del tipo
any
:En TypeScript, el tipoany
permite omitir la verificación de tipos, es decir que el transpilador no realizará ningún chequeo, entonces ¿para que utilizar tipos en primer lugar? Esto solo complica la lectura del código y aumenta el riesgo de errores. Si realmente no conocemos el tipo de dato de una variable es mejor utilizar el tipounknown
, lo cual indica claramente que el tipo de dato es desconocido sin perder seguridad en tiempo de compilación. En Python ocurre algo similar, el uso delAny
como tipo simplemente dificulta la tarea de otros desarrolladores. - Evitar el casteo de tipos: En TypeScript, el casteo de tipos nos permite forzar la interpretación de un dato como otro tipo sin modificar su valor real. A diferencia de Python, donde un
int()
ostr()
transforma efectivamente un dato, en TypeScript simplemente se le dice al transpilador que “confíe” en el programador. Esto puede ocultar errores, provocar inconsistencias y hacer que el código sea menos mantenible.
Por otro lado, con el tipado estático evitamos errores y hacemos nuestro código más claro y mantenible. Para aprovechar esta funcionalidad al máximo, es recomendable seguir algunas buenas prácticas
- Definir tipos personalizados: Tanto en TypeScript como en Python, podemos crear nuestros propios tipos mediante interfaces, clases o alias. Promueve la reutilización de estructuras de datos bien definidas y mejora la claridad.
- Validar tipos de fuentes desconocidas: Al trabajar con APIs, librerías de terceros o datos de origen desconocido, es fundamental validar los tipos para evitar errores. En TypeScript, librerías como
Zod
permiten definir esquemas de validación robustos, mientras que en Python, herramientas comoPydantic
facilitan la validación de datos en tiempo de ejecución. Si un dato no cumple con el formato esperado, estas herramientas permiten lanzar errores de manera controlada, evitando fallos más graves en el sistema.
Otras recomendaciones
Seguir las convenciones del lenguaje
Los desarrolladores son libres de escribir el código de la manera que ellos deseen siempre y cuando este funcione correctamente. Sin embargo cada lenguaje de programación cuenta con un conjunto de directrices que recomiendan estilos, prácticas y métodos para distintos aspectos del desarrollo. Estas convenciones buscan estandarizar la jerarquía y arquitectura de archivos y carpetas, las reglas para comentarios, y el formato de nombres y espaciado, entre otros aspectos.
Seguir estas convenciones ayuda a mantener la uniformidad en los proyectos de software. Si el código luce consistente en todos los archivos y módulos, será más fácil comprender su estructura y funcionamiento. Como resultado, el mantenimiento y la colaboración se simplifican. Aunque no es obligatorio seguir estas normas, conocerlas y aplicarlas es esencial para dominar un lenguaje por completo.
A continuación, se presentan las convenciones de nombres para funciones, variables, clases y otros elementos en Python y JavaScript.
Python
- Funciones: en minúsculas, con palabras separadas por guión bajo (snake_case). Ejemplo
my_function
. - Variables: siguen la misma convención que las funciones.
- Clases: cada palabra inicia con mayúscula y no se usan separadores (PascalCase). Ejemplo:
MyClass
. - Métodos: igual que las funciones, en snake_case.
- Constantes: igual que las funciones, pero completamente en mayúsculas (SCREAMING_SNAKE_CASE). Ejemplo
THIS_CONSTANT
. - Módulos: igual que las funciones, en snake_case.
- Paquetes: en minúsculas, sin guiones bajos. Ejemplo
mypackage
.
JavaScript
- Funciones: la primera palabra en minúscula, las siguientes con mayúscula inicial y sin separadores (camelCase). Ejemplo
myFunction
. - Variables: siguen la misma convención que las funciones.
- Clases: igual que en Python, usando PascalCase. Ejemplo:
MyClass
. - Métodos: igual que las funciones y variables, encamelCase.
- Constantes: se escriben en mayúsculas con guión bajo (SCREAMING_SNAKE_CASE), como en Python. Ejemplo
THIS_CONSTANT
. - Módulos: depende del tipo de proyecto y de archivo, en general no hay una convencion definida salvo casos especiales.
Ser consistentes en el uso del idioms
Elegir un idioma y mantenerlo a lo largo de un proyecto es fundamental para mantener la coherencia en el código. Si, por ejemplo, en un archivo utilizamos una variable counter
y luego en otro una variable contador
, estaremos creando una inconsistencia que puede generar confusion, especialmente en equipos de trabajos con hablantes de diferentes idiomas.
Por lo general, el inglés suele ser el idioma preferido para escribir código, ya que coincide con las palabras claves de la mayoría de los lenguajes de programación y además que facilita la comunicación e integración en equipos de trabajo multiculturales. Es importante evitar el uso de caracteres especiales como ñ, á, ü
ya que pueden provocar errores de compatibilidad o dificultar la escritura y comprensión del código. Por otro lado, gran parte de la documentación de lenguajes, librerías y APIs está en inglés, por lo que al elegir este idioma también facilitamos el acceso a recursos y buenas prácticas.