Diccionario de un proceso y mantener un estado

El estado representa la información que se conserva junto al proceso para que haga sus cosas. En esta lección te muestro dos estrategias sobre cómo persistir esa información entre llamadas a receive: mediante funciones intermedias y mediante diccionarios.

¿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 a Process.put(:id, "1234"), puedo colocar una entrada llamada :id en 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/1 y Process.get/2 podemos sacar un valor del diccionario a partir de la clave con la que fue insertado al llamar a Process.put. El segundo parámetro de Process.get es 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 vale nil, así que pedir una clave que no existe devolverá nil o 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.
  • Process.delete/1: permite borrar una entrada del diccionario a partir del nombre con el que previamente se guardó mediante Process.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
Lista de reproducción
  1. 1
    Cómo crear procesos
    11 minutos
  2. 2
    Cómo pasar mensajes entre procesos
    13 minutos
  3. 3
    Diccionario de un proceso y mantener un estado
    15 minutos
  4. 4
    Cómo enlazar procesos para detectar fallos
    13 minutos
  5. 5
    Cómo monitorizar procesos
    10 minutos
  6. 6
    ¿Qué es un GenServer?
    15 minutos
  7. 7
    Cómo enviar mensajes con un GenServer
    19 minutos
  8. 8
    Control de errores y gestión de un GenServer
    19 minutos
  9. 9
    Cómo renombrar procesos
    11 minutos
  10. 10
    Cómo crear un Supervisor Tree
    17 minutos
  11. 11
    Estrategias para trabajar con Supervisor
    18 minutos
  12. 12
    Estrategias para crear un Supervisor
    11 minutos
  13. 13
    Resumen sobre procesos OTP
    12 minutos
  14. 14
    Cómo usar Application
    12 minutos
  15. 15
    Ejemplo de Application con hijos
    14 minutos