Skip to content

Lenguaje y Bloques: Diseño viejo

Alf Sanzo edited this page Oct 18, 2016 · 13 revisions

Introducción

El objetivo de esta página es documentar mínimamente el uso de Blockly en Pilas Bloques, hasta Septiembre de 2016.

Este diseño fue mutando porque inició como forma de tunear el lenguaje JavaScript que venía ya definido en Blockly.

Sin embargo éste diseño debería cambiar bastante, porque deberíamos agregar un nuevo lenguaje PilasBloquesProgramBuilder (o algo así) en lugar de estar sobreescribiendo características del lenguaje JavaScript ya implementado en Blockly.

Para poder decidir un buen diseño, hay que primero documentar el existente.

Partes involucradas

Al utilizar Blockly, es necesario pensar en estas partes:

  • La definición visual de cada bloque. def visual bloque
  • El toolbox (que define categorías y dónde van a estar los bloques creados) toolbox
  • El lenguaje (que especifica, para cada bloque del toolbox, cómo se armará el texto final que representa lo que programamos) lenguaje

Toolbox (bloques y categorías)

El toolbox se define a través de un XML.

¿Dónde se arma el toolbox?

En actividad.js puede verse desde dónde se define el toolbox (nótese que durante el artículo usaré link estático a un commit específico, para que si se actualiza el archivo la desscripción actual no cambie)

Tiene la siguiente forma:

    Blockly.inject(contenedor, {
      // Varias opciones más....
      toolbox: Blockly.Xml.textToDom(this.obtenerLenguaje()),
});

Ese this.obtenerLenguaje() devuelve un string XML que representa la definición del toolbox usando la especificación de Blockly

¿Cómo se arma el XML?

Básicamente En Pilas Bloques hay tres tipos de objetos: Lenguaje, Bloque, Categoría.

Clases en Pilas Bloques (pone /edit/ para editar)

Nos creamos nuestros propios objetos para poderles pedir a cada uno que escupa el XML

Objeto Método Qué hace
Lenguaje build() A partir de todos los bloques previamente cargados, devuelve el xml completo para el toolbox, incluyendo categorías y bloques.
Categoria generarXML(bloques) A partir de muchos bloques, devuelve el xml correspondiente a la categoría, que luego irá en el toolbox (y tendrá todos esos bloques dentro)
Bloque build() Devuelve el xml correspondiente a ese bloque

Nota: Estaría bueno hacer un refactor que haga que todos esos métodos se llamen igual: xml(). Así se entiende que devuelven el xml, muy simple.

Nota: Otro refactor bueno sería cambiar el nombre de la clase "Lenguaje" por "Toolbox", ya que no es el lenguaje de posta. El lenguaje de posta debería ser el conjunto de métodos block_javascript() (ver abajo).

La razón por la que las categorías se obtienen a través de Lenguaje.ordenCategorias() es para que el toolbox se cree con las categorías en ese orden. La razón por la que los bloques conocen a las categorías y no al revés, es para tener la facilidad de definir, en cada bloque, a qué categoría pertenece, y no tener que andar saltando a otra clase para hacerlo (se define en el mismo bloque).

Resumen

  • Para escupir el XML del toolbox es necesario "llenar" al lenguaje con las clases de bloques que se desea, y luego al mandarle el mensaje build() se obtiene el XML para el toolbox.
  • Cuando hacemos Blockly.inject(... Allí el mensaje Actividad.obtenerLenguaje() toma los bloques de la actividad, se los carga al lenguaje, y llama al método Lenguaje.build().
  • Esto es independiente del lenguaje. Podríamos decir que, si quisiéramos generar el lenguaje "Gobstones", siempre vamos a necesitar hacer lo mismo: definir un toolbox, con categorías y un orden, y armar el xml.

Definición visual de un bloque

Pilas Bloques utiliza la API Javascript de Blockly, en lugar del JSON. Esto permite usar la jerarquía de objetos Bloque de Pilas Bloques para hacer la creación más flexible. (En la doc oficial elegir la solapa "Javascript" para ver los ejemplos con esa API)

¿Dónde se define la parte visual?

Todos los Bloques que heredan de la clase Bloque implementan el método block_init(). Ese es el mensaje que se ejecuta cuando se desea construir visualmente un bloque:

Visual bloque

El mensaje block_init() se wrappea en Bloque.registrarVista() como el método init() que llama Blockly para crearlo.

En otras palabras, en block_init() se escribe el código de la API Javascript de Blockly:

var EstructuraDeControl = Bloque.extend({
  block_init(block) {
    this._super(block);
    block.setColour(Blockly.Blocks.loops.COLOUR);
    block.setInputsInline(true);
    block.setPreviousStatement(true);
    block.setNextStatement(true);
  }
});

Lenguaje (Code Generation)

  • Documentación oficial de Blockly acá.
  • Cada bloque registra una función en Blockly.NombreDelLenguaje[idDelBloque] que es la función que escupe el código a generar. (ver abajo, Registro de Bloques).
  • Pilas Bloques toma su clase Bloque (de Bloques.js) y lo que registra en Blockly es el método block_javascript(block) .
  • Entonces, el código generado se encuentra viendo los métodos block_javascript(block) en bloques.js .
  • En estos métodos hay mucha magia negra, y merecen refactor, que hay que hacer leyendo la documentación de Blockly. OJO. Acá es donde están resueltos los problemas de evaluación recursiva de expresiones, y de llamados a métodos, y definición de procedimientos... ¡Leer la doc oficial!

generar codigo

Registro de bloques

Para que todo lo anterior funcione, antes de inyectar el toolbox hay que registrar ambas partes de un bloque de Blockly: la parte viscual (en Blockly.Blocks) y la parte del lenguaje (en Blockly.NombreDelLenguaje).

Es por eso que cada llamada a Lenguaje.agregar_bloque(claseBloque) (al construir el toolbox) lo que hace es registrar ambas cosas:

Bloque registrar

  registrar_en_blockly() {
    this.registrarVista();
    this.registrarGeneracionJS();
  },

  registrarVista(){
    var myThis = this;
    Blockly.Blocks[this.get('id')] = {
      init() {
        myThis.block_init(this);
      }
    };
  },

  registrarGeneracionJS(){
    var myThis = this;
    Blockly.JavaScript[this.get('id')] = function(block) {
      return myThis.block_javascript(block);
    };
  },

Además, es interesante ver que la clase CambioJSDeBlockly está allí para sobreescribir el método registrar_en_blockly(). La idea es que los bloques que hereden de CambioJSDeBlockly no registren su vista, ya que la vista es la que viene definida por defecto en Blockly acá.

var CambioDeJSDeBlocky = Bloque.extend({
  registrar_en_blockly() {
    // La vista ya está registrada originalmente por el lenguaje Javascript.
    // Sólo registro la generación diferente de código
    this.registrarGeneracionJS();
  },
});

Por ejemplo, el bloque cuyo id es procedures_callnoreturn, que es la llamada a un procedimiento, está definido visualmente por defecto en Blockly.Blocks, y el comportamiento está definido en nuestro bloques.js.

En conclusión, si se quiere definir un bloque para el que se mantiene la vista original por defecto de Blockly, hay que heredar de CambioDeJSDeBlocky.

Nota: Para crear nuestro propio lenguaje Blockly.PilasBloquesProgramBuilder, una de las posibilidades sería cambiar este método registrarGeneracionJS() para que cargue las cosas en ese lenguaje en vez de en JavaScript. (Además habría que agregar varias otras cosas al lenguaje). Una primera aproximación podría ser: Blockly.PilasBloquesProgramBuilder = Blockly.JavaScript.clone() y apartir de ahí pisar cosas. Ó hacer que herede 😄

AccionBuilder

El AccionBuilder es un Builder que permite crear de manera uniforme y desde sólo ese punto de entrada y sin pensar en el objeto Bloque, los bloques más sencillos y diversos que aparecen en todas las actividades: las típicas "Primitivas" (con el mensaje build(...)) y los "Sensores" (con el mensaje buildSensor())

Recibe un objeto con 5 atributos:

  • descripcion es el texto que aparecerá en el bloque
  • id es el que usa Blockly para registrar el bloque.
  • icono es el ícono que irá junto al texto de la primitiva para reforzar su significado.
  • comportamiento es el comportamiento de ejerciciosPilas al que se asocia este bloque (parte del Code Generator para este bloque, esta info irá en el método block_javascript())
  • argumentos es el argumento del comportamiento.

Ejemplo:

var AlimentarPez = AccionBuilder.build({
  descripcion: 'Alimentar pez',
  id: 'AlimentarPez',
  icono: 'icono.pez.png',
  comportamiento: 'RecogerPorEtiqueta',
  argumentos: '{etiqueta: "PezAnimado", idTransicion: "alimentarPez"}',
});

Nota: Un refactor posible podría ser que AccionBuilder pase a llamarse PrimitiveBuilder.

También hay que volarle el código repetido.

Resumen

  • La clase Bloque, de la que heredan todos los bloques, tiene cuatro partes importantes que requieren ser definidos en las subclases:
  • El método block_init() , que define cómo se construye visualmente un bloque. Usa la API Javascript de Blockly para construir la parte visual.
  • El método block_javascript(), que define el String que escupirá Blockly al leer el workspace y pasar por ese bloque. Usa una API diferente que sirve para leer el estado actual del bloque ya construido.
  • El atributo id, que es el id del bloque que usará Blockly para referirse internamente a él.
  • El método build(), que escupe el xml para el toolbox y para el workspace, que Blockly usa para saber dónde crear el bloque.
  • El AccionBuilder es un Builder que permite crear de manera uniforme y desde sólo ese punto de entrada, olvidándose de las 4 partes anteriores, los bloques más sencillos y diversos que aparecen en todas las actividades: las típicas "Primitivas" (con el mensaje build(...)) y los "Sensores" (con el mensaje buildSensor())

Nota: Un refactor interesante sería hacer que el Bloque.id sea un método, para no tener que redefinir el método init() en todas las subclases de Bloque. ¡Usar un Template Method! Mucho más prolijo que el super() en todos lados.

Más Info sobre Blockly

  • Es muy buena la guía que hay en la página de Blockly. La sección de Custom Blocks es altamente recomendable, y se lee rápido.