4to paso: diseñar una interfaz (parte VII)

Mientras yo estoy devanándome los sesos pensando en cuál será la mejor manera de explicar la lógica subyacente en el proceso de validación de datos —que es la parte central de esta etapa 2—, lo veo a Pier entusiasmado ensayando con distintas inflexiones de voz el guión que le pasamos en la etapa anterior.
Pero lo que le espera en esta etapa irá más allá de cuestiones como sensibilidad e histrionismo… deberemos capacitarlo para que, a través de un análisis lógico, se encargue de aceptar o no la entrada por teclado del usuario, y en los casos donde la misma sea errónea sea capaz de dar las explicaciones del caso y solicitar cortesmente una nueva entrada… todo un nuevo desafío para nuestro amigo.
A nosotros se nos presentará también un serio desafío : generar la lógica algorítmica que resuelva el problema de la validación, enfrentar nuevos problemas a la hora de su implementación en Scratch (vinculado al tema tipificación de datos, sobre el cual daremos un breve pantallazo), y otro no menor: convencer a Pier de que es capaz de llevar todo esto a cabo por sí sólo…
Armate de paciencia y valor… lo que trataremos en esta página puede llegar a resultarte extenuante.

Etapa 2: un gato lógico

Siguiendo el mismo esquema de trabajo que usamos para la etapa anterior, nos vamos a concentrar en lo estrictamente necesario para proyectar esta segunda etapa: aquí están la descripción y las metas que dimos para la misma…
  • Pier deberá proponerle al usuario que ingrese un dato vinculado con la cantidad de lados que quiera que tenga el polígono a ser dibujado. Ese dato debe ser un número entero mayor o igual a 3¡no hay polígonos de menos de 3 lados!—.
  • Si el usuario ingresa un valor numérico que no cumpla con la condición enunciada anteriormente, Pier deberá informar de esta situación al usuario, y solicitar un nuevo ingreso de dato.
  • Si el usuario ingresa texto en vez de número (por ej. si escribe tres en vez de 3) también será considerado erróneo, no porque el valor lo sea sino porque no cumple con el formato necesario para que el programa funcione. Pier debe encargarse aquí también de informar al usuario de la situación, para acto seguido solicitarle un nuevo dato.
  • El programa debe contemplar el hecho de que puede haber varios ingresos consecutivos de datos erróneos, y Pier debe encargarse en cada uno de estos casos de informar sobre el error en particular.
  • Sólo cuando el dato ingresado sea correcto es que Pier va a realizar su dibujo….
Si tuviésemos que ensayar una síntesis de lo que buscamos conseguir en esta etapa, diríamos: consiste en preguntar al usuario y luego validar su respuesta. La función de validación puede pensarse como un proceso de tamizado, por un pasaje del dato proporcionado a través de un filtro complejo que deberemos construir. Este es el tema (nunca antes abordado) que deberemos tratar a continuación.

El arte del filtro

Para empezar, confieso que la idea del subtítulo proviene de un artículo/entrevista a Umberto Eco que leí hace algún tiempo, donde se expone —entre otras cuestiones— la siguiente premisa: "En el futuro, la educación tendrá como objetivo aprender el arte del filtro"  [absolutamente nada que ver en forma directa con nuestro tema, pero si sois un educador dádle un vistazo…].
Volviendo a nuestro problema, la situación es esta: dentro del universo de posibles respuestas que puede dar el usuario existe SOLO un subconjunto de ellas a las que podremos clasificar como una "respuesta válida". Creo haberlo mencionado en algún momento… a la hora de pensar en una interfaz usuario/programa es un error presuponer que el usuario no se equivocará o que tendrá un conocimiento cabal sobre cómo interactuar con nuestro trabajo de manera correcta: sólo nosotros los programadores somos los que sabemos claramente que tipo de interacción es la adecuada y cual no. Y nuestro programa debe ser capaz de lidiar con esta situación y debe poder corregir al usuario de ser necesario para que él consiga ingresar una respuesta válida.

Yendo a la cuestión central: en nuestro proyecto consideraremos como respuesta válida a un número entero mayor o igual que tres.

Estrategia a seguir

La estrategia que deberemos emplear para solventar situaciones de validación de datos en el ámbito de la programación (y en Scratch en particular) puede encuadrarse dentro de la denominada "lógica binaria". Consiste en ir, en forma progresiva, subdividiendo el universo inicial mediante el recurso de formular preguntas con sólo dos respuestas posibles: verdadero o falso.
Como resultado de este proceso de subdivisiones deberíamos poder filtrar las respuestas válidas de las no válidas o erróneas —respondiendo por supuesto a la definición de respuesta válida que enunciamos previamente, donde en principio se deben cumplir 3 condiciones al mismo tiempo—.
Los niveles de jerarquía entre estas condiciones no son similares (si hablamos de entero o de mayor o igual que 3 deberemos estar ya hablando de un número). Aquí se presentará una pista a la que deberemos prestar atención al momento de elucubrar nuestra estrategia de filtrado (en lo que hace a su orden).
Por suerte (?) para nosotros, uno de los pilares en que se basa la generación de algoritmos es nuestra ya conocida estructura condicional (o de decisión), por lo que nuestro problema de validación podrá resolverse construyendo un algoritmo que haga uso de estas estructuras.
núcleo validación
Ya estás viendo un boceto del núcleo funcional de nuestro futuro filtro:
  • el primer bloque de decisión nos permite separar aquellas respuestas numéricas de aquellas que no lo son (como ser el caso de una respuesta expresada en palabras —cuatro en lugar de 4—, o cualquier tipeado erróneo que incluya letras o símbolos de variada índole).
  • el segundo bloque de decisión se encargará de dar por permitidas aquellas respuestas del conjunto anterior que cumplan con la condición de ser mayor o igual a tres.
  • Por último el restante bloque de decisión sólo dará como válidas aquellas respuestas que le lleguen y sean números enteros.
Si la entrada de dato que proporciona el usuario alcanza a traspasar estas 3 etapas de validación simple, estaremos en condiciones de decir que la entrada será válida ya que cumple con los 3 requisitos que enunciamos más arriba: número, entero, mayor o igual que 3.

Lamentablemente para nosotros este algoritmo que tan bien funciona en el papel tropezará —a la hora de ser programado— con ciertos condicionamientos inherentes a la instrumentación de cualquier lenguaje de programación… en nuestro caso la complicación vendrá desde el lado de los tipos de datos, y afectará específicamente a nuestra primera división del universo entre datos numéricos y datos no numéricos.

Tipos de datos en Scratch

Hablar in extenso sobre el tema tipo de datos supera en mucho los alcances de este humilde tutorial (y los conocimientos de este humilde escriba). Por suerte para todos encontré un nuevo documento intitulado The Scratch Programming Language and Environment, con el que me desasné un poco sobre cómo es el acercamiento de Scratch 1.4 hacia esta cuestión. Con esto y algunas conclusiones empíricas de mi cosecha personal espero que pueda llegar a explicar algo (y estoy abierto a cualquier corrección por parte de cualquiera de los lectores que esté más ducho que yo en estas cuestiones).
En un sentido amplio, un tipo de datos define un conjunto de valores y las operaciones sobre estos valores. [Wikipedia]

Sistema de tipos de datos

Un sistema de tipos define la manera en la cual un lenguaje de programación clasifica los valores y expresiones en tipos, como pueden ser manipulados dichos tipos y como interactúan.
El objetivo de un sistema de tipos es verificar y normalmente poner en vigor un cierto nivel de exactitud en programas escritos en el lenguaje en cuestión, detectando ciertas operaciones inválidas.
Los lenguajes débilmente tipados permiten que un valor de un tipo pueda ser tratado como de otro tipo, por ejemplo una cadena puede ser operada como un número. Esto puede ser útil a veces, pero también puede permitir ciertos tipos de fallas que no pueden ser detectadas durante la compilación o a veces ni siquiera durante la ejecución. Los lenguajes con tipos débiles permiten un gran número de conversiones de tipo implícitas, las que son útiles con frecuencia, pero también pueden ocultar errores de programación.
[Wikipedia]
Sintetizando: lo que se está exponiendo versa sobre el establecimiento de una clasificación basada en ciertos atributos que se aplica a la información almacenada en variables, a las expresiones y a los datos devueltos por alguna función, y en base a la cual luego pueden imponerse restricciones sobre el tipo de operaciones que puedan ser realizadas sobre ellas.

En el lenguaje Scratch se definen tres tipos de datos: booleanos (o lógicos), numéricos y cadenas de texto (string).

Las restricciones sobre las operaciones quedarán de manifiesto al manipular la interfaz visual cuando construímos nuestros programas: sólo ciertos bloques pueden ser insertados en los slots o casillas que se proporcionan en aquellos bloques preparados para contener datos.
Los slots booleanos son los más estrictos: sólo esperan un bloque que devuelva un valor VERDADERO o FALSO (true o false, 1 ó 0, como quieras verlo).
Los slots numéricos y los de cadena de texto son menos estrictos: ellos aceptan bloques vinculados a cualquiera de estos dos tipos de datos, efectuando la conversión al tipo que necesitan en forma automática si es necesario. Por ejemplo, si la cadena "123" es pasada a una operación aritmética será convertida a un dato numérico, mientras que si un número es pasado a un bloque como decir ( ), será convertido a una cadena de texto.
Notemos entonces que en este caso el tipo de dato dependerá del contexto en que es utilizado.

Sospecho que estarás pensando: "¿…de veras tengo que aprenderme todo esto para usar Scratch?" 

En palabras de los mismos creadores del entorno y lenguaje: "Dadas las metas pensadas para Scratch la conversión automática es preferible a requerir que el usuario especifique una conversión explícita entre tipos de datos" Fuente: The Scratch Programming Language and Environment
Las metas en Scratch apuntan a la simplicidad, y en la inmensa mayoría de los casos no te vas a topar con problemas vinculados al sistema de tipificación de datos porque el sistema lo resuelve de manera transparente al programador. Pero en algunos pocos casos —como el que nos ocupará en un momento— la débil tipificación del lenguaje y el automatismo en la conversión, lejos de ayudarnos, nos vá a enfrentar con un serio problema al intentar contestar la simple pregunta ¿respuesta es un número?.

7 , ocho , nueve(9) , 10lados , #&%@f11_*

No. No estoy loco ni me terminé durmiendo arriba del teclado —aunque ganas no me faltan—. En el subtítulo están mostradas 5 posibles respuestas de un usuario, pertenecientes al infinito universo de respuestas posibles (más allá de que tengan sentido o no).

Nuestro script de filtrado debe ser capaz de validar (siempre pensando en este conjunto de cinco entradas posibles) sólo a la primera de ellas ( 7 ), y de rechazar a las otras 4 por erróneas. En lo que sigue me voy a explayar sobre la solución que yo encontré para resolver esta situación, en la cual, dada la poca documentación con que contaba, tuve que apelar y mucho al método de prueba y error.

Pensemos un poco para ver si encontramos una vía de solución a nuestro intríngulis. Por mis experimentaciones previas, creo no equivocarme al decir que el tipo que le asigna inicialmente el sistema al dato contenido en respuesta es el de cadena de texto (string), pudiendo esta cadena contener todos los símbolos que pueden ser ingresados por teclado (incluyendo los símbolos usados para los números).
Por otro lado, dada la laxitud del lenguaje en lo que hace a la tipificación, no podremos contar con una instrucción que de manera directa nos permita determinar si el valor ingresado por el usuario y almacenado en respuesta es un número o no. Será momento entonces de apelar a nuestro ingenio y de echar mano a nuestros nuevos conocimientos adquiridos respecto al tema "tipos de datos".

A experimentar se ha dicho…


Como mencionamos previamente, se producirá una conversión automática de tipo en los casos en que una cadena de texto sea pasada a una operación aritmética… ¿que resultado obtendríamos si intentásemos una operación como la siguiente?

por 1
  • Si respuesta = 7 el resultado es 7.
  • Si respuesta = ocho el resultado es 0 (cero).
  • Si respuesta = nueve(9) el resultado es 0 (cero)
    [la cadena empieza con una letra].
  • Si respuesta = 10lados el resultado es 10
    —la cadena empieza con un número—.
  • Si respuesta = #&%@f11_* el resultado es 0 (cero)
    [la cadena empieza con un símbolo no numérico].

NOTA IMPORTANTE

Esta parte del análisis se corresponde con lo experimentable SÓLO en la versión 1.4 de Scratch, ya que ha partir de la versión 2.0 no se presentará el inconveniente excepcional señalado a continuación.
Al final de esta página agregaré un anexo con el script de validación corespondiente a Scratch 2.0, que será ligeramente más simple que el necesario para Scratch 1.4.
La evidencia empírica nos indica que el pasar la cadena de texto almacenada en respuesta a una operación matemática arroja un resultado cero en 3 de los 4 casos en donde dicha cadena no debería ser validada; la excepción ocurre cuando la cadena empieza con un número y sigue con otros símbolos (en donde se manifiesta un "truncamiento" de todos los símbolos no numéricos a partir del primero de ellos).
Viendo el vaso medio lleno, de no tener nada pasamos a tener una forma "imperfecta pero perfectible" de resolver esa pregunta que es fuente de tantos desvelos: ¿respuesta es un número?

No perdamos de vista lo siguiente: multiplicar por 1 parece ser una manera (imperfecta) de detectar una entrada no válida.

A perfeccionar lo imperfecto

Según nuestras pautas, una cadena de texto que empieza con un número y sigue con una serie de símbolos no numéricos será una entrada de datos errónea.
Vimos que una estrategia de validación donde se proponga el rechazo de aquellos casos donde la multiplicación del valor de respuesta por uno dé por resultado cero falla en estos casos (absurdos pero posibles)… ¡pero no bajemos los brazos!. Veamos si podemos perfeccionar esta solución apelando de nuevo al ingenio.
¿En qué se diferencia una cadena de texto como —y volviendo sobre nuestro ejemplo— 10lados de una que sea 10 (que es el resultado obtenido al multiplicar 10lados por uno)? Es una pregunta con múltiples respuestas, pero yo quiero en la mía focalizarme en un aspecto: el de la cantidad de símbolos presentes en cada una de ellas. Son 7 en la primera cadena y 2 en la segunda. En la primera el usuario agregó información que no es considerada válida (por error, por mala interpretación, vaya uno a saber…)

¿Qué concluiríamos si al comparar los tamaños entre la cadena de texto ingresada por el usuario y el de la cadena resultante de la multiplicación por uno comprobásemos que son distintos? Estaríamos ante un caso en donde sin duda fueron tipeados uno o más símbolos que ocasionarían la invalidación del dato almacenado en respuesta.

comparar tamaños
Afortunadamente contamos con un bloque que nos permite determinar la longitud (cantidad de caracteres) de una cadena de texto determinada; usando ésto podemos construir un bloque de respuesta booleana en donde si el resultado es TRUE quedará de manifiesto la discrepancia entre los tamaños de ambas cadenas… un síntoma de error.
test numérico
Ensamblando esto con lo previo lograremos filtrar el caso que nos quedó pendiente en el "imperfecto" intento anterior. Aquí dejo para tu consideración el programa de prueba que utilicé para chequear las elucubraciones aquí expuestas.
Dimos un gran paso adelante en perfeccionar lo imperfecto… pero ahora vamos a corregir un defecto de análisis que en su momento se me pasó por alto porque, amigos, también debería haber considerado el caso siguiente:
  • Si respuesta = 0 (cero) el resultado es 0 (cero).

El 0 es un número

Vaya novedad que les traigo. Sin embargo si se diera el caso de que un alienado usuario pretendiese que Pier dibuje un polígono regular de 0 lados, la respuesta que obtendría de nuestro script sería:
"Debes ingresar números, no palabras",
contribuyendo sin duda a su estado de confusión —por lo menos en lo que a las matemáticas se refiere—.
Multiplicar una cadena de texto por 1 arroja como resultado cero (salvo cuando la cadena empieza con un número y sigue con otros símbolos), y esto es lo que astutamente usamos como primer herramienta de filtrado. Pero multiplicar 0 por 1 también dará —obviamente— como resultado cero, y el filtro aquí se equivoca en encuadrarlo como un ingreso de cadena de texto.
¿Será que debemos descartar todo lo hecho? Nada de eso. Sólo deberemos hacer una división inicial del universo de manera de darle un tratamiento especial al posible caso respuesta = 0 . De darse esta situación particular, Pier deberá informar que no existen polígonos de 0 lados… de no darse seguirá en pie todo nuestro razonamiento previo, aunque esta vez solo aplicable a toda entrada distinta de 0.

Con este último agregado vamos a considerarnos por satisfechos con el perfeccionamiento de esta primera etapa de filtrado, y declarar como resuelto el dilema "¿respuesta es un número?"

Creo que es un buen momento para proponer un nuevo diagrama de flujo —una versión mucho más realista que aquel inocente boceto que presentamos al inicio de esta página— en donde queden plasmadas todas las propuestas hechas hasta aquí, y donde también podamos ver proyectados los pasos siguientes:
algoritmo de validación

Tamizando números

Ya conseguimos dividir el universo inicial de entradas por teclado posibles entre datos numéricos y todos aquellos que no lo son. Nos queda por delante tamizar el conjunto numérico de manera de quedarnos sólo con los enteros mayores o igual a 3 (los últimos bloques del diagrama de flujo anterior).
Para conseguir esto deberemos generar 2 nuevas subdivisiones, con la particularidad de que el orden de filtrado aquí no será relevante.
¿Mayor o igual que 3? versus ¿menor que 3? 
Si lo que queremos es dividir el subconjunto de datos numéricos entre los que son mayores o igual que 3 y aquellos que no lo son, cualquiera de las dos preguntas son herramientas válidas para conseguirlo (notad que sus resultados son mutuamente excluyentes). Por cuestiones de comodidad nosotros usaremos en nuestro filtro definitivo la pregunta ¿menor que 3? —como ya veremos—.
Son los valores que no cumplan con esta condición los que pasarán al filtrado siguiente.
A por los enteros 
Otra vez la débil tipificación del lenguaje nos enfrentará a un nuevo problema —más fácil de resolver que aquel que ya demandó horas de escritura, te anticipo— : el nuevo desafío a vencer es… ¿respuesta es un nº entero?
No existe un operador que nos entregue "bandeja en mano" la solución a esta pregunta, por lo que deberemos sacar otro as de la manga: tenemos un bloque que es un serio candidato para llevar a cabo nuestra labor de prestidigitación … el operador mod.
test mod
Operador mod
Este bloque es un operador matemático que nos entrega como resultado el resto de la división entre dos números. Por ejemplo, si hacemos (8) mod (3) su resultado será 2, ya que el resto de la división 8/3 es 2 (porque 8 = 2 * 3 + 2).

mod es un operador pensado inicialmente para aceptar como argumentos a números enteros, pero descubrí que también acepta números decimales, y que aún así sigue entregando el resto de la división… ¿será sólo anecdótico este comentario?

Pensemos en un usuario entrando un valor no entero por teclado, por ej. 9,75 ó 9.75 (es indistinto porque Scratch parece convertir una entrada decimal que usa comas al formato de separación usando puntos)   ¿Que resultaría de la operación (9,75) mod (1)?
Descompongamos la división: 9.75 = 9 * 1 + 0.75… el resto es de 0.75 (que es no casualmente la parte decimal del nº ingresado).

Extrapolando esta conclusión a cualquier dato ingresado: si usamos al número 1 como segundo argumento de mod podremos separar la parte decimal presente en cualquier númeropensalo, pero es así

Y la pregunta del millón es: ¿que distinguirá a cualquier número entero dentro del universo de valores numéricos con componente decimal (números reales, bah)? RTA: que su parte decimal es nula (0), o si querés, que no existe.

Conclusión

Pasar cualquier número entero por un bloque mod cuyo 2do argumento sea 1 arrojará como resultado 0 (cero).
Ya tenemos la carta, ese as que nos permitirá construir un bloque de respuesta booleana (lo estás viendo): cualquier valor almacenado en respuesta que no sea entero, al ser operado por esta construcción dará una respuesta TRUE (SI); por ende si es entero dará resultado FALSE (NO).
validar enteros

Lo que nos dejó toda esta alquimia

Alquimia de lógica algorítmica, pero alquimia al fin. Toda entrada por teclado que no supere estos filtros terminará generando una repregunta y la petición de una nueva entrada. Toda entrada que los supere será una respuesta válida.

Destaquemos algo: el producto final de nuestro destilado será nada más ni nada menos que el valor asociado a la cantidad de lados del polígono regular que necesita Pier para poder realizar su dibujo, dato que deberá ser resguardado convenientemente en la variable n para su posterior utilización en la etapa tres (la dedicada propiamente al dibujo del polígono).
La instrucción que se usará será fijar (n) a respuesta.

etapa 2
¿Qué es lo que resta hacer en esta etapa 2? Muy poco en realidad, pero muy importante.
El último paso corresponde a la instrucción que cerrará funcionalmente esta etapa, y es la que debe generar la activación de la etapa 3 de nuestro proyecto. Como ya dijimos, la vía para propiciar esto es el envío de un mensaje —el que oportunamente debemos crear, y que recibirá de nombre dibujar—.
El megascript que tanto trabajo nos llevó desarrollar (y que ya estás viendo acompañando estas palabras) puede resultar algo intimidante —lo reconozco—, pero es casi el reflejo exacto de lo planteado en el último diagrama de flujo que presentamos.
Unas últimas palabras a modo de síntesis: este script "per se" se encarga de todo el trabajo de pregunta y validación que fue planificado para esta segunda etapa del proyecto, y es un claro ejemplo de subrutina que puede reutilizarse toda vez que sea necesaria (dependiendo de las capacidades y/o veleidades del usuario de turno).

Lo que queda…

… es retomar el proyecto tal y cual lo dejamos al fin del paso anterior (etapa 1): en él ya habíamos implementado el script de inicio y teníamos un segundo script por ahora desactivado. Deberemos —con infinita paciencia— construir el script de pregunta y validación aquí mostrado.
Se sugiere poner especial atención sobre el orden de "anidado" de las estructuras de decisión, y también al momento de construir los bloques booleanos que los encabezan.
Una vez construido este script es conveniente testearlo con entradas de texto válidas y no válidas, para ver como reacciona al filtrado de las mismas (podés usar 7 , ocho , nueve(9) , 10lados , #&%@f11_*, 0, 2, 6.25 y 9,75 como hice yo). De existir alguna anormalidad es posible que haya un error en la construcción del script —en mi testeo se invalidaron todas las entradas de la serie previa menos la entrada 7, lo cual se corresponde con el comportamiento esperado—.
Si todo te funcionó OK, podés guardar los cambios… las dos primeras etapas del proyecto quedarán así concluídas.

A convencer a Pier

Supongo que el único extenuado no debo ser yo. Quizás a ustedes les quede el trabajo de rumiar sobre lo aquí tratado —espero que no, pero no sería ninguna deshonra—. A mí me queda un trabajito nada envidiable… cuando le cuente a Pier todo lo que deberá aprenderse para llevar adelante esta etapa de validación lo más probable es que ponga pies en polvorosa (lo bello de aclarar que estoy recurriendo al uso de una antigüedad lingüística es que me permite usar las diéresis).
Por suerte el análisis de la etapa siguiente será —gracias a todo lo que ya aprendimos— a piece of cake… I'm going to the bed.

Anexo Scratch 2.0

Retomemos el análisis prometido anteriormente, en caso de que usemos Scratch 2.0 para realizar la ya conocida operación de conversión automática de tipo:

por 1
  • Si respuesta = 7 el resultado es 7.
  • Si respuesta = ocho el resultado es 0 (cero).
  • Si respuesta = nueve(9) el resultado es 0 (cero)
    [la cadena empieza con una letra].
  • Si respuesta = 10lados el resultado es 0 (cero)
    —la cadena empieza con un número—.
  • Si respuesta = #&%@f11_* el resultado es 0 (cero)
    [la cadena empieza con un símbolo no numérico].
Como vemos, en Scratch 2.0 la operación nos entrega lo que exactamente estamos necesitando: convierte todas las entradas no "estrictamente" numérica a cero, con lo que nos ahorraremos parte de lo implementado en el script de validación pensado para Scratch 1.4.
No voy a reelaborar el segundo diagrama de flujo, pero sí te dejo el script de validación correspondiente a Scratch 2.0 a tu consideración… ¡Que lo disfrutes!
script de validación para Scratch 2.0
Última actualización: Septiembre 21, 2016

No hay comentarios.:

Publicar un comentario

© Scratch CodeLab | D153ñ0 b454d0 3n l4 pl4n71ll4 SimpleRWD d3 Oloblogger