Background Image
TECNOLOGÍA

Inclinarse hacia los tipos

January 10, 2024 | 5 Minuto(s) de lectura

En este artículo, utilizo pseudocódigo inspirado en F# para mis ejemplos. Sin embargo, muchas de estas estrategias pueden funcionar con un poco de maña en otros lenguajes.

Los tipos nos ayudan a detectar errores, diseñar nuestras interfaces y crear código legible. Sin embargo, podemos obtener aún más de ellos si consideramos reemplazar nuestras banderas con tipos. Veamos un ejemplo de seguridad. Cada vez que leemos una cadena de una fuente no confiable, puede tener código inyectado en ella. Por lo tanto, debemos desinfectar la cadena antes de usarla. Digamos que codificamos esto de la manera más directa:

let readQuery (source:Datasource) : string = ...
let someLogic (x:string) : Results = ...
someLogic (readQuery …)

Esto nos deja sin protección contra el uso de cadenas no desinfectadas. Por ejemplo, si readQuery devuelve una cadena con código malicioso inyectado en ella, someLogic lo usaría, con efectos desastrosos. Queremos asegurarnos de que someLogic no pueda usar una cadena no desinfectada. Esta es una tarea para tipos, pero ¿QUÉ tipos? Por ejemplo, ¿qué pasa si envolvemos nuestra cadena en un tipo y tenemos una bandera de sanitización, junto con el código apropiado, así?

type ExternalString = {
      value: string;
      isSanitized: bool;
}

let sanitize (x:string) : ExternalString = {
      value = …;
      isSanitized = true;
}

let readQuery (source:Datasource) : string = …
let someLogic (x:ExternalString) : Results = …
let results = someLogic (sanitize (readQuery …))

Este enfoque requiere que añadamos lógica a algunaLógica para comprobar el indicador de desinfección antes de utilizar la cadena. Eso es trabajo extra y podemos olvidarnos de hacerlo. Sin embargo, ¿qué pasaría si en lugar de utilizar una bandera, hiciéramos que las cadenas desinfectadas fueran de un tipo diferente?

type SanitizedString = Sanitized of string
let readQuery (source:Datasource) : string = …
let sanitize (x:string) : SanitizedString = Sanitized (...)
let someLogic (x:SanitizedString) : Results = …
let results = someLogic (sanitize (readQuery …))

Ahora someLogic sólo acepta el tipo SanitizedString. Y como la única forma de obtenerlo es llamando a sanitizees necesario sanear la cadena antes de utilizarla.

En lugar de establecer una bandera para indicar que la cadena ha sido desinfectada, nos aseguramos de que las cadenas desinfectadas son de un tipo completamente diferente. La idea clave es que una bandera a menudo marca los datos como un tipo diferente. Entonces, ¿por qué no crear un tipo completamente nuevo?

De este modo se evitan los casos en los que nos olvidamos de aplicar la desinfección de cadenas. Además, al no requerir que verifiquemos una bandera de sanitización en nuestra lógica, elimina los errores que surgen de olvidar hacer eso también. No es a prueba de balas, pero hay menos formas de meterse en problemas.

No necesitamos tipos de datos algebraicos para esto. Podríamos declarar un nuevo registro, clase, etc... como una envoltura para una cadena desinfectada en cualquier lenguaje con una comprobación de tipos decente. Pero lo que sigue es (que yo sepa) exclusivo de los lenguajes que soportan tipos algebraicos.

Los lenguajes que soportan tipos de datos algebraicos (como F#) también comprobarán si nos hemos saltado algunos casos. Por ejemplo, digamos que queremos alterar nuestra función de sanitización para que compruebe nuestra cadena y si tiene código malicioso, la etiquete como tal, en caso contrario la marque como cadena segura. Ahora estamos tratando con 2 tipos. Además, si una cadena es maliciosa, no nos importa lo que contiene, sólo queremos indicar que se ha recibido una cadena maliciosa. De hecho, no queremos guardarla ya que queremos evitar la posibilidad de que alguien pueda usarla por error. Esto nos da ahora esto:

type SanitizedString =
      | Safe of string
      | Malicious

Además de nuestros otros cambios, someLogic necesita manejar esto:

let someLogic (x:SanitizedString) : Results =
      match x with
      | Safe x -> ...

Pero cuando intento construir en F#, obtengo esto:

Coincidencias de patrón incompletas en esta expresión. Por ejemplo, el valor "Malicioso" puede indicar un caso no cubierto por el patrón o patrones.

Sí, me olvidé de añadir la lógica para manejar el caso de Malicioso¡! Y como lo declaré como un tipo de dato algebraico, F# lo vio y me informó de ello. No conseguiría esa comprobación con una declaración corriente sobre una bandera. Así que corrijo el código:

let someLogic (x:SanitizedString) : Results =
      match x with
      | Sanitized x -> ...
      | Malicious -> ...

Ahora todo está bien con el mundo.

Así que el uso de tipos también ayudó a comprobar la integridad de nuestra lógica. Por cierto, hay al menos un lenguaje (Idris) que va un paso más allá y genera el esqueleto que cubre todos tus casos si se lo pides. De nuevo, utiliza tipos de datos algebraicos para conocer todas las posibilidades.

Veamos ahora un ejemplo no relacionado con la seguridad. Imagina que estamos escribiendo código para procesar solicitantes de empleo. Estos solicitantes tienen una variedad de estados, con datos adicionales que varían con cada estado. Por ejemplo, un solicitante de empleo que...

1. Envió un currículum vitae debe incluir el currículum vitae.

2. Tiene una entrevista programada debe tener una fecha y hora para la entrevista.

3. Es Rechazado tiene una razón para el rechazo.

4. Ha recibido una oferta de trabajo tiene que ir acompañada de una oferta salarial.

Estos son los únicos estados legales; así, por ejemplo, un candidato rechazado no puede tener una oferta salarial. Esto significa que no podemos hacer esto:

type Resume = ...
type RejectionReason = ...

type StatusType =
      | SubmittedResume
      | ScheduledInterview
      | Rejected
      | MadeOffer

type ApplicantStatus = {
      firstName: string;
      lastName: string;
      ...
      statusType: StatusType;
      resume: Resume;
      offer: decimal;
      rejectionReason: RejectionReason;
      interviewDate: DateTime;
}

Esto permitiría estados ilegales y necesitaríamos añadir lógica para manejar esos estados. En su lugar, podemos evitar incluso representar tales estados con esto:

type Resume = ...
type RejectionReason = ...

type Applicant = {
      firstName: string;
      lastName: string;
      ...
}

type ApplicantStatus =
      | SubmittedResume of Applicant * Resume
      | ScheduledInterview of Applicant * DateTime
      | Rejected of Applicant * RejectionReason
      | MadeOffer of Applicant * decimal

Cada estado tiene una tupla que lo acompaña, y sólo se permite un tipo de tupla para cualquier estado. Como puedes ver, este tipo impone los estados listados arriba sin una sola línea de lógica; todo se hace a través del sistema de tipos. Tuvimos que pensar un poco más sobre nuestros tipos, pero recuperamos nuestro esfuerzo con menos lógica que tuvimos que escribir y errores reportados en tiempo de compilación y para todas las rutas de código.

En conclusión, tener un sistema de tipos fuerte no es suficiente. Debemos elegir usar el sistema de tipos incluso cuando no tengamos que hacerlo y debemos pensar cuidadosamente sobre nuestros tipos. Un consejo para hacer esto es reemplazar las banderas por tipos. Inclinarse más fuertemente hacia los tipos convierte nuestro sistema de tipos en un mejor aliado, en lugar de la barrera que demasiados desarrolladores creen que es.

Tecnología

Reflexiones más recientes

Explore las entradas de nuestro blog e inspírese con los líderes de opinión de todas nuestras empresas.
Asset - Multi-Cloud Strategies for Developers image 2
NUBE

Aprovechamiento de SAP Analytics Cloud mediante widgets personalizados

SAP Muestra de widget personalizado