Programación funcional
⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.
r8vnhill/functional-programming-kt
Si tienes gh
instalado, puedes obtener el código haciendo:
gh repo clone r8vnhill/functional-programming-kt
cd functional-programming-kt || exit
git checkout base
Si quieres tener tu propia copia del código, puedes hacer un fork del repositorio y clonarlo desde tu cuenta de GitHub.
gh repo fork r8vnhill/functional-programming-kt
cd functional-programming-kt || exit
git checkout --track origin/base
group
en gradle.properties
.group
en el archivo gradle.properties
por tu nombre de dominio.Comencemos por definir una tarea base para esta unidad...
Primero definiremos una tarea reutilizable para esta unidad, no te dejes intimidar por la cantidad de código, es solo una plantilla para crear módulos en el proyecto que ya viene incluido en el repositorio. Sin embargo, comprender cómo funciona puede ser útil como práctica y repaso sobre tareas de Gradle.
- Parte 1
- Parte 2
- Parte 3
- Parte 4
- Parte 5
- Parte 6
- Parte 7
- Código completo
abstract class ModuleSetupTask @Inject constructor(
private val layout: ProjectLayout
) : DefaultTask() {
init {
group = "setup"
}
}
Esta clase define una tarea personalizada que hereda de DefaultTask
, como es habitual en Gradle.
Justo después del nombre de la clase, aparece una sección que empieza con @Inject constructor(...)
. Esto indica que la tarea necesita ciertos datos para funcionar, y que Gradle se encargará de proporcionarlos automáticamente. A esto se le llama inyección de dependencias.
En este caso, lo que Gradle entrega es una instancia de ProjectLayout, un objeto que ofrece una forma segura de acceder a carpetas y archivos dentro del proyecto.
Usar ProjectLayout
en lugar de acceder directamente con project.file(...)
es importante porque mejora la compatibilidad con el configuration cache, una característica de Gradle que permite acelerar las compilaciones.
Finalmente, la línea group = "setup"
simplemente indica que esta tarea forma parte del grupo de tareas de configuración, lo cual ayuda a organizarla mejor cuando se listan las tareas con gradle tasks
.
private val capturedProjectName: String = project.name
private val capturedGroup: String = project.rootProject.group.toString()
@get:Input
abstract val module: Property<String>
@get:Internal
val main: File
get() = baseDir.resolve("src/main/kotlin")
@get:Internal
val test: File
get() = baseDir.resolve("src/test/kotlin")
private val baseDir: File
get() = layout.projectDirectory.file(module.get().replace(":", "/")).asFile
Esta sección define las propiedades que la tarea necesita para generar la estructura de un submódulo:
capturedProjectName
ycapturedGroup
almacenan el nombre del proyecto actual y del grupo raíz, respectivamente. Estos valores se capturan en el momento en que se crea la instancia de la tarea, lo cual es importante porque acceder directamente aproject.*
en tiempo de ejecución es incompatible con el configuration cache de Gradle. Al capturarlos de forma anticipada, evitamos errores futuros y mejoramos la compatibilidad.module
es una propiedad de entrada (@get:Input
). Esto significa que su valor puede cambiar entre ejecuciones, y Gradle debe tenerlo en cuenta al decidir si necesita volver a ejecutar la tarea.main
ytest
son rutas internas calculadas que apuntan a los directoriossrc/main/kotlin
ysrc/test/kotlin
dentro del módulo. Se marcan con@get:Internal
porque su valor depende de otras propiedades (module
) y no necesitan ser consideradas directamente por Gradle como entradas externas al momento de evaluar si la tarea puede ser cacheada.baseDir
calcula el directorio raíz del módulo a partir demodule
y del layout del proyecto. Se accede usandolayout.projectDirectory
, una forma segura y declarativa de trabajar con rutas en Gradle moderna, evitando los métodos imperativos comoproject.file(...)
.
En conjunto, esta configuración busca asegurar compatibilidad con el configuration cache y mantener un diseño declarativo y limpio de la tarea.
private fun createSettingsFile() {
val settingsFile: File = layout.projectDirectory.file("settings.gradle.kts").asFile
if (!settingsFile.exists()) {
settingsFile.writeText(
"""
rootProject.name = "$capturedProjectName"
include("${module.get()}")
""".trimIndent()
)
} else if (module.get() !in settingsFile.readText()) {
settingsFile.appendText("\ninclude(\"${module.get()}\")")
}
println("The module ${module.get()} was added to the settings file")
}
Esta función agrega el nuevo módulo al archivo settings.gradle.kts
, que Gradle usa para registrar todos los subproyectos del build.
Primero se obtiene una referencia segura al archivo de configuración usando layout.projectDirectory.file(...)
, que es la forma recomendada en tareas compatibles con el configuration cache.
Luego, se evalúan dos casos:
- Si el archivo no existe, se crea con el nombre del proyecto (
rootProject.name
) y elinclude(...)
correspondiente al nuevo módulo. - Si el archivo ya existe pero aún no incluye el nuevo módulo, se agrega la línea
include(...)
al final.
El valor module.get()
se refiere al nombre del módulo proporcionado como entrada a la tarea. Al final, se imprime un mensaje indicando que el módulo fue agregado al archivo.
Esta lógica garantiza que el módulo será reconocido por Gradle la próxima vez que se ejecute el build.
private fun createModuleDirectory() = baseDir.run {
when {
exists() -> printError("Directory already exists: $absolutePath")
mkdirs() -> println("Directory created: $absolutePath")
else -> printError("Failed to create directory: $absolutePath")
}
}
Creamos una función privada createModuleDirectory
que se encarga de crear el directorio del módulo. Si el directorio ya existe, se imprime un mensaje de error. Si el directorio se crea correctamente, se imprime un mensaje de éxito. Si falla la creación del directorio, se imprime un mensaje de error.
private fun createBuildFile() = baseDir.resolve("build.gradle.kts").run {
if (exists()) printError("The build file already exists")
else {
writeText("// Intentionally left blank\n")
println("The build file was created successfully")
}
}
Creamos una función privada createBuildFile
que se encarga de crear el archivo de configuración build.gradle.kts
en el directorio del módulo. Si el archivo ya existe, se imprime un mensaje de error. Si el archivo se crea correctamente, se imprime un mensaje de éxito.
fun createFiles(packageName: String, vararg files: Pair<File, String>) {
files.forEach { (dir, name) ->
val packageDir = dir.resolve("$capturedGroup/$packageName".replace(".", "/"))
val file = packageDir.resolve(name)
if (file.exists()) {
printError("The file $name already exists")
} else {
packageDir.mkdirs()
file.writeText("package $capturedGroup.$packageName\n\n")
println("The file $name was created successfully")
}
}
}
Esta función crea uno o más archivos de Kotlin dentro de un paquete específico, organizándolos según la convención de directorios basada en el nombre del grupo (capturedGroup
) y el nombre del paquete recibido como argumento.
Para cada par (dir, name)
:
- Se construye el path final resolviendo el directorio base
dir
con la estructura del paquete (capturedGroup.packageName
), reemplazando los puntos por barras para reflejar la estructura de carpetas. - Si el archivo ya existe, se imprime un mensaje de error.
- Si no existe, se crean los directorios necesarios (
mkdirs()
) y se escribe un archivo con una declaraciónpackage
adecuada en la primera línea.
Esto es útil para generar archivos fuente organizados automáticamente dentro de la estructura estándar de Kotlin.
@TaskAction
fun setupSubmodule() {
createSettingsFile()
createModuleDirectory()
createBuildFile()
}
Creamos una función setupSubmodule
que se encarga de configurar el módulo. Esta función llama a las funciones createSettingsFile
, createModuleDirectory
y createBuildFile
para agregar el módulo al archivo de configuración, crear el directorio del módulo y crear el archivo de configuración del módulo, respectivamente.
package tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.ProjectLayout
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import java.io.File
import javax.inject.Inject
abstract class ModuleSetupTask @Inject constructor(
private val layout: ProjectLayout
) : DefaultTask() {
// Aquí guardamos el nombre del proyecto de forma inmutable
// en tiempo de configuración, para NO llamarlo en tiempo de ejecución.
private val capturedProjectName: String = project.name
private val capturedGroup: String = project.rootProject.group.toString()
@get:Input
abstract val module: Property<String>
@get:Internal
val main: File
get() = baseDir.resolve("src/main/kotlin")
@get:Internal
val test: File
get() = baseDir.resolve("src/test/kotlin")
private val baseDir: File
get() = layout.projectDirectory.file(module.get().replace(":", "/")).asFile
init {
group = "setup"
}
@TaskAction
fun setupSubmodule() {
createSettingsFile()
createModuleDirectory()
createBuildFile()
}
private fun createSettingsFile() {
val settingsFile: File = layout.projectDirectory.file("settings.gradle.kts").asFile
if (!settingsFile.exists()) {
settingsFile.writeText(
"""
rootProject.name = "$capturedProjectName"
include("${module.get()}")
""".trimIndent()
)
} else if (module.get() !in settingsFile.readText()) {
settingsFile.appendText("\ninclude(\"${module.get()}\")")
}
println("The module ${module.get()} was added to the settings file")
}
private fun createModuleDirectory() = baseDir.run {
when {
exists() -> printError("Directory already exists: $absolutePath")
mkdirs() -> println("Directory created: $absolutePath")
else -> printError("Failed to create directory: $absolutePath")
}
}
private fun createBuildFile() = baseDir.resolve("build.gradle.kts").run {
if (exists()) printError("The build file already exists")
else {
writeText("// Intentionally left blank\n")
println("The build file was created successfully")
}
}
fun createFiles(packageName: String, vararg files: Pair<File, String>) {
files.forEach { (dir, name) ->
val packageDir = dir.resolve("$capturedGroup/$packageName".replace(".", "/"))
val file = packageDir.resolve(name)
if (file.exists()) {
printError("The file $name already exists")
} else {
packageDir.mkdirs()
file.writeText("package $capturedGroup.$packageName\n\n")
println("The file $name was created successfully")
}
}
}
private fun printError(message: String) {
logger.error(message)
}
}
La programación funcional es un paradigma de desarrollo de software que se centra en construir programas mediante la aplicación y composición de funciones puras. A diferencia de la programación imperativa, que se enfoca en el cómo realizar una tarea a través de la modificación de estados y el control de flujo, la programación funcional adopta un enfoque más declarativo, especificando qué debe hacerse sin detallar los pasos exactos. En lugar de modificar el estado del programa, se utilizan funciones que transforman datos inmutables, facilitando el razonamiento y la predictibilidad del código.
Conceptos Clave
- Funciones Puras: Son funciones que, dado el mismo input, siempre producen el mismo output y no tienen efectos secundarios ni dependen de estados externos.
- Inmutabilidad: Una vez que se crea un dato, no cambia. En lugar de modificar estructuras existentes, se crean nuevas con las modificaciones necesarias.
- Funciones de Orden Superior: Funciones que pueden recibir otras funciones como argumentos y/o retornarlas como resultados.
- Composición de Funciones: Técnica para combinar funciones simples y construir operaciones más complejas de manera modular.
- Evaluación Perezosa: Las expresiones se evalúan solo cuando su valor es necesario, lo que puede mejorar la eficiencia y permitir estructuras de datos infinitas.
Orígenes
Este paradigma tiene sus raíces en el cálculo lambda, un sistema formal desarrollado por Alonzo Church en la década de 1930 que trata las funciones como entidades de primera clase. Aunque inicialmente no fue tan popular como la programación imperativa, hoy en día la programación funcional es ampliamente utilizada tanto en la industria como en la academia debido a sus beneficios en claridad, mantenibilidad y facilidad para la paralelización del código.
Lenguajes Funcionales Populares
Algunos lenguajes diseñados específicamente para la programación funcional incluyen:
- Haskell: Conocido por su fuerte sistema de tipos estático y la pureza de sus funciones.
- Lisp y sus dialectos como Scheme y Clojure: Pioneros en la programación funcional con una sintaxis basada en listas.
- OCaml y F#: Lenguajes que combinan programación funcional con características imperativas y orientadas a objetos.
- Erlang y Elixir: Utilizados especialmente en sistemas distribuidos y concurrentes, enfatizando la tolerancia a fallos y la escalabilidad.
Influencia en Otros Lenguajes
Muchos lenguajes de programación no funcionales han incorporado características funcionales en sus últimas versiones:
- JavaScript: Soporta funciones de primera clase, clausuras y funciones de orden superior, permitiendo un estilo funcional.
- Python: Incluye funciones anónimas (lambdas) y funciones integradas como
map
,filter
yreduce
. - Java: Desde la versión 8, introdujo expresiones lambda y el API de Streams para procesamiento funcional de datos.
- C#: Incorpora expresiones lambda y LINQ para consultas y transformaciones de datos de manera declarativa.
- Kotlin: Soporta funciones de primera clase, lambdas y una rica biblioteca funcional, promoviendo la inmutabilidad y la programación funcional.
Incluso lenguajes tradicionalmente imperativos como C++ han adoptado características funcionales, como funciones lambda y expresiones funcionales, para mejorar la expresividad y modularidad del código.
- Código Más Predecible: Al evitar efectos secundarios, es más fácil razonar sobre el comportamiento del código.
- Facilita el Paralelismo y la Concurrencia: La inmutabilidad reduce problemas asociados con estados compartidos en entornos concurrentes.
- Reutilización y Composición: Las funciones puras y la composición facilitan la creación de código modular y reutilizable.
- Menor Propensión a Errores: Al minimizar cambios de estado y efectos secundarios, se reducen los bugs difíciles de detectar.
¿Qué Aprendimos?
En esta lección, exploramos la programación funcional, un paradigma que se centra en la aplicación y composición de funciones puras para evitar efectos secundarios y cambios de estado. Vimos cómo este enfoque declarativo contrasta con la programación imperativa al priorizar la transformación de datos inmutables y la creación de funciones previsibles.
Aprendimos también sobre:
- Funciones puras: Garantizan el mismo resultado para un input dado sin alterar estados externos.
- Inmutabilidad: Los datos no cambian una vez creados, lo que mejora la predictibilidad.
- Funciones de orden superior: Permiten tratar las funciones como valores, facilitando la composición.
- Evaluación perezosa: Optimiza el rendimiento evaluando expresiones solo cuando es necesario.
Además, exploramos los lenguajes funcionales más destacados como Haskell y Clojure, y vimos cómo lenguajes tradicionales como JavaScript y Python han adoptado características funcionales.
Finalmente, discutimos los beneficios clave de la programación funcional, tales como su capacidad para facilitar el paralelismo, reducir errores, y mejorar la modularidad y reutilización del código.
Bibliografías Recomendadas
- 📚 "What is functional programming?". (2023). en Functional programming in Kotlin, (pp. 3–16.) Manning Publications Co.
- 📚 "Styles". (2024). en API Design for C++, (Second edition, pp. 179–207.) Morgan Kaufmann.
Bibliografías Adicionales
- 🌐 "What Is Functional Programming and Why It Is Important to Learn?." [En línea]. Disponible en: https://www.turing.com/kb/introduction-to-functional-programming