Aceptando a Julia Una Carta de Invitación

Aceptando a Julia Una Carta de Invitación

Cálidamente extendido a los amantes de Python, magos de la computación científica y científicos de datos

Julia es un lenguaje de programación de propósito general, dinámico, de alto rendimiento y de alto nivel que se compila en tiempo de ejecución. Es un lenguaje bastante reciente, con su versión principal 1.0 lanzada solo en 2018. En esta historia, pretendemos demostrar que este lenguaje vale la pena agregarlo a tu arsenal si te interesa la ciencia de datos, la computación científica o si eres un usuario ávido de Python. Puede que sea cierto que este sea el lenguaje de programación más hermoso que encontrarás.

Arte digital de una galaxia con planetas morados, verdes y rojos - Generado por el autor utilizando DALLE 2

En esta historia, exploraremos las alturas de las ideas con Julia y por qué vale la pena aprenderlo. Una vez que hayas terminado, te recomendamos encarecidamente que consultes la siguiente historia De Python a Julia: Una guía definitiva para realizar una transición fácil de Python a Julia.

Tabla de contenidos

· Julia es de alto nivelSintaxis básicaSintaxis elegante para matemáticas· Julia es rápidaBenchmarkEl problema de los dos lenguajesJulia se compila en tiempo real· Julia resuelve el problema de expresiónEl problema de expresiónDespacho múltipleTipos abstractos y concretos· Julia es totalmente funcionalSoporte para arreglosSoporte para cadenasMulti-threadingIntegración fácil con código CLa biblioteca estándar· Julia es de propósito generalIntroducciónAutomatización y scripting· Julia es altamente extensibleIntroducciónMacros· Conclusión

Foto de Daniele Levis Pelusi en Unsplash

Julia es de alto nivel

La introducción ya puede haberte dado la sensación de que esto será como Python, un lenguaje de propósito general, dinámico y de alto nivel. Para verificarlo, veamos cómo se ve el código básico de Julia en comparación con Python.

Sintaxis básica

Considera el siguiente juego de adivinanzas en Python:

import randomdef adivina_numero(max):    numero_aleatorio = random.randint(1, max)    print(f"Adivina un número entre 1 y {max}")    while True:        user_input = input()        intento = int(user_input)        if intento < numero_aleatorio:            print("Demasiado bajo")        elif intento > numero_aleatorio:            print("Demasiado alto")        else:            print("¡Correcto!")            breakadivina_numero(100)

El siguiente es el equivalente en Julia:

function adivina_numero(max::Integer)    numero_aleatorio = rand(1:100)    println("Adivina un número entre 1 y $max")    while true        user_input::String = readline()        intento = parse(Int, user_input)        if intento < numero_aleatorio            println("Demasiado bajo")        elseif intento > numero_aleatorio            println("Demasiado alto")        else            println("¡Correcto!")            break        end    endendadivina_numero(100)

Las principales diferencias aquí son que Julia no asume ninguna indentación ni requiere dos puntos, sino que requiere un “end” explícito para finalizar los ámbitos de construcciones como condiciones if, bucles y funciones. Te sentirás como en casa con esto si vienes de Matlab o Fortran.

Otra diferencia que puedes haber notado es que Julia naturalmente admite anotaciones de tipo en las declaraciones de variables, argumentos de funciones (y tipos de retorno, aunque rara vez se utilizan). Son siempre opcionales pero generalmente se usan para afirmaciones de tipo, lo que permite al compilador elegir la instancia de método correcta para llamar cuando el mismo método se sobrecarga para varios tipos y en algunos casos de declaración de variables y estructuras, para obtener beneficios de rendimiento.

Sintaxis elegante para matemáticas

# Expresiones elegantes x = 2z = 2y + 3x - 5# Soporte oficial de Unicodeα, β, γ = 1, 2, π/2# funciones en una líneaf(r) = π*r^2f'(3)  # derivada (con el paquete Flux.jl)# El vector columna es literalmente una columnav₁ = [1      2      3      4]  v₂ = [1 2 3 4]# transpuestoprintln(v1' == v2)# Esto es literalmente una matriz 3x3M⁽ⁱ⁾ = [1 2 3        4 5 7        7 8 9]# Modelo explícito de valores faltantesX = [1, 2, missing, 3, missing]

Una ventaja decisiva que tiene Julia sobre Python es el soporte de sintaxis para matemáticas. No es necesario utilizar * al multiplicar constantes por variables, se admiten símbolos latinos para los nombres de variables (es posible que necesites usar una extensión de VSCode para convertir \pi a π, v\_1 a v₁, etc.) y las matrices en general respetan el diseño en la definición de código.

Por ejemplo, si quisieras implementar el descenso de gradiente para una red neuronal.

En Python, probablemente escribirías:

import numpy as np# Descenso de gradiente en una red neuronalJ_del_B_n = [np.zeros(b) for b in B_n]J_del_W_n = [np.zeros(W) for W in W_n]for (x, y) in zip(x_batch, y_batch):    J_del_B_n_s, J_del_W_n_s = backprop(x, y)    J_del_B_n = [J_del_b + J_del_b_s for J_del_b,                 J_del_b_s in zip(J_del_B_n, J_del_B_n_s)]    J_del_W_n = [J_del_W + J_del_W_s for J_del_W,                 J_del_W_s in zip(J_del_W_n, J_del_W_n_s)]d = len(x_batch)W_n = [(1 - lambda_val * alpha / d) * W - lambda_val /       d * J_del_W for W, J_del_W in zip(W_n, J_del_W_n)]B_n = [(1 - lambda_val * alpha / d) * b - lambda_val /       d * J_del_b for b, J_del_b in zip(B_n, J_del_B_n)]

Compara la legibilidad de eso con lo que puedes escribir usando Julia:

# Descenso del gradiente en una red neuronalმJⳆმBₙ = [zeros(b) para b en Bₙ]მJⳆმWₙ = [zeros(W) para W en Wₙ]para (x, y) en zip(x_batch, y_batch)    მJⳆმBₙₛ, მJⳆმWₙₛ = backprop(x, y)    მJⳆმBₙ = [მJⳆმb + მJⳆმbₛ para მJⳆმb, მJⳆმbₛ en zip(მJⳆმBₙ, მJⳆმBₙₛ)]      მJⳆმWₙ = [მJⳆმW + მJⳆმWₛ para მJⳆმW, მJⳆმWₛ en zip(მJⳆმWₙ, მJⳆმWₙₛ)]d = len(x_batch)Wₙ = [(1 - λ*α/d)* W - λ/d * მJⳆმW para W, მJⳆმW en zip(Wₙ, მJⳆმWₙ)]Bₙ = [(1 - λ*α/d)* b - λ/d * მJⳆმb para b, მJⳆმb en zip(Bₙ, მJⳆმBₙ)]

Puedes intentar escribir código como este en Python, pero los editores a menudo pondrán cuadros amarillos alrededor de las variables Unicode (o no las resaltarán) y tu código puede no funcionar con paquetes de terceros como Pickle.

Foto de Solaiman Hossen en Unsplash

Julia es rápida

Otra razón importante por la que Julia puede considerarse el sueño hecho realidad de Python es que, a diferencia de Python, Ruby y otros lenguajes de alto nivel, no compromete la velocidad por ser de alto nivel. De hecho, puede ser tan rápido como lenguajes de bajo nivel como C y C++.

Benchmarks

Como referencia, a continuación se informa sobre el rendimiento de Julia, junto con otros lenguajes, en benchmarks de rendimiento populares:

Julia Microbenchmarks: Imagen a través de JuliaLang bajo licencia MIT

El problema de los dos lenguajes

Un corolario del rendimiento de Julia es que resuelve el problema de los dos lenguajes:

  • El código de investigación (por ejemplo, un modelo de aprendizaje automático) suele escribirse en un lenguaje de alto nivel como Python porque es de alto nivel e interactivo; por lo tanto, permite centrarse más en la ciencia (menos problemas de código) y permite más exploración.
  • Una vez que el código de investigación está finalizado, debe reescribirse en un lenguaje de bajo nivel como C antes de implementarlo en producción.

El problema aquí es que el mismo código debe reescribirse en más de un lenguaje. Esto generalmente es difícil y propenso a errores; considera si el código de investigación se modifica después de implementarse, en el peor de los casos todo tendrá que reescribirse en el lenguaje de bajo nivel nuevamente.

Una forma de solucionar este problema es escribir bibliotecas de alto rendimiento (por ejemplo, Numpy) en lenguajes de bajo nivel, como C, luego es posible envolverlos con funciones de Python que llamen internamente a las funciones en C que se pueden utilizar tanto para la investigación como para la producción sin preocuparse por el rendimiento. En realidad, esto es muy limitado porque:

  • Esto dificulta mucho que los nuevos desarrolladores contribuyan o colaboren con nuevos métodos científicos que hayan escrito, ya que pueden necesitar reescribirlos en un lenguaje de bajo nivel como C para mejorar su rendimiento antes de exponerlos en la biblioteca de alto nivel.
  • En el ámbito de la informática científica, se pueden imponer restricciones hilarantes a los desarrolladores del lenguaje de alto nivel. Por ejemplo, escribir bucles explícitos puede estar fuertemente desaconsejado.

Julia resuelve el problema de los dos lenguajes al ser de alto nivel, interactivo y bastante rápido, incluso para producción.

Julia se compila Just-in-time

Hay una pequeña nota relacionada con el rendimiento de Julia. Debido a que Julia se compila en tiempo de ejecución, la primera vez que se ejecute cualquier fragmento de código de Julia, se tardará más tiempo en completarse. Durante este tiempo, cada código de función se convertirá en código nativo (es decir, código que el procesador puede interpretar) para los tipos de variables específicos inferidos del código. Una vez que lo haga, almacenará en caché la representación compilada para que, si se llama a la función nuevamente con diferentes entradas del mismo tipo, se interprete de inmediato.

Para ampliar la explicación, para una función con N argumentos, hay un número posiblemente exponencial de posibles representaciones de código nativo; una para cada combinación posible de tipos para los N argumentos. Julia compilará la función a la representación que corresponda a los tipos inferidos del código la primera vez que se ejecute el código. Una vez hecho esto, las llamadas posteriores a la función serán sencillas. Tenga en cuenta que no necesariamente utiliza anotaciones de tipo (que son opcionales y pueden tener otros propósitos que mencionamos) durante la inferencia de tipo, los tipos se pueden inferir a partir de los valores en tiempo de ejecución de las entradas.

Esto no es un problema porque el código de investigación o el código que se ejecuta en un servidor solo tiene que compilar inicialmente una vez y una vez hecho eso, cualquier ejecución posterior (llamadas de API reales o experimentación adicional) del código será extremadamente rápida.

Foto de Thom Milkovic en Unsplash

Julia resuelve el problema de la expresión

El problema de la expresión

El problema de la expresión se trata de poder definir una abstracción de datos que sea extensible tanto en sus representaciones (es decir, tipos admitidos) como en sus comportamientos (es decir, métodos admitidos). Es decir, una solución al problema de la expresión permite:

  • Agregar nuevos tipos a los cuales se apliquen operaciones existentes
  • Agregar nuevas operaciones a las cuales se apliquen tipos existentes

sin violar el principio de abierto-cerrado (o causar otros problemas). Esto implica que debe ser posible agregar los nuevos tipos sin modificar el código de las operaciones existentes y debe ser posible agregar nuevas operaciones sin modificar el código de los tipos existentes.

Python, al igual que muchos otros lenguajes de programación, es orientado a objetos y no resuelve el problema de la expresión.

Supongamos que tenemos la siguiente abstracción de datos:

# Clase baseclass Shape:    def __init__(self, color):        pass    def area(self):        pass# Clase hijaclass Circle(Shape):    def __init__(self, radius):        super().__init__()        self.radius = radius    def area(self):        return 3.14 * self.radius * self.radius

Es muy fácil agregar nuevos tipos a los cuales se deben aplicar los métodos existentes. Solo hay que heredar de la clase base Shape. No requiere modificar ningún código existente:

class Rectangle(Shape):    def __init__(self, width, height):        super().__init__()        self.width = width        self.height = height    def area(self):        return self.width * self.height

Mientras tanto, no es fácil agregar operaciones a las cuales se apliquen los tipos existentes. Si queremos agregar un método perimeter, entonces tenemos que modificar la clase base y todas las clases hijas implementadas hasta ahora.

Una consecuencia de este problema es que si el paquete x es mantenido por el autor X y inicialmente admite el conjunto de operaciones Sx, y si otro conjunto de operaciones Sy es útil para otro conjunto de desarrolladores Y, ellos deben poder modificar el paquete de X para agregar estos métodos. En la práctica, los desarrolladores Y simplemente hacen otro paquete por su cuenta, posiblemente duplicando código del paquete x para implementar el tipo, porque el desarrollador X puede no estar contento con más código para mantener y Sy puede ser un género diferente de métodos que no tienen que estar en el mismo paquete.

Por otro lado, porque es fácil agregar nuevos tipos para los cuales se aplican operaciones existentes, si el desarrollador Y solo quiere definir un nuevo tipo que implemente operaciones en el tipo implementado por X, entonces podrían hacerlo fácilmente sin siquiera necesitar modificar el paquete x o duplicar código en él. Solo importar el tipo y luego heredar de él.

Múltiples Despachos

Para resolver el problema de la expresión, que permite una integración masiva entre paquetes diferentes, Julia elimina por completo la programación orientada a objetos tradicional. En lugar de clases, Julia utiliza definiciones de tipo abstracto, estructuras (instancias de tipos abstractos personalizados) y métodos y una técnica llamada múltiples despachos que, como veremos, resuelve perfectamente el problema de la expresión.

Para ver una equivalencia de lo que teníamos arriba:

### Tipo Abstracto "Forma" (Interfaz)
abstract type Shape end

function area(self::Shape)  end

### Tipo "Círculo" (Implementa la interfaz)
struct Circle <: Shape
    radius::Float64
end

function area(circle::Circle)
    return 3.14 * circle.radius^2
end

Aquí hemos definido un tipo abstracto “Forma”. El hecho de que sea abstracto implica que no se puede instanciar; sin embargo, otros tipos (clases) pueden heredar de él. Después, definimos un tipo de círculo, como un subtipo del tipo abstracto Forma y definimos el método area especificando que la entrada debe ser de tipo Círculo. De esta manera, podemos hacer:

c = Circle(3.0)
println(area(c))

Esto imprimiría 28.26. Aunque c satisface ambas definiciones de area porque también es una Forma, la segunda es más específica, por lo que es la que el compilador elige para la llamada.

Similar a la programación orientada a objetos basada en clases, es fácil agregar otro tipo “rectángulo” sin modificar el código existente:

struct Rectangle <: Shape
    length::Float64
    width::Float64
end

function area(rect::Rectangle)
    return rect.length * rect.width
end

Y ahora, cuando hacemos:

rect = Rectangle(3.0, 6.0)
println(area(rect))

Obtenemos 18.0. Esto es el múltiple despacho en acción; la instancia correcta del método area se asignó dinámicamente según el tipo de los argumentos en tiempo de ejecución. Si vienes de un entorno de C o C++, esto te debe recordar a la sobrecarga de funciones. La diferencia es que la sobrecarga de funciones no es dinámica, se basa en los tipos encontrados durante el tiempo de compilación. Por lo tanto, se pueden crear ejemplos donde su comportamiento es diferente.

Además, y a diferencia de la programación orientada a objetos basada en clases, podemos agregar métodos a cualquiera de los tipos Forma, Círculo o Rectángulo sin necesidad de modificar sus archivos. Si todos los archivos anteriores están en mi paquete y deseas agregar un conjunto de métodos que produzcan animaciones y gráficos 3D de las formas geométricas (lo cual no me interesa), todo lo que necesitas hacer es importar mi paquete. Ahora puedes acceder a los tipos Forma, Círculo y Rectángulo y puedes escribir las nuevas funciones y luego exportarlas en tu propio paquete “ShapeVisuals”.

### Definiciones de la interfaz
function animate(self::Shape)  end
function ThreeDify(self::Shape)  end

### Definiciones de Círculo
function animate(self::Circle)  ...end
function ThreeDify(self::Circle)  ...end

### Definiciones de Rectángulo
function animate(self::Rectangle)  ...end
function ThreeDify(self::Rectangle)  ...end

Cuando lo piensas, la distinción principal entre esto y la POO que conoces es que sigue el patrón func(obj, args) en lugar de obj.func(args). Como bonus, también facilita cosas como func(obj1, obj2, args). La otra distinción es que no encapsula métodos y datos juntos ni impone ninguna protección sobre ellos; tal vez sea una medida irrelevante cuando los desarrolladores son lo suficientemente maduros y el código se revisa de todos modos.

Tipos Abstractos y Concretos

El hecho de que ahora sabes que un tipo abstracto es simplemente un tipo del cual no puedes instanciar valores, pero que otros tipos pueden ser subtipos, abre camino para discutir el sistema de tipos de Julia. Recuerda que es opcional usar la sintaxis var::type para anotar los tipos de variables al momento de declararlas, como argumentos o retornos de funciones.

Cualquier tipo en Julia es abstracto o concreto. Los tipos concretos son aquellos que puedes instanciar, como los tipos personalizados que definimos anteriormente.

Julia tiene el siguiente sistema de tipos jerárquico para los números:

Julia Microbenchmarks: Imagen a través de Julia for Optimization and Learning bajo licencia MIT

Si tu función recibe un argumento y opera en cualquier tipo Number, usarás func(x::Number). Esto solo generará un error si se pasa un valor no numérico, como una cadena. Por otro lado, si solo funciona para cualquier tipo de punto flotante, usarías func(x::AbstractFloat). No se generará un error si la entrada es de tipo BigFloat, Float64, Floar32 o Floar16. Debido a que existe el despacho múltiple, también puedes definir otra instancia de la función func(x::Integer) para manejar el caso en el que el número dado sea un entero.

Julia tiene de manera similar un sistema de tipos jerárquico para otros tipos abstractos como AbstractString, pero son mucho más simples.

Foto de Paul Melki en Unsplash

Si lo piensas, Python solo viene con funcionalidad básica de fábrica. Por ejemplo, puedes hacer muy poco en ciencia de datos y computación científica si solo usas Python sin paquetes populares como Numpy. La gran mayoría de otros paquetes en el campo también dependen en gran medida de Numpy. Todos usan y asumen el tipo de array “Numpy” (en lugar del tipo de lista predeterminado de Python) como si fuera parte del lenguaje.

Julia no es así. Viene con muchas características importantes de fábrica, incluyendo:

Soporte para arrays

Julia cuenta con soporte para arrays similar a Numpy de fábrica, que incluye soporte para broadcasting y vectorización. Por ejemplo, lo siguiente compara operaciones populares de Numpy con cómo las escribirías nativamente en Julia:

#> 1. Creación de un array NumPy### Pythonarr = np.array([[1, 2, 3],                [4, 5, 6],                [7, 8, 9]])### Juliaarr = [1 2 3       4 5 6       7 8 9]#> 2. Obtener la forma de un array### Pythonshape = arr.shape### Juliashape = size(arr)#> 3. Reorganizar un array### Pythonreshaped_arr = arr.reshape(3, 3)### Juliareshaped_arr = reshape(arr, (3, 3))#> 4. Acceder a elementos por índice### Pythonelement = arr[1, 2]### Juliaelement = arr[1, 2]#> 5. Realizar operaciones aritméticas elemento a elemento### Pythonmultiplication = arr * 3### Juliamultiplication = arr .* 3# 6. Concatenación de arrays### Pythonarr1 = np.array([[1, 2, 3]])arr2 = np.array([[4, 5, 6]])concatenated_arr = np.concatenate((arr1, arr2), axis=0)### Juliaarr1 = [1 2 3]arr2 = [4 5 6]concatenated_arr = vcat(arr1, arr2)#> 7. Máscara booleana### Pythonmask = arr > 5masked_arr = arr[mask]### Juliamask = arr .> 5masked_arr = arr[mask]#> 8. Calcular la suma de los elementos de un array### Pythonmean_value = arr.sum()### Juliamean_value = sum(arr)

Soporte de cadenas

Julia también cuenta con un amplio soporte para cadenas y expresiones regulares:

nombre = "Alice"edad = 13## concatenacióngreeting = "¡Hola, " * nombre * "!"## interpolaciónmessage2 = "El próximo año tendrás $(edad + 1) años."## expresión regulartexto = "Aquí hay algunas direcciones de correo electrónico: [email protected]"# Define una regex para correosemail_pattern = r"[\w.-]+@[\w.-]+\.\w+"# match emailsdirecciones_correo = match(email_pattern, texto)"aby" > "abc"       # verdadero

Cuando se comparan cadenas, aquellas que están más adelante en el orden lexicográfico (orden alfabético general) se consideran mayores que aquellas que aparecen antes en el orden. Se puede demostrar que la mayoría de lo que se puede hacer con cadenas en lenguajes avanzados de procesamiento de cadenas como Perl, también se puede hacer en Julia.

Multi-threading

El hecho de que Python no admita un verdadero multi-threading paralelo se justifica porque viene con un Bloqueo Global del Interprete (GIL, por sus siglas en inglés). Esto impide que el intérprete ejecute múltiples hilos al mismo tiempo como una solución demasiado fácil para garantizar la seguridad de los hilos. Solo es posible alternar entre múltiples hilos (por ejemplo, si un hilo del servidor está ocupado esperando una solicitud de red, el intérprete puede alternar a otro hilo).

Afortunadamente, no es difícil liberar este bloqueo en programas en C llamados por Python, lo que explica por qué es posible Numpy. Sin embargo, si tienes un bucle de cálculo masivo, no puedes escribir código Python que lo ejecute en paralelo para acelerar el cálculo. La triste realidad para Python es que la gran mayoría de las operaciones matemáticas que se aplican a grandes estructuras de datos como matrices, son paralelizables.

Mientras tanto, en Julia, el multi-threading paralelo verdadero es compatible nativamente y es tan fácil como esto:

# Antes del multi-threadingfor i in eachindex(x)    y[i] = a * x[i] + y[i]end# Después del multi-threadingThreads.@threads for i in eachindex(x)    y[i] = a * x[i] + y[i]end

Cuando ejecutas el código, puedes especificar cuántos hilos quieres usar entre los disponibles en tu sistema.

Integración fácil con C Code

El proceso de llamar código C desde Julia está oficialmente soportado y puede hacerse de manera más eficiente y más fácil que en Python. Si quieres llamar

#include <stdio.h>int add(int a, int b) {    return a + b;}

entonces el paso principal (después de una configuración pequeña) para llamar esta función en Julia es escribir

# especificar función, tipo de retorno, tipos de argumentos y entrada. Prefijar tipos con "C"resultado = ccall(add, Cint, (Cint, Cint), 5, 3)

Es mucho más complicado hacer esto en Python y puede ser menos eficiente. Especialmente porque es mucho más fácil hacer coincidir los tipos y estructuras de Julia con los de C.

Una consecuencia importante de esto es que es posible ejecutar la gran mayoría de los lenguajes que pueden generar código de objeto C aquí en Julia. Por lo general, existen paquetes externos bien conocidos para eso. Por ejemplo, para llamar código Python puedes usar el paquete PyCall.jl de la siguiente manera:

using PyCallnp = pyimport("numpy")# Crea un arreglo de NumPy en Pythonpy_array = np.array([1, 2, 3, 4, 5])# Realiza algunas operaciones en el arreglo de NumPypy_mean = np.mean(py_array)py_sum = np.sum(py_array)py_max = np.max(py_array)

Casi no se necesita configuración previa para esto, aparte de instalar el paquete. También es posible llamar a funciones escritas en Fortran, C++, R, Java, Mathematica, Matlab, Node.js y más usando paquetes similares.

Por otro lado, es posible llamar a Julia desde Python, aunque no de una manera tan elegante. Esto probablemente se ha utilizado antes para acelerar funciones sin recurrir a implementarlas en C.

La Biblioteca Estándar

Un conjunto de paquetes viene preinstalado (pero debe ser cargado explícitamente) con Julia. Esto incluye los paquetes Statistics y LinearAlgebra, el paquete Downloads para acceder a Internet, y más importante aún, el paquete Distribued para computación distribuida (como Hadoop), también el paquete Profile para perfiles (ayuda a optimizar el código) y notablemente el paquete Tests para pruebas unitarias y el paquete Pkg para gestión de paquetes junto con muchos otros.

Tengo que decir que soy un usuario ávido de Python que ha desarrollado varios paquetes en Python. No hay comparación entre el paquete de terceros “Setuptools” en Python y Pkg en Julia, que es realmente mucho más limpio y fácil de usar. Nunca pude comprender por qué Python no tiene su propio sistema de gestión de paquetes y herramientas de prueba. Estas son necesidades realmente básicas en un lenguaje de programación.

Foto de Tom M en Unsplash

Julia es de Uso General

Introducción

Si has encontrado Julia en el pasado, sería natural pensar que Julia es un lenguaje específico de dominio donde el cálculo científico es el dominio. Es cierto que Julia ha sido diseñado cuidadosamente para ser expresivo y eficiente para el cálculo científico, pero eso no impide que sea un lenguaje de uso general. Es solo uno construido teniendo en cuenta el cálculo científico. Existen grados hasta los cuales un lenguaje puede ser de uso general. Por ejemplo, Julia se puede utilizar para ciencia de datos y aprendizaje automático, desarrollo web, automatización y scripting, robótica además del cálculo científico, pero aún no existen paquetes maduros que ayuden a los desarrolladores a utilizar Julia para cosas como el desarrollo de juegos similar a Pygame en Python. Incluso si el paquete de Julia Genie.jl está muy cerca de estar a la altura de Flask, puede quedarse corto en comparación con frameworks más completos como Django. En resumen, aunque Julia no sea tan de uso general como desearías en este momento, está construido teniendo eso en mente y se espera que lo sea eventualmente.

Automatización y Scripting

Habiendo mencionado que Julia se puede utilizar para automatización y scripting, vale la pena señalar que ayuda a hacerlo con una sintaxis elegante similar a la de la terminal.

Por ejemplo, aquí tienes un conjunto de operaciones de sistema de archivos y procesos que se pueden realizar en Julia:

# Crear un directoriomkdir("my_directory")# Cambiar el directorio de trabajocd("my_directory")# Listar archivos en el directorio actualprintln(readdir())# Eliminar el directoriorm("my_directory"; recursive=true)# Comprobar si un archivo existeif isfile("my_file.txt")    println("El archivo existe.")else    println("El archivo no existe.")end# Ejecutar un comando de terminal simple desde Juliarun(`echo "¡Hola, Julia!"`)# Capturar la salida de un comando de terminalresultado = read(`ls`, String)println("Contenido del directorio actual: $resultado")

Observa la similitud con lo que realmente escribes en la terminal.

Una alternativa al arte digital Noche estrellada — Generado por el autor usando DALLE 2

Julia es Extensivamente Extensible

Introducción

Una característica hermosa en el lenguaje de programación LISP es que es homoicónico. Esto significa que el código se puede tratar como datos y, por lo tanto, se pueden agregar nuevas características y semántica al lenguaje por parte de los desarrolladores comunes. Julia también fue construido para ser homoicónico. Por ejemplo, recuerda que dije que Julia solo admite la despacho múltiple. Bueno, parece que alguien ha creado un paquete ObjectOriented.jl que permite a los desarrolladores escribir POO en Julia. Como otro ejemplo, si creas un nuevo tipo, es fácil sobrecargar funciones y operadores base (que son solo funciones) para que funcionen con tu nuevo tipo.

Macros

El soporte de Julia para macros es una de las principales razones por las que esto es posible. Puedes pensar en una macro como una función que devuelve el código que se ejecutará durante el tiempo de análisis del programa. Supongamos que defines la siguiente macro:

macro add_seven(x)    quote        $x + 7    endend

Similar a una función, esto te permite llamarla de esta manera:

x = 5@add_seven x

lo cual devuelve 12. Lo que sucede en el fondo es que durante el tiempo de análisis (antes de la compilación) la macro se ejecuta, devolviendo el código 5 + 7 que durante el tiempo de compilación se evalúa como 12. Puedes pensar en las macros como una forma de realizar dinámicamente operaciones de CTRL+H (buscar y reemplazar).

Para otro caso de uso práctico, supongamos que tienes un paquete con 10 métodos útiles y quieres agregar una nueva interfaz al paquete, lo cual significa que debes escribir 10 estructuras, una para cada método. Supongamos que es sistemático escribir cualquiera de las estructuras dado el correspondiente función, entonces simplemente puedes escribir una sola macro donde iteras sobre las 10 funciones para generar código para las 10 estructuras. En efecto, el código que escribas será equivalente a escribir una sola estructura de una manera genérica, lo cual ahorra tiempo.

El hecho de que las macros sean posibles permite mucho más magia. Por ejemplo, si recuerdas lo que mencionamos anteriormente, pudimos multihilo un bucle for usando la macro Threads.@threads. Para medir el tiempo de ejecución de una llamada a una función, simplemente haces @time func(), si estás usando el paquete BenchmarkTools, entonces @benchmark func() llamaría a la función muchas veces para devolver estadísticas sobre el tiempo e incluso un pequeño gráfico. Si conoces qué es la memoización, incluso eso se puede aplicar a cualquier función con una simple macro @memoize. No es necesario modificarla de ninguna manera. Incluso hay @code_native func() que te mostraría el código nativo generado por la función, y hay otras macros que te muestran otras representaciones del código durante el proceso de compilación.

Conclusión

Resulta que todas las características del lenguaje de programación que hemos mencionado inicialmente eran parte del plan para Julia. Como se indica en el sitio web de Julia, esta es la visión del lenguaje:

“Queremos un lenguaje que sea de código abierto, con una licencia liberal. Queremos la velocidad de C con la dinamicidad de Ruby. Queremos un lenguaje que sea homoicónico, con verdaderas macros como Lisp, pero con una notación matemática obvia y familiar como Matlab. Queremos algo tan utilizable para programación general como Python, tan fácil para estadísticas como R, tan natural para el procesamiento de cadenas como Perl, tan poderoso para álgebra lineal como Matlab, tan bueno para unir programas como la terminal. Algo que sea fácil de aprender pero que mantenga a los hackers más serios felices. Queremos que sea interactivo y que esté compilado”.

Después de leer la historia, deberías poder reflexionar sobre cada palabra mencionada en la declaración de visión en este momento.

Espero que al leer esto hayas aprendido más sobre el lenguaje Julia y consideres aprenderlo. Hasta la próxima, au revoir.

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Aprendizaje Automático

CEO de NVIDIA Los creadores serán potenciados por la IA generativa.

La inteligencia artificial generativa “potenciará” a los creadores en todas las industrias y tipos de con...

Aprendizaje Automático

AI Modelos de Lenguaje y Visión de Gran Escala

Este artículo analiza la importancia de los modelos de lenguaje y visualización en la inteligencia artificial, sus ca...

Inteligencia Artificial

Una introducción práctica a los LLMs

Este es el primer artículo de una serie sobre el uso de Modelos de Lenguaje Grande (LLMs) en la práctica. Aquí daré u...

Inteligencia Artificial

Evaluar las solicitudes RAG con las RAGAs

Evaluando los componentes de recuperación y generación de un sistema de generación mejorado con recuperación (RAG) po...

Inteligencia Artificial

Ingenieros del MIT desarrollan réplica robótica del ventrículo derecho del corazón

Los ingenieros de la prestigiosa Universidad Tecnológica de Massachusetts (MIT) han desarrollado una revolucionaria r...