¿Qué es el estado de un proceso?
En Elixir, el estado será la información persistente que un proceso mantendrá como memoria a lo largo de la vida de ese proceso. Esto es especialmente importante cuando ese proceso se mantiene en ejecución durante un tiempo no trivial, por ejemplo, como parte de una arquitectura cliente-servidor.
Supón que pretendes implementar una caché usando procesos, de tal forma que luego le puedas mandar a un proceso mediante su PID una serie de mensajes como obtener de la caché, guardar en la caché o expirar de la caché. Será necesario que ese proceso mantenga la caché como tal. Además, es necesario que esa caché se conserve entre mensajes para que sea realmente útil durante la vida del programa.
Es importante tener en cuenta que esa memoria sería interna para el proceso que actúa de caché y que la única forma de obtenerla o manipularla desde el exterior sería mediante ese paso de mensajes del que se viene hablando desde capítulos anteriores.
Formas de mantener el estado de un proceso
En Elixir existen formas sofisticadas de que un proceso tenga estado. Como mínimo, lo mismo que os voy a contar en esta lección forma parte de la biblioteca estandar de Elixir mediante lo que se conoce como agentes.
Los GenServers también tienen estado. Los estudiaremos posteriormente en este curso, y traen un sistema más preciso para organizar el estado por parte de Elixir.
Sin embargo, puede ser útil, sobre todo para practicar el uso de las primitivas de control de procesos, hacer lo mismo a mano. Para ello, podremos utilizar funciones recursivas que vayan pasándose el estado como parámetro, o hacer uso de los diccionarios de proceso.
Mantener el estado como un parámetro
Una forma primitiva de crear un estado local entre procesos podría ser provocar que la función que se evalúa en el proceso aparte contenga como parámetro el estado del mismo. De este modo, la invocación de un proceso se acaba correspondiendo más con una función de la forma f(x1), donde x1 es el estado actual, y que una vez ha gestionado su mensaje invoca a f(x2) donde x2 sea el nuevo estado de la función, típicamente derivado a partir de x1.
Supongamos que queremos implementar un contador. Este proceso puede recibir una serie de mensajes: :get para obtener el valor actual del contador, :inc para incrementarlo, :dec para decrementarlo y :reset para volver a ponerlo a 0. Usando pattern matching, el esqueleto sería el siguiente:
def contador
receive do
:get -> IO.puts("Llaman a :get")
:inc -> IO.puts("Llaman a :inc")
:dec -> IO.puts("Llaman a :dec")
:reset -> IO.puts("Llaman a :reset")
end
contador
end
Hasta el momento no he metido nada en concreto, solamente he especificado cuatro posibles mensajes a recibir por parte de esta función cuando se esté evaluando en un proceso, y una llamada recursiva a contador para que vuelva a escuchar el siguiente mensaje.
Sin embargo, supongamos que ahora mantengo un parámetro con ese estado. Y supongamos también que el mensaje :get recibiese también el PID del proceso al que queremos responder el valor actual del contador. Entonces la función quedaría implementada del siguiente modo:
def contador(v)
v2 = receive do
{:get, pid} ->
send(pid, v)
v
{:inc} ->
v + 1
{:dec} ->
v - 1
{:reset} ->
0
end
contador(v2)
end
En este caso, estamos derivando un nuevo estado llamado v2 a partir de lo que haga el bloque receive, que sería el que usaría para la siguiente iteración y para la siguiente escucha. A tener en cuenta que en el caso de que el mensaje recibido sea un :get, le estoy enviando de vuelta al proceso que se me pase como parámetro (que vamos a imaginar es el remitente del mensaje recibido por contador) el valor actual de v.
Diccionarios de proceso
Los procesos en Elixir tienen un diccionario como parte de sus metadatos que sirve para guardar información de estado. Los diccionarios pertenecen al proceso y por lo tanto cada proceso tiene su propio diccionario independiente.
Estos diccionarios se comportan como otra colección parecida a una keyword list, pues consisten en un conjunto de pares clave-valor con la información que queramos guardar en ellos, referenciados por sus claves. Para interactuar con el diccionario, un proceso puede usar las siguientes primitivas que forman parte del módulo Process:
Process.put/2: permite establecer o sustituir un valor en el diccionario del proceso. Como primer parámetro, pondremos el nombre de la entrada de diccionario; como segundo parámetro le pondremos el valor a guardar. Por ejemplo, con una llamada aProcess.put(:id, "1234"), puedo colocar una entrada llamada:iden el diccionario, cuyo valor es"1234".Process.get: son una familia de funciones de varias aridades con las que se puede obtener un valor del diccionario.- Mediante
Process.get/1yProcess.get/2podemos sacar un valor del diccionario a partir de la clave con la que fue insertado al llamar aProcess.put. El segundo parámetro deProcess.getes opcional e indica lo que devolvería la función en caso de que el diccionario de proceso no posea una entrada con esa clave. Por defecto ese parámetro valenil, así que pedir una clave que no existe devolveránilo el segundo parámetro, si es que lo hemos puesto. - Mediante
Process.get/0(o sea, sin parámetros), se devolverán todos los elementos registrados en el diccionario.
- Mediante
Process.delete/1: permite borrar una entrada del diccionario a partir del nombre con el que previamente se guardó medianteProcess.put.
Como principal desventaja, los diccionarios se parecen más a una variable global, ya que Process.put y Process.delete no devuelven el nuevo estado, modifican el que tiene el proceso, por lo que es una solución que a lo mejor no debería ser usada si tenemos otros medios alternativos. Sin embargo, en algunas ocasiones puede ser útil, y por esa razón lo voy a incluir.
En cuanto a cómo usarlo, es bastante sencillo:
# Con `get/1` podemos recuperar el valor de una clave.
# Como no existe, devuelve nil.
iex(1)> Process.get(:identifier)
nil
# Podemos usar `put/2` para insertar o actualizar una clave.
iex(2)> Process.put(:identifier, "12341234")
nil
# Y esto provocará que futuras llamadas a `get/1` con esa clave funcionen.
iex(3)> Process.get(:identifier)
"12341234"
# También podemos borrar una entrada con `delete/1`.
iex(4)> Process.delete(:identifier)
"12341234"
# Eso hará que ya no exista al recuperarla con `get/1`.
iex(5)> Process.get(:identifier)
nil
# Podemos modificar el valor que se devuelve si no existe al usar `get/2`:
iex(6)> Process.get(:identifier, :NONE)
:NONE