Translate
martes, 2 de julio de 2024
Tipos personalizados en Gleam
lunes, 1 de julio de 2024
Funciones anónimas en Erlang
Las funciones anónimas, o funs, permiten declarar un tipo especial de función en línea, sin nombrarlas. Pueden hacer prácticamente todo lo que pueden hacer las funciones normales, excepto llamarse a sí mismas de forma recursiva (¿cómo podrían hacerlo si son anónimas?). Su sintaxis es:
fun(Args1) -> Expression1, Exp2, ..., ExpN;
(Args2) -> Expression1, Exp2, ..., ExpN;
(Args3) -> Expression1, Exp2, ..., ExpN
end
Y se puede utilizar de la siguiente manera:
7> Fn = fun() -> a end.
#Fun<erl_eval.20.67289768>
8> Fn().
a
9> hhfuns:map(fun(X) -> X + 1 end, L).
[2,3,4,5,6]
10> hhfuns:map(fun(X) -> X - 1 end, L).
[0,1,2,3,4]
¡Sostén el teléfono Batman! ¿Que está pasando aqui? Bueno, antes que nada, declaramos una función anónima asignada a PrepareAlarm. Esta función aún no se ha ejecutado: solo se ejecuta cuando PrepareAlarm("baño"). se llama. En ese momento se evalúa la llamada a io:format/2 y se emite el texto "Alarma configurada". La segunda expresión (otra función anónima) se devuelve a la persona que llama y luego se asigna a AlarmReady. Tenga en cuenta que en esta función, el valor de la variable Habitación se toma de la función 'principal' (PrepareAlarm). Esto está relacionado con un concepto llamado cierres.
Para entender los cierres, primero hay que entender el alcance. El alcance de una función se puede imaginar como el lugar donde se almacenan todas las variables y sus valores. En la función base(A) -> B = A + 1., A y B están definidos como parte del alcance de base/1. Esto significa que en cualquier lugar dentro de base/1, puede hacer referencia a A y B y esperar que se les vincule un valor. Y cuando digo "en cualquier lugar", no bromeo, chico; Esto también incluye funciones anónimas:
base(A) ->
B = A + 1,
F = fun() -> A * B end,
F().
B y A todavía están vinculados al alcance de base/1, por lo que la función F aún puede acceder a ellos. Esto se debe a que F hereda el alcance de base/1. Como la mayoría de los tipos de herencia en la vida real, los padres no pueden obtener lo que tienen los hijos:
base(A) ->
B = A + 1,
F = fun() -> C = A * B end,
F(),
C.
En esta versión de la función, B sigue siendo igual a A + 1 y F seguirá ejecutándose bien. Sin embargo, la variable C solo está en el alcance de la función anónima en F. Cuando base/1 intenta acceder al valor de C en la última línea, solo encuentra una variable independiente. De hecho, si hubiera intentado compilar esta función, el compilador habría dado un error. La herencia sólo va en un sentido.
Es importante tener en cuenta que el alcance heredado sigue a la función anónima dondequiera que esté, incluso cuando se pasa a otra función:
a() ->
Secret = "pony",
fun() -> Secret end.
b(F) ->
"a/0's password is "++F().
Tuplas en Gleam
import gleam/io
pub fn main() {
let triple = #(1, 2.2, "three")
io.debug(triple)
let #(a, _, _) = triple
io.debug(a)
io.debug(triple.1)
}
Funciones de orden superior en erlang
Una parte importante de todos los lenguajes de programación funcionales es la capacidad de tomar una función y pasarla como parámetro a otra función. Esto, a su vez, vincula ese parámetro de función a una variable que puede usarse como cualquier otra variable dentro de la función. Una función que puede aceptar otras funciones transportadas de esa manera se denomina función de orden superior. Las funciones de orden superior son un poderoso medio de abstracción y una de las mejores herramientas para dominar Erlang.
Nuevamente, este es un concepto arraigado en las matemáticas, principalmente en el cálculo lambda. No entraré en muchos detalles sobre el cálculo lambda porque a algunas personas les cuesta entenderlo y está un poco fuera de alcance. Sin embargo, lo definiré brevemente como un sistema donde todo es una función, incluso los números. Debido a que todo es una función, las funciones deben aceptar otras funciones como parámetros y pueden operar sobre ellas con aún más funciones.
Muy bien, esto puede resultar un poco extraño, así que comencemos con un ejemplo:
-module(hhfuns).
-compile(export_all).
one() -> 1.
two() -> 2.
add(X,Y) -> X() + Y().
Ahora abra el shell de Erlang, compile el módulo y comience:
1> c(hhfuns).
{ok, hhfuns}
2> hhfuns:add(one,two).
** exception error: bad function one
in function hhfuns:add/2
3> hhfuns:add(1,2).
** exception error: bad function 1
in function hhfuns:add/2
4> hhfuns:add(fun hhfuns:one/0, fun hhfuns:two/0).
3
¿Confuso? No tanto, una vez que sabes cómo funciona (¿no es siempre así?) En el comando 2, los átomos uno y dos se pasan a add/2, que luego usa ambos átomos como nombres de funciones (X() + Y ()). Si los nombres de las funciones se escriben sin una lista de parámetros, esos nombres se interpretan como átomos y los átomos no pueden ser funciones, por lo que la llamada falla. Esta es la razón por la que la expresión 3 también falla: los valores 1 y 2 tampoco se pueden llamar funciones, ¡y lo que necesitamos son funciones!
Es por eso que se debe agregar una nueva notación al lenguaje para permitirle pasar funciones desde fuera de un módulo. Esto es lo divertido que es Módulo: Función/Arity: le dice a la VM que use esa función específica y luego la vincule a una variable.
Entonces, ¿cuáles son las ventajas de utilizar funciones de esa manera? Bueno, quizá sea necesario un pequeño ejemplo para entenderlo. Agregaremos algunas funciones a hhfuns que funcionan de forma recursiva en una lista para sumar o restar uno de cada número entero de una lista:
increment([]) -> [];
increment([H|T]) -> [H+1|increment(T)].
decrement([]) -> [];
decrement([H|T]) -> [H-1|decrement(T)].
¿Ves cuán similares son estas funciones? Básicamente hacen lo mismo: recorren una lista, aplican una función en cada elemento (+ o -) y luego se llaman a sí mismos nuevamente. Casi nada cambia en ese código: solo la función aplicada y la llamada recursiva son diferentes. El núcleo de una llamada recursiva en una lista como esa es siempre el mismo. Resumiremos todas las partes similares en una sola función (mapa/2) que tomará otra función como argumento:
map(_, []) -> [];
map(F, [H|T]) -> [F(H)|map(F,T)].
incr(X) -> X + 1.
decr(X) -> X - 1.
Que luego se puede probar en el shell:
1> c(hhfuns).
{ok, hhfuns}
2> L = [1,2,3,4,5].
[1,2,3,4,5]
3> hhfuns:increment(L).
[2,3,4,5,6]
4> hhfuns:decrement(L).
[0,1,2,3,4]
5> hhfuns:map(fun hhfuns:incr/1, L).
[2,3,4,5,6]
6> hhfuns:map(fun hhfuns:decr/1, L).
[0,1,2,3,4]
Aquí los resultados son los mismos, ¡pero acabas de crear una abstracción muy inteligente! Cada vez que quieras aplicar una función a cada elemento de una lista, solo tienes que llamar a map/2 con tu función como parámetro. Sin embargo, es un poco molesto tener que poner cada función que queremos pasar como parámetro a map/2 en un módulo, nombrarla, exportarla, luego compilarla, etc. De hecho, es claramente poco práctico. Lo que necesitamos son funciones que puedan declararse sobre la marcha...
viernes, 28 de junio de 2024
Guards en Gleam
import gleam/io
pub fn main() {
let numbers = [1, 2, 3, 4, 5]
io.debug(get_first_larger(numbers, 3))
io.debug(get_first_larger(numbers, 5))
}
fn get_first_larger(numbers: List(Int), limit: Int) -> Int {
case numbers {
[first, ..] if first > limit -> first
[_, ..rest] -> get_first_larger(rest, limit)
[] -> 0
}
}
La palabra clave if se puede utilizar con expresiones para agregar una protección a un patrón. Una guardia es una expresión que debe evaluarse como Verdadera para que el patrón coincida.
Solo se puede utilizar un conjunto limitado de operadores y no se puede invocar ninguna función.
domingo, 23 de junio de 2024
Árboles binarios en Erlang
En primer lugar, es importante definir qué es un árbol. En nuestro caso, son nodos hasta abajo. Los nodos son tuplas que contienen una clave, un valor asociado a la clave y luego otros dos nodos. De estos dos nodos, necesitamos uno que tenga una clave más pequeña y otro que tenga una clave más grande que el nodo que los contiene. ¡Así que aquí está la recursividad! Un árbol es un nodo que contiene nodos, cada uno de los cuales contiene nodos, que a su vez también contienen nodos. Esto no puede continuar para siempre (no tenemos infinitos datos para almacenar), por lo que diremos que nuestros nodos también pueden contener nodos vacíos.
Para representar nodos, las tuplas son una estructura de datos apropiada. Para nuestra implementación, podemos definir estas tuplas como {node, {Key, Value, Smaller, Larger}} , donde Smaller y Larger pueden ser otro nodo similar o un nodo vacío ({nodo, nil} ).
Comencemos a construir un módulo para nuestra implementación de árbol muy básica. La primera función, vacía/0, devuelve un nodo vacío. El nodo vacío es el punto de partida de un nuevo árbol, también llamado raíz:
-module(tree).
-export([empty/0, insert/3, lookup/2]).
empty() -> {node, 'nil'}.
Al usar esa función y luego encapsular todas las representaciones de nodos de la misma manera, ocultamos la implementación del árbol para que la gente no necesite saber cómo está construido. Toda esa información puede estar contenida únicamente en el módulo. Si alguna vez decide cambiar la representación de un nodo, puede hacerlo sin romper el código externo.
Para agregar contenido a un árbol, primero debemos entender cómo navegar recursivamente a través de él. Procedamos de la misma manera que lo hicimos con todos los demás ejemplos de recursividad, intentando encontrar el caso base. Dado que un árbol vacío es un nodo vacío, nuestro caso base es lógicamente un nodo vacío. Entonces, cada vez que lleguemos a un nodo vacío, ahí es donde podemos agregar nuestra nueva clave/valor. El resto del tiempo, nuestro código tiene que recorrer el árbol intentando encontrar un nodo vacío donde poner contenido.
Para encontrar un nodo vacío comenzando desde la raíz, debemos aprovechar el hecho de que la presencia de nodos más pequeños y más grandes nos permite navegar comparando la nueva clave que tenemos que insertar con la clave del nodo actual. Si la nueva clave es más pequeña que la clave del nodo actual, intentamos encontrar el nodo vacío dentro de Smaller, y si es más grande, dentro de Larger. Sin embargo, hay un último caso: ¿qué pasa si la nueva clave es igual a la clave del nodo actual? Allí tenemos dos opciones: dejar que el programa falle o reemplazar el valor por el nuevo. Esta es la opción que tomaremos aquí. Poner en una función toda esta lógica funciona de la siguiente manera:
insert(Key, Val, {node, 'nil'}) ->
{node, {Key, Val, {node, 'nil'}, {node, 'nil'}}};
insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey < Key ->
{node, {Key, Val, insert(NewKey, NewVal, Smaller), Larger}};
insert(NewKey, NewVal, {node, {Key, Val, Smaller, Larger}}) when NewKey > Key ->
{node, {Key, Val, Smaller, insert(NewKey, NewVal, Larger)}};
insert(Key, Val, {node, {Key, _, Smaller, Larger}}) ->
{node, {Key, Val, Smaller, Larger}}.
Tenga en cuenta aquí que la función devuelve un árbol completamente nuevo. Esto es típico de los lenguajes funcionales que tienen una sola asignación. Si bien esto puede considerarse ineficiente, la mayoría de las estructuras subyacentes de dos versiones de un árbol a veces resultan ser las mismas y, por lo tanto, la VM las comparte y las copia solo cuando es necesario.
Lo que queda por hacer en esta implementación de árbol de ejemplo es crear una función de búsqueda/2 que le permitirá encontrar un valor de un árbol proporcionando su clave. La lógica necesaria es extremadamente similar a la que se usa para agregar contenido nuevo al árbol: recorremos los nodos y verificamos si la clave de búsqueda es igual, menor o mayor que la clave del nodo actual. Tenemos dos casos base: uno cuando el nodo está vacío (la clave no está en el árbol) y otro cuando se encuentra la clave. Como no queremos que nuestro programa falle cada vez que buscamos una clave que no existe, devolveremos el átomo "indefinido". De lo contrario, devolveremos {ok, Valor}. La razón de esto es que si solo devolviéramos Valor y el nodo contuviera el átomo 'indefinido', no tendríamos forma de saber si el árbol devolvió el valor correcto o no pudo encontrarlo. Al agrupar los casos exitosos en una tupla de este tipo, facilitamos la comprensión de cuál es cuál. Aquí está la función implementada:
lookup(_, {node, 'nil'}) ->
undefined;
lookup(Key, {node, {Key, Val, _, _}}) ->
{ok, Val};
lookup(Key, {node, {NodeKey, _, Smaller, _}}) when Key < NodeKey ->
lookup(Key, Smaller);
lookup(Key, {node, {_, _, _, Larger}}) ->
lookup(Key, Larger).
Y hemos terminado. Probémoslo haciendo una pequeña libreta de direcciones de correo electrónico. Compile el archivo e inicie el shell:
1> T1 = tree:insert("Jim Woodland", "jim.woodland@gmail.com", tree:empty()).
{node,{"Jim Woodland","jim.woodland@gmail.com",
{node,nil},
{node,nil}}}
2> T2 = tree:insert("Mark Anderson", "i.am.a@hotmail.com", T1).
{node,{"Jim Woodland","jim.woodland@gmail.com",
{node,nil},
{node,{"Mark Anderson","i.am.a@hotmail.com",
{node,nil},
{node,nil}}}}}
3> Addresses = tree:insert("Anita Bath", "abath@someuni.edu", tree:insert("Kevin Robert", "myfairy@yahoo.com", tree:insert("Wilson Longbrow", "longwil@gmail.com", T2))).
{node,{"Jim Woodland","jim.woodland@gmail.com",
{node,{"Anita Bath","abath@someuni.edu",
{node,nil},
{node,nil}}},
{node,{"Mark Anderson","i.am.a@hotmail.com",
{node,{"Kevin Robert","myfairy@yahoo.com",
{node,nil},
{node,nil}}},
{node,{"Wilson Longbrow","longwil@gmail.com",
{node,nil},
{node,nil}}}}}}}
Y ahora puedes buscar direcciones de correo electrónico con él:
4> tree:lookup("Anita Bath", Addresses).
{ok, "abath@someuni.edu"}
5> tree:lookup("Jacques Requin", Addresses).
undefined
¡Esto concluye nuestro ejemplo de libreta de direcciones funcional construida a partir de una estructura de datos recursiva distinta de una lista!
viernes, 21 de junio de 2024
Hola mundo en Vaadin
Vamos a hacer un hola mundo en Vaadin. Yo voy a dar por sobreentendido que sabemos crear un proyecto con spring. Si no lo sabes es tan fácil como ir a https://start.spring.io/ y de ahi vas configurando las cosas y agregamos el framework vaadin.
Luego creamos un nuevo paquete como por ejemplo : com.example.vaadin.ui. Dentro del paquete, creamos una nueva clase MainView.java
package com.example.vaadin.ui;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@Route("")
public class MainView extends VerticalLayout {
public MainView() {
Button button = new Button("Haz clic aquí",
event -> Notification.show("¡Hola, Mundo!"));
add(button);
}
}
martes, 18 de junio de 2024
Inyectar condicionalmente un bean con profiles
Tengo 2 beans que implementan una interfaz, por ejemplo un repositorio jpa con una base de datos y otro con archivos o en memoria, ponele... Y quiero algunas veces utilizar un bean y otras otro dependiendo de una configuración, esto se puede resolver de 2 maneras. Se puedes utilizar la anotación @Conditional o @Profile.
Veamos ejemplo utilizando profile:
public interface MyService {
void performService();
}
@Service
@Profile("serviceA")
public class ServiceA implements MyService {
@Override
public void performService() {
System.out.println("Service A implementation");
}
}
@Service
@Profile("serviceB")
public class ServiceB implements MyService {
@Override
public void performService() {
System.out.println("Service B implementation");
}
}
lunes, 17 de junio de 2024
Alias en patrones en Gleam
import gleam/io
pub fn main() {
io.debug(get_first_non_empty([[], [1, 2, 3], [4, 5]]))
io.debug(get_first_non_empty([[1, 2], [3, 4, 5], []]))
io.debug(get_first_non_empty([[], [], []]))
}
fn get_first_non_empty(lists: List(List(t))) -> List(t) {
case lists {
[[_, ..] as first, ..] -> first
[_, ..rest] -> get_first_non_empty(rest)
[] -> []
}
}
El operador as se puede utilizar para asignar subpatrones a variables.
El patrón [_, ..] como primero coincidirá con cualquier lista que no esté vacía y asignará esa lista a la variable first.
domingo, 16 de junio de 2024
Quicksort en Erlang
Una implementación ingenua de Quicksort funciona tomando el primer elemento de una lista, el pivote, y luego colocando todos los elementos más pequeños o iguales al pivote en una nueva lista, y todos los más grandes en otra lista. Luego tomamos cada una de estas listas y hacemos lo mismo con ellas hasta que cada lista se hace cada vez más pequeña. Esto continúa hasta que no tenga nada más que una lista vacía para ordenar, que será nuestro caso base. Se dice que esta implementación es ingenua porque las versiones más inteligentes de Quicksort intentarán elegir pivotes óptimos para ser más rápidos. Sin embargo, eso realmente no nos importa para nuestro ejemplo.
Necesitaremos dos funciones para esto: una primera función para dividir la lista en partes más pequeñas y más grandes y una segunda función para aplicar la función de partición en cada una de las nuevas listas y unirlas. En primer lugar, escribiremos la función de pegamento:
quicksort([]) -> [];
quicksort([Pivot|Rest]) ->
{Smaller, Larger} = partition(Pivot,Rest,[],[]),
quicksort(Smaller) ++ [Pivot] ++ quicksort(Larger).
Esto muestra el caso base, una lista ya dividida en partes más grandes y más pequeñas por otra función, el uso de un pivote con ambas listas ordenadas rápidamente añadidas antes y después. Entonces esto debería encargarse de armar listas. Ahora la función de partición:
partition(_,[], Smaller, Larger) -> {Smaller, Larger};
partition(Pivot, [H|T], Smaller, Larger) ->
if H =< Pivot -> partition(Pivot, T, [H|Smaller], Larger);
H > Pivot -> partition(Pivot, T, Smaller, [H|Larger])
end.
Y ahora puede ejecutar su función de clasificación rápida. Si ha buscado ejemplos de Erlang en Internet antes, es posible que haya visto otra implementación de clasificación rápida, una que es más simple y fácil de leer, pero que utiliza listas por comprensión. Las piezas fáciles de reemplazar son las que crean nuevas listas, la función partición/4:
lc_quicksort([]) -> [];
lc_quicksort([Pivot|Rest]) ->
lc_quicksort([Smaller || Smaller <- Rest, Smaller =< Pivot])
++ [Pivot] ++
lc_quicksort([Larger || Larger <- Rest, Larger > Pivot]).
Las principales diferencias son que esta versión es mucho más fácil de leer, pero a cambio, tiene que recorrer la lista dos veces para dividirla en dos partes. Esta es una lucha entre la claridad y el rendimiento, pero el verdadero perdedor aquí eres tú, porque ya existe una función listas:ordenar/1. Usa ese en su lugar.
Toda esta concisión es buena para fines educativos, pero no para el rendimiento. ¡Muchos tutoriales de programación funcional nunca mencionan esto! En primer lugar, aquí ambas implementaciones deben procesar valores que sean iguales al pivote más de una vez. En su lugar, podríamos haber decidido devolver 3 listas: elementos más pequeños, más grandes e iguales al pivote para hacerlo más eficiente.
viernes, 14 de junio de 2024
Patrones alternativos en Gleam
import gleam/int
import gleam/io
pub fn main() {
let number = int.random(10)
io.debug(number)
let result = case number {
2 | 4 | 6 | 8 -> "This is an even number"
1 | 3 | 5 | 7 -> "This is an odd number"
_ -> "I'm not sure"
}
io.debug(result)
}
Se puede realizar una acción con varias cohicidencias de patrones con el operador | . Si alguno de los patrones coincide, entonces la cláusula coincide.
Si un patrón define una variable, entonces todos los patrones alternativos para esa cláusula también deben definir una variable con el mismo nombre y el mismo tipo.
Actualmente no es posible tener patrones alternativos anidados, por lo que el patrón [1 | 2 | 3] no es válido.
martes, 11 de junio de 2024
Patrones con múltiples valores en Gleam
import gleam/int
import gleam/io
pub fn main() {
let x = int.random(2)
let y = int.random(2)
io.debug(x)
io.debug(y)
let result = case x, y {
0, 0 -> "Both are zero"
0, _ -> "First is zero"
_, 0 -> "Second is zero"
_, _ -> "Neither are zero"
}
io.debug(result)
}
A veces necesitaamos patrones en múltiples valores. Para hacer esto, puedes dar múltiples temas y múltiples patrones, separados por comas.
Al hacer coincidir varios temas, debe haber la misma cantidad de patrones que temas.
Más Funciones Recursivas en Erlang parte 4
Para llevar las cosas un poco más allá, escribiremos una función de compresión. Una función de compresión tomará dos listas de la misma longitud como parámetros y las unirá como una lista de tuplas que contienen dos términos. Nuestra propia función zip/2 se comportará de esta manera:
1> recursive:zip([a,b,c],[1,2,3]).
[{a,1},{b,2},{c,3}]
Dado que queremos que nuestros parámetros tengan la misma longitud, el caso base comprimirá dos listas vacías:
zip([],[]) -> [];
zip([X|Xs],[Y|Ys]) -> [{X,Y}|zip(Xs,Ys)].
Sin embargo, si desea una función zip más indulgente, puede decidir que finalice cada vez que finalice una de las dos listas. Por lo tanto, en este escenario, tiene dos casos base:
lenient_zip([],_) -> [];
lenient_zip(_,[]) -> [];
lenient_zip([X|Xs],[Y|Ys]) -> [{X,Y}|lenient_zip(Xs,Ys)].
Observe que no importa cuáles sean nuestros casos base, la parte recursiva de la función sigue siendo la misma.
La recursividad de cola como se ve aquí no hace que la memoria crezca porque cuando la máquina virtual ve una función que se llama a sí misma en una posición de cola (la última expresión que se evaluará en una función), elimina el marco de pila actual. Esto se denomina optimización de llamada final (TCO) y es un caso especial de una optimización más general denominada optimización de última llamada (LCO).
LCO se realiza siempre que la última expresión que se evaluará en el cuerpo de una función es otra llamada de función. Cuando eso sucede, al igual que con el TCO, Erlang VM evita almacenar el marco de la pila. Como tal, la recursividad de cola también es posible entre múltiples funciones. Como ejemplo, la cadena de funciones a() -> b(). b()->c(). c()->a(). creará efectivamente un bucle infinito que no se quedará sin memoria ya que LCO evita el desbordamiento de la pila. Este principio, combinado con nuestro uso de acumuladores, es lo que hace que la recursividad de cola sea útil.
lunes, 10 de junio de 2024
Más Funciones Recursivas en Erlang parte 3
Otra función a implementar podría ser sublist/2, que toma una lista L y un número entero N, y devuelve los N primeros elementos de la lista. Como ejemplo, sublista([1,2,3,4,5,6],3) devolvería [1,2,3]. Nuevamente, el caso base intenta obtener 0 elementos de una lista. Sin embargo, tenga cuidado, porque sublist/2 es un poco diferente. ¡Tienes un segundo caso base cuando la lista aprobada está vacía! Si no verificamos si hay listas vacías, se generará un error al llamar a recursive:sublist([1],2). mientras que queremos [1] en su lugar. Una vez definido esto, la parte recursiva de la función solo tiene que recorrer la lista, manteniendo los elementos a medida que avanza, hasta llegar a uno de los casos base:
sublist(_,0) -> [];
sublist([],_) -> [];
sublist([H|T],N) when N > 0 -> [H|sublist(T,N-1)].
Que luego se puede transformar a una forma recursiva de cola de la misma manera que antes:
tail_sublist(L, N) -> tail_sublist(L, N, []).
tail_sublist(_, 0, SubList) -> SubList;
tail_sublist([], _, SubList) -> SubList;
tail_sublist([H|T], N, SubList) when N > 0 -> tail_sublist(T, N-1, [H|SubList]).
domingo, 9 de junio de 2024
Recursividad de listas en Gleam
import gleam/io
pub fn main() {
let sum = sum_list([18, 56, 35, 85, 91], 0)
io.debug(sum)
}
fn sum_list(list: List(Int), total: Int) -> Int {
case list {
[first, ..rest] -> sum_list(rest, total + first)
[] -> total
}
}
Si bien es más común usar funciones en el módulo gleam/list para iterar a través de una lista, en ocasiones es posible que prefieras trabajar con la lista directamente.
El patrón [primero, ..rest] coincide en una lista con al menos un elemento, asignando primero el primer elemento a la variable y el resto de la lista a la variable resto. Al usar este patrón y un patrón para la lista vacía [], una función puede ejecutar código en cada elemento de una lista hasta llegar al final.
Este código suma una lista recurriendo a la lista y agregando cada int a un argumento total, devolviéndolo cuando se llega al final.