Scala: One language to rule them all (II)

You wouldn’t put a layman under the controls of a brand new Airbus 380. Such a powerful tool requires its users to be trained; something similar happens with Scala when it is used to cast new rings of power, I mean, DSLs. Some theory and lots of learning by doing.  We’ve already seen a bit of theory and now is time to learn by building a DSL from scratch.

Our DSL’s target

Our brand new DSL is intended to serve as an didactic exercise. However, despite of its purpose, it needs to have a target. That is (or):

  • A process or system to govern.
  • Another language to serve as proxy of.

In our example we’ll tackle the second case.

Introducing AWK

Are you kidding? does it need introduction?
Let your unix terminal introduce it for me:

man awk | head -n 6

GAWK(1) Utility Commands GAWK(1)

NAME
gawk – pattern scanning and processing language

OK, Wikipedia seems a little more verbose:

AWK is an interpreted programming language designed for text processing and typically used as a data extraction and reporting tool. It is a standard feature of most Unix-like operating systems.

The AWK language is a data-driven scripting language consisting of a set of actions to be taken against streams of textual data – either run directly on files or used as part of a pipeline

Consider these two heroes:

Gnu-and-penguin-color

Gnu has a belt you can’t see in this drawing and there he hides a powerful weapon: AWK.
It is so powerful because it allows many scripts, running  upon (and used to build) GNU/Linux/BSD distributions, to transform, filter, aggregate,… data from other commands outputs. Let’s see an example:

awk_sample

Here, the output generated by lsmod is piped to AWK whereby each line is processed by extracting its second column value which will be accumulated in a total byte counter. This way, when the output has been exhausted, total will be printed as the total amount of Kilo Bytes of memory used by Linux Kernel modules.

A hard nut to crack?

The applications of AWK are innumerable as well as the amount of time you can save once you have a grasp of it. However, for many people, it is more like…

6bkFb7B

… than a helping tool. Its 1475 lines of man pages aren’t precisely light reading.

It seem therefore that guiding an user through the composition of AWK programs could be of great help. Does this phrase ring a bell to you?

DSLs are small and concise which implies that they guide their users in the process of describing actions, entities or relations within their domain of application.

Yes! A Scala internal DSL could be used to build such a tool!

Hands on the DSL Scalawk construction

The most important thing in the programming language is the name…
Donald Erving Knuth

First things first and, if we take the argument from authority as a valid argument, we should start by giving a name to our DSL.

To be brief: We’ll compose AWK programs by the use of a Scala internal DSL,

Scala + AWK = Scalawk

So far so good!

You can already clone Scalawk source code GitHub

git clone https://github.com/pfcoperez/scalawk.git

Divide & Conquer

In the last episode we agreed that the safest way to design a DSL is by the use of the state machine model. These machines are easily decomposed into:

  • States (one of them is the machine initial state).
  • Transitions:
    • Inputs triggering the transition.
    • Transition effects:
      • The machine changes its current state.
      • Some output could be generated besides the state transition.
  • Machine alphabet: Machine input entities.

By drawing the state machine diagram, which is not different from designing an interactive guide, we’ll complete the whole creative process in the creation of the DSL. The remaining work isn’t more than beautiful Scala boilerplate.

machine

All possible users interaction with Scalawk are represented in the previous graph, e.g:

lines splitBy ";" arePresentedAs ('c1, 'c2)

machine_walkthrough

This machine modelling and decomposition leads to the following Scala packages structure:

packages

The nuts & bolts or: How I Learnt to Stop Worrying and Love the Building Blocks that Scala Provides

States, transitions and auxiliary elements are entities contained by the packages listed above. In fact, they are nothing but Scala objects, classes, methods and traits.

Initial State

initial_state

As we already know,  states are objects. Either they are instances of classes or singleton objects. On the other hand, we’ve also seen that the right way to implement state machines is to make these states immutable, being transitions responsible for new states generation.

The initial state ins’t generated by any transition, it exists as it is from the beginning of the program. That is a good indicator of its singleton nature which is definitely confirmed by the fact that no other instance of this initial state can exist:

object lines extends ToCommandWithSeparator

From the DSL user standpoint, this initial state should be just a word indicating the start of a phrase in the language. That’s another reason supporting the singleton object approach.

The initial state need to transit to the next one, that’s is why lines is extending ToCommandWithSeparator . Don’t rush, but keep in mind that ToCommandWithSeparator is transition set trait.

Transitory and final states

Yeah, states are objects… is that all? No!  There are different kinds of states, besides, many states are quite alike and they could be built from templates. Lets review some tricks and tips.

The top-level classification of non-initial states should be this one: Transitory and final. Conceptually, the former can’t be used to generate a result whereas the latter can. In the concrete case of Scalawk that implies that transitory states can’t generate valid AWK programs but final states can.

with_initialprogram_st
Non-final state
solidcomand_st
Final state

In Scalawk, any entity able to generate valid AWK code should mix AwkElement

trait AwkElement {
  def toAwk: String
}

By doing so, we are adding toAwk method to that entity, the entry point to ask for AWK code from client code.

Despite of their differences, almost all states share a common set of attributes from which AWK commands can be composed:

  • Command line options, e.g: Token separator
  • Initial program: Instructions to be run by AWK before starting line processing. e.g: Initialize a counter value.
  • Line program: Instructions to be executed for each line of the AWK input. e.g: Printing a line; adding a value to an accumulator initialized at the Initial Program.
  • End program: Instructions to be executed after all lines have been processed, that is, after Line Program has been run using each single input line as its own input. e.g: Printing counters values.

At each state, these fields might be empty or not and when a final state is asked to produce an AWK program, they will be used to generate the result string value.

abstract class AwkCommand protected(
  private[scalawk] val commandOptions: Seq[String] = Seq.empty,
  private[scalawk] val linePresentation: Seq[AwkExpression] = Seq.empty,
  private[scalawk] val lineProgram: Seq[SideEffectStatement] = Seq.empty,
  private[scalawk] val initialProgram: Seq[SideEffectStatement] = Seq.empty,
  private[scalawk] val endProgram: Seq[SideEffectStatement] = Seq.empty
) {
  def this(prev: AwkCommand) = this(
    prev.commandOptions,
    prev.linePresentation,
    prev.lineProgram,
    prev.initialProgram,
    prev.endProgram
  )
}

So far, we know that non-initial states:

  • For sure, contain the building blocks of a result and propagate the previous state contents for these fields:  Then they should extend AwkCommand abstract class.
  • Most probably, add or change some piece of information from the previous state AwkCommand attributes: Then they should override AwkCommand attributes.
  • Optionally, can transit to another state: If it is the case, they should have a method with the type of the target state as return value or mix  a transition family trait.

You might be thinking: Why is AwkCommand an abstract class and not a trait?
Well, AwkCommand‘s goal is to provide a re-usable code for continuity. That is, it provides the constructor to build a state from another state (prev parameter). This way, states code is reduced to just their transitions and AwkCommand attribute overrides but only for those attributes whose information is changing in the new state.

Obviously, the only way to provide a constructor in a class hierarchy is by providing a class, if this class can’t be instantiated: Make it abstract.

abstract_with_constructor

class CommandWithLineProgram(
                              statements: Seq[SideEffectStatement]
                            )(prev: AwkCommand) extends AwkCommand(prev)
  with ToSolidCommand {

  override private[scalawk]val lineProgram: Seq[SideEffectStatement] = statements

}

CommandWithLineProgram is a non-final state hence it doesn’t mix AwkElement trait.

//This is the first state which can be used to obtain an AWK command string `toAwk`
class SolidCommand(val lineResult: Seq[AwkExpression], prevSt: AwkCommand) extends AwkCommand(prevSt)
  with AwkElement
  with ToCommandWithLastAction {
 ...
 ...
 ...
}

On the contrary, SolidCommand does, therefore needs to provide an implementation to toAwk method:

 // AWK Program sections

// BEGIN
protected def beginBlock: String = programToBlock(initialProgram)

// Per-line
protected def eachLineActionBlock: String =
programToBlock(lineProgram ++ linePresentation.headOption.map(_ => Print(linePresentation)))


// END
protected def endBlock: String = programToBlock(endProgram)

protected def programToBlock(program: Seq[SideEffectStatement]) =
{program.map(_.toAwk) mkString "; "} +
program.headOption.map(_ => "; ").getOrElse("")

protected def optionsBlock: String =
{commandOptions mkString " "} + commandOptions.headOption.map(_ => " ").getOrElse("")

override def toAwk: String =
s"""|awk ${optionsBlock}'
|${identifyBlock("BEGIN", beginBlock)}
|${identifyBlock("", eachLineActionBlock)}
|${identifyBlock("END", endBlock)}'""".stripMargin.replace("\n", "")

//Auxialiary methods
private[this] def identifyBlock(blockName: String, blockAwkCode: String): String =
blockAwkCode.headOption.map(_ => s"$blockName{$blockAwkCode}").getOrElse("")

This class hierarchy enables code re-utilization, for example, SolidCommandWithLastAction is almost an exact copy of SolidCommand and nothing prevents us from extending it in order to define SolidCommandWithLastAction:

class SolidCommandWithLastAction(lastAction: Seq[SideEffectStatement])(prevSt: SolidCommand)
extends SolidCommand(prevSt) {...}

At this point, you should be able to start exploring the repository as well as to associate each state from the graph diagram with a state class in the code. Just in case, the following table collect these associations:

Graph Node

Entity

Is final state?

Entity Kind

init

lines

No

Singleton Object

command

CommandWithSeparator

No

Class

with line program

CommandWithLineProgram

No

Class

with initial program

CommandWithInitialProgram

No

Class

solid command

SolidCommand

Yes

Class

with last action

SolidCommandWithLastAction

Yes

Class

Transitions

Transitions between states are the easy part, they are as simple as methods returning new states. Thanks to Scala infix notation they create the illusion of natural language expressions, at least to some degree…

Some states might share transitions so it seems a good a idea to create traits containing them. By the use of mixing, states can thus use them as LEGO pieces in order to build their own transition set.

There are two special cases which deserve special attention: Groups of transitions and Empty input transitions.

Group of transitions…

… are composed by transitions which are always present together or which are different versions of the same transition. These are normally defined at the same trait named following the pattern To<STATE_CLASS_NAME>.

trait ToCommandWithSeparator {

  def splitBy(separator: String) = new CommandWithSeparator(separator)
  def splitBy(separator: Regex) =  new CommandWithSeparator(separator)

}

The example above is a rather clear case of two versions of the same transition: One receiving a string input and the other receiving a regular expression.

Note that, in relation with the abstract state machine, the machine input is both the method name and its parameters.

Empty input transitions

Consider the following transition, extracted from our state machine:

empty_transition

State machines can move from one state to another when the input is an empty string. It might seem bizarre but it can be done with our object modelling of state machines thanks to implicit conversions.

An implicit conversion from a state (Source) to another (Target) by just trying to access one of Target methods having a Source instance will be perceived by the DSL user as an empty transition. As simply as it sounds.

What is more, by just defining the implicit conversion at the companion object of either the Source or Target class/trait, it will be available in the scope of the sentence where the empty transition occurs. No need of imports which means: ABSOLUTELY TRANSPARENCY for the user.

Thus, the following code:

object ToCommandWithSeparator {
  implicit def toCommandWithSep(x: ToCommandWithSeparator) = new CommandWithSeparator()
}

… enables the transition described in the diagram below:

empty_transition2

VvqNk6o

– If ToCommandWithSeparator is a transition family trait, isn’t its equally named companion object the companion object of that transition family? Didn’t we set that the implicit conversion should be defined within Source or Target‘s companion object and, therefore, within a state class?

– Exactly! And what’s ToCommandWithSeparator‘s fate if not to be mixed at a state class definition?

In Scala, implicit conversions defined at a trait companion object will also apply for those classes extending or mixing that trait and they’ll be available on any scope where that class is available. This feature, besides being extremely useful, seems to be a very rational one: The class mixing/extending the trait can be regarded as a kind-of that trait, a subtype, so any of its instances are also of the type given by the trait and it should be expected that any conversion applying to that type could also apply to these instances.

Take, for example, the traits T and , both having companion objects where implicit conversions to and are respectively defined:

case class E(x: Int)
case class D(x: Int)

trait T
object T { implicit def toD(from: T): D = D(1) }

trait S
object S { implicit def toE(from: S): E = E(2) }

Mix both of them in the definition of the class C

case class C() extends T with S

… and check how a C instance is implicitly converted into instances of E or D.

scala> val c: C = C()
c: C = C()

scala> val d: D = c
d: D = D(1)
scala> val e: E = c
e: E = E(2)

Expressions

Most Scalawk transition inputs fit into the pattern transition name + basic type value. However, some of them receive expressions, identifiers or sentences. These are particular types created to express instructions and values within the AWK program. Hence they should not be part of a generic guide on how to create a DSL. Yet, the constructs behind them are not uncommon in many Scala Internal DSLs so we’ll take a brief look at some of them.

Identifiers in Scalawk (Internal Identifiers)

Some DSL expressions, such as arePresentedAs, need to make reference to AWK variables, declared by some other DSL expression. You could use strings to represent these internal identifiers. But having to surround our DSL identifiers in double quotes throws a bucket of cold water on its user experience, making the user conscious of the fact that she/he is actually using Scala instead of a crafted-from-scratch domain language.

Scala offers a mechanism to get unique objects for equal strings. That is exactly what a good DSL identifier needs.

If anyone writes:

'counter

… her/he will be obtaining a reference to a Symbol instance.  Symbol class has a name attribute whereby you can recover the character string used to obtain the instance.

The user would just write ‘counter and the DSL developer can obtain the string counter and use it for the internal representation of, in this case, the AWK variable.

Sentences

By combining internal identifiers with ad-hoc classes and implicit conversions it isn’t difficult to express assignation sentences, event algebraic operations.

's := 's + 1

This article is already far too long and with what has been hinted and the tricks of previous sections it is actually easy to understand the code to build this kind of expressions. That code is located under entities package.  Take that package as if it were a embedded DSL within Scalawk, yes, a DSL embedded in a DSL which is in turn embedded in Scala.

Some final thoughts

Developing internal DSLs isn’t precisely a piece of cake. If being forced to fall back to the host language constructs, users will easily wake up from the dream of being using a language built for them from the ground up. Nobody likes to be reminded he/her is not that special.

scala_has_you

You’ll encounter many pitfalls when trying to faithfully reproduce the state machine, the temptation to abandon the model is huge. Trust me, this is complex and, if you leave the path of the state machine, the ogres in the forest will have your heart for breakfast sooner than you imagine. Scala has proven it can offer a solution to any bump of the state machine road, outside that road you are free to face a minefield.

As a last piece of advice, I’d recommend you to buy a white/black board, a paper notebook and a good pen… whatever you feel confortable to draw on.

These are two examples of the early stages of design of Scalawk:

whiteboard

notebook

Think! draw! think! and draw again! so you are a better DSL architect than this guy…
BE-21-architect

… so your Neo(s) wouldn’t wake so easily.

Scala: Un lenguaje para gobernarlos a todos (II)

A ninguna aerolínea se le ocurriría poner a los mandos de un Airbus 380 a alguien sin entrenamiento.
Algo parecido ocurre con la creación de anillos de poder, ¡ups! DSLs, en Scala. Es una actividad que requiere entrenamiento, aunque por suerte no tan intensivo. Ha de ser tanto teórico como práctico pero, sobre todo, práctico.  En esta serie de posts ya se ha tratado algo de teoría y ahora toca la parte divertida del aprendizaje ¡Es hora de construir un DSL desde cero!

El objetivo de nuestro DSL

El fin último de nuestro futuro DSL es el de servir de ejercicio didáctico. Sin embargo, todo DSL ha de ser de ayuda en un dominio concreto, bien (o):

  • Facilitando la gestión o control de un sistema.
  • Sirviendo de lenguaje intermedio para otros lenguajes que son demasiado complejos o extensos.

En el ejemplo que vamos a desarrollar abordaremos el segundo caso.

Os presento a AWK

¡Menuda broma! Quién no conoce AWK ¡Si hasta tu Linux Box sabe qué es!

man awk | head -n 6

GAWK(1) Utility Commands GAWK(1)

NAME
gawk – pattern scanning and processing language

OK, es necesario recurrir a Wikipedia para encontrar una explicación algo menos tácita:

AWK es un lenguaje de programación diseñado para procesar datos basados en texto, ya sean ficheros o flujos de datos…


AWK fue una de las primeras herramientas en aparecer en Unix (en la versión 3) y ganó popularidad como una manera de añadir funcionalidad a las tuberías de Unix. La implementación de alguna versión del lenguaje AWK es estándar en casi todo sistema operativo tipo unix moderno.

Recordemos a este tandem de superheroes:

Gnu-and-penguin-color

Aunque no puede apreciarse en el dibujo, GNU tiene un cinturón en el que oculta un arma muy poderosa: AWK.
Su poder más destacado es el de permitir la creación de scripts que pueden transformar, filtrar, agregar… los datos originados por las salidas de otros mandatos. Convirtiéndose, de esta manera, en una herramienta fundamental para la construcción de scripts de sistema en los que se apoyan nuestras distribuciones y aplicaciones favoritas.

Ejemplo:

awk_sample

Con esta, aparentemente simple, línea hemos escrito un script capaz de indicarnos la memoria total utilizada por los módulos cargados en el sistema. Procesa la salida del comando de listado de módulos y agrega los valores de tamaño en memoria de cada entrada.

¿Un hueso duro de roer?

Siendo un lenguaje que aporta una máquina de Turing completa, las aplicaciones de AWK son infinitas. Es capaz de ahorrar horas de trabajo, facilitando la automatización de muchas tareas del sistema pero todo tiene un precio, y en el caso de AWK, el precio es que asusta a los usuarios que acuden por primera vez a él. Para muchos de ellos, se parece más a …

6bkFb7B

… que a una herramienta para facilitarles la vida. Las 1475 líneas de su entrada en Man tampoco suponen una lectura ligera.

Si en esa cabina ponemos a un piloto al que pedirle que nos guíe ¿A que perdemos un poco el miedo?

Los DSLs son pequeños y concisos, esto implica que son de gran ayuda ya que guían a sus usuarios a través del proceso de describir acciones

Si, parece que un DSL interno en Scala podría ser de gran utilidad como guía en la utilización de AWK.

Manos a la obra en la construcción del DSL de Scalawk

La característica más importante de un lenguaje de programación es el nombre…
Donald Erving Knuth

Empecemos por lo más importante, y si tomamos el principio de autoridad como un argumento válido, deberíamos empezar dándole nombre al DSL que vamos a construir.

Queremos componer programas AWK usando un DSL interno en Scala:

Scala + AWK = Scalawk

¡Buen comienzo!

Todo el código de Scalawk está disponible en GitHub:

git clone https://github.com/pfcoperez/scalawk.git

Divide y vencerás

En la entrega anterior de esta serie de posts llegamos a la conclusión de que la mejor forma de diseñar un DSL es usando el modelo de máquinas de estado. Estas máquinas son fácilmente divisibles en componentes:

  • Estados (siendo el estado inicial un caso especial).
  • Transiciones (cambios de estado):
    • Entradas que provocan la transición.
    • Efectos derivados de la transición:
      • La máquina cambia su estado.
      • Laterales: Al margen del cambio de estado, es posible que la transición provoque la generación de salida.
  • Alfabeto de la máquina: Conjunto de entradas válidas.

Diseñar y dibujar el diagrama de estados de la máquina, lo cual no es diferente a diseñar la guía interactiva que queremos construir, constituye el 90% del proceso creativo en la creación del DSL. La mayor parte del trabajo restante no es más que elegante y bonita fontanería Scala.

machine

Todas las posibles interacciones del usuario con Scalawk están representadas en el diagrama de estados anterior. Por ejemplo:

lines splitBy arePresentedAs ('c1, 'c2)

machine_walkthrough

Esta descomposición y modelado de la máquina de estados nos regala la estructura de paquetes de nuestro proyecto:

packages

Al grano: O como perder el miedo y disfrutar de la materia prima que nos da Scala

Los estados, transiciones y elementos auxiliares son entidades definidas dentro de los paquetes descritos más arriba. De hecho, no son más que objetos, clases, métodos y traits Scala.

Estado inicial

initial_state

Como ya sabemos, los estados son objetos. O bien son instancias de clases u objetos singleton. Por otra parte, también hemos vista que la forma correcta de implementar máquinas de estado es hacer que estos estados, en definitiva objetos, sean inmutables. De esa manera, son las transiciones las responsables de generar nuevos estados totalmente nuevos.

El estado inicial, al diferencia del resto de estados, no se genera por medio de una transición. Es el estado actual al iniciar la interacción con el DSL. Esto indica su naturaleza como singleton, la cual queda confirmada definitivamente por el hecho de que no puede existir ninguna otra instancia de él.

object lines extends ToCommandWithSeparator

Desde el punto de vista del usuario de Scalawk, este estado inicial debería ser una palabra indicando el comienzo de una frase en el DSL. Esta es otra razón para justificar el que sea implementado como un objeto singleton.

El estado inicial necesita transitar a otro, es por ello por lo que el singleton lines extiende el trait ToCommandWithSeparator. Para no adelantar acontecimientos basta que, por el momento, tengamos en cuenta que ToCommandWithSeparator es un trait que contiene un conjunto de transiciones.

Estados transitorios y finales

¡Claro! Los estados son objetos pero… ¿Eso es todo? ¡No!
Hay diferentes tipos de estados. Además, muchos de estos estados son bastante parecidos entre sí y podrían construirse a partir de una plantilla común. Revisemos algunos trucos y consejos para implementarlos.

La clasificación de más alto nivel para los estados no iniciales debería ser la siguiente: Transitorios finales.
Conceptualmente, los estados del primer grupo no pueden usarse para generar resultados en tanto que los del segundo sí. Obviamente, esta limitación ocurre igualmente en la implementación de los estados y ello implica que los estados transitorios no pueden generar programas AWK mientras que los finales si.

with_initialprogram_st
Estado no final
solidcomand_st
Estado final

En Scalawk, cualquier entidad capaz de generar código AWK, sin importar de que se trate de un programa completo o no, debería extender o mezclarse con el trait AwkElement.

trait AwkElement {
  def toAwk: String
}

De este modo, añadimos el método toAwk a dicha entidad. Esto es, le estamos otorgando del punto de entrada por el que la entidad es capaz de proveer código AWK.

A pesar de sus diferencias, prácticamente todos los estados comparten un conjunto común de atributos a partir del cual es posible componer cadenas de texto que contienen mandatos AWK:

  • Opciones de línea de comando, e.j: Token separador (espacio, salto de linea, …)
  • Programa inicial: Instrucciones a ser ejecutadas por AWK antes de empezar a procesar la entrada línea a línea. e.g: Inicializar un contador.
  • Programa de línea: Instrucciones que AWK ejecutará para cada una de las líneas de texto de entrada, las mismas instrucciones para todas las líneas. e.j: Imprimir una línea, incrementar un contador o acumulador, etc.
  • Programa final: Programa a ejecutar una vez que toda la entrada ha sido procesada, esto es, después de que el programa de línea se haya ejecutado para todas y cada una de las líneas de entrada. e.j: Imprimir el valor de los contadores.

En cada estado estos atributos pueden estar vacíos o no y, cuando se le pide a un estado final que genere un programa AWK, dichos atributos se utilizarán para generar el resultado en formato de cadena de texto.

abstract class AwkCommand protected(
  private[scalawk] val commandOptions: Seq[String] = Seq.empty,
  private[scalawk] val linePresentation: Seq[AwkExpression] = Seq.empty,
  private[scalawk] val lineProgram: Seq[SideEffectStatement] = Seq.empty,
  private[scalawk] val initialProgram: Seq[SideEffectStatement] = Seq.empty,
  private[scalawk] val endProgram: Seq[SideEffectStatement] = Seq.empty
) {
  def this(prev: AwkCommand) = this(
    prev.commandOptions,
    prev.linePresentation,
    prev.lineProgram,
    prev.initialProgram,
    prev.endProgram
  )
}

Hasta el momento sabemos de los estados no iniciales que:

  • Siempre contienen las piezas constituyentes de un resultado e inicialmente adquieren los valores de estas piezas a partir del estado que les precede: Por tanto, han de extender la clase abstracta AwkCommand.
  • Muy probablemente, añadan o cambien alguna de estas piezas de información: En ese caso, han de sobreescribir todos o algunos de los atributos de  AwkCommand.
  • De forma opcional, pueden transitar a otro estado: En cuyo caso, han de ofrecer al menos un método cuyo valor de retorno sea del tipo del estado destino. También podrían mezclar sus clases con algún trait de familia o conjunto de transiciones.

Surge la pregunta ¿Por AwkCommand es una clase abstracta y no un trait?
Bien, el objetivo de AwkCommand es proveer código reutilizable para garantizar la continuidad. Es decir, proveer un constructor para construir nuevos estados a partir del que les precede (parámetro prev). Gracias a este constructor, el código de cada clase de estado queda reducido a tan sólo la definición de sus transiciones y sobreescrituras de los atributos heredados de AwkCommand (únicamente cuando estos atributos han de cambiar al transitar al nuevo estado).

Obviamente, la única forma de proveer un constructor en una jerarquía de clases es por medio de una clase, no hay ningún problema si esta clase no debe ser instanciada: Basta con hacerla abstracta.

abstract_with_constructor

class CommandWithLineProgram(
                              statements: Seq[SideEffectStatement]
                            )(prev: AwkCommand) extends AwkCommand(prev)
  with ToSolidCommand {

  override private[scalawk]val lineProgram: Seq[SideEffectStatement] = statements

}

La clase CommandWithLineProgram se corresponde con un estado no final y por ello no mezcla el trait AwkElement.

//This is the first state which can be used to obtain an AWK command string `toAwk`
class SolidCommand(val lineResult: Seq[AwkExpression], prevSt: AwkCommand) extends AwkCommand(prevSt)
  with AwkElement
  with ToCommandWithLastAction {
 ...
 ...
 ...
}

En cambio, SolidCommand representa un estado final y ha de proporcionar una implementación del método toAwk:

 // AWK Program sections

// BEGIN
protected def beginBlock: String = programToBlock(initialProgram)

// Per-line
protected def eachLineActionBlock: String =
programToBlock(lineProgram ++ linePresentation.headOption.map(_ => Print(linePresentation)))


// END
protected def endBlock: String = programToBlock(endProgram)

protected def programToBlock(program: Seq[SideEffectStatement]) =
{program.map(_.toAwk) mkString "; "} +
program.headOption.map(_ => "; ").getOrElse("")

protected def optionsBlock: String =
{commandOptions mkString " "} + commandOptions.headOption.map(_ => " ").getOrElse("")

override def toAwk: String =
s"""|awk ${optionsBlock}'
|${identifyBlock("BEGIN", beginBlock)}
|${identifyBlock("", eachLineActionBlock)}
|${identifyBlock("END", endBlock)}'""".stripMargin.replace("\n", "")

//Auxialiary methods
private[this] def identifyBlock(blockName: String, blockAwkCode: String): String =
blockAwkCode.headOption.map(_ => s"$blockName{$blockAwkCode}").getOrElse("")

La jerarquía de clases presentada posibilita la reutilización. Por ejemplo, SolidCommandWithLastAction es prácticamente una copia de SolidCommand y nada nos impide que extendamos esta segunda clase a la hora de definir SolidCommandWithLastAction.

class SolidCommandWithLastAction(lastAction: Seq[SideEffectStatement])(prevSt: SolidCommand)
extends SolidCommand(prevSt)

En este punto, deberíamos ser capaces de empezar a explorar el repositorio y asociar cada estado del grafo con su clase correspondiente en el código.

Nodo en el grafo

Entidad

¿Es estado final?

Tipo de entidad

init

lines

No

Objeto singleton

command

CommandWithSeparator

No

Clase

with line program

CommandWithLineProgram

No

Clase

with initial program

CommandWithInitialProgram

No

Clase

solid command

SolidCommand

Clase

with last action

SolidCommandWithLastAction

Clase

Transiciones

Las transiciones entre estados son bastante más sencillas, son simples métodos que devuelven nuevos estados. Gracias a la notación infija, Scala proporciona la ilusión de estar manejando expresiones en lenguaje natural, tan natural como describir en nuestro idioma humano lo que queremos.

Algunos estados pueden compartir transiciones por lo que una buena idea es agruparlos en traits. Por medio de mixings, los estados pueden usar estos traits de transición como si de piezas de LEGO se tratase para construir sus conjuntos de transiciones.

Hay dos casos especiales que requieren especial atención: Grupos de transiciones transiciones de entrada vacía.

Los grupos de transiciones…

… están compuestos por transiciones que siempre se agrupan de la misma forma, digamos que son transiciones amigas que jamás se separan, a veces estas transiciones son diferentes versiones de la misma transición y el trait en el que se agrupan suele tomar nombres que encajan en el patrón To<TargetState>.

trait ToCommandWithSeparator {

  def splitBy(separator: String) = new CommandWithSeparator(separator)
  def splitBy(separator: Regex) =  new CommandWithSeparator(separator)

}

Este es un caso bastanete evidente de dos versiones de la misma transición: Una en la que recibe una cadena de texto y otra en la que recibe una expresión regular.

Nótese que, en relación con la máquina abstracta de estados, la entrada es la conjunción del nombre del método de transición y sus parámetros.

Transiciones de entrada vacía

Consideremos la siguiente transición (extraída de la máquina de estados):

empty_transition

Las máquinas de estado pueden transitar con entradas vacías. Esto puede parecer extraño pero es bastante común, tanto que ocurre con Scalawk. Además, no hay problema a la hora de plasmar esto en el modelo que estamos implementando con Scala gracias a las conversiones implícitas.

Una conversión implícita desde un estado (Fuente) a otro (Objetivo), que se aplica al intentar acceder a uno de los métodos de Objetivo teniendo una instancia de Fuente, será percibida por el usuario del DSL como una transición de entrada vacía. Tan simple como suena. 

Es más, basta con definir la conversión implícita en el objeto de compañía de Fuente o en el de Objetivo  para  que esté disponible en el ámbito de la sentencia en la que la transición de entrada vacía ocurre. Sin necesidad de imports explícitos, esto es: De forma TOTALMENTE TRANSPARENTE para el usuario.

Así, el siguiente código:

object ToCommandWithSeparator {
  implicit def toCommandWithSep(x: ToCommandWithSeparator) = new CommandWithSeparator()
}

… activa la transición descrita en el diagrama presentado a continuación:

empty_transition2

VvqNk6o
– Si ToCommandWithSeparator es un trait de familia de transiciones, el objeto homónimo (del snippet de código anterior) ¿No es el objeto de compañía de un trait de familia de transiciones? ¿No hemos dicho que la conversión implícita ha de estar definida en el objeto de compañía de Fuente Objetivo y, por tanto, en el objeto de compañía de una clase de estado?

– ¡Y así es! ¿Cual es el fin de ToCommandWithSeparator si no ser mezclado en una clase de estado?

En Scala, las conversiones implícitas definidas en el objeto de compañía de un trait aplicarán también a las clases que mezclen o extiendan ese trait y estarán disponibles en todos los scopes en los que la clase esté disponible. Esto, además de ser extremadamente útil para casos como el que nos ocupa, encaja con el sentido común: Se entiende que dicha clase es un sub-tipo del trait.

Por ejemplo, si declaramos los traits S, con objetos de compañía en los que  hay conversiones implícitas a respectivamente:

case class E(x: Int)
case class D(x: Int)

trait T
object T { implicit def toD(from: T): D = D(1) }

trait S
object S { implicit def toE(from: S): E = E(2) }

Para luego mezclarlos en en una tercera clase C…

case class C() extends T with S

… entonces, cualquier instancia de C puede implícitamente convertirse en instancias de E o D:

scala> val c: C = C()
c: C = C()

scala> val d: D = c
d: D = D(1)
scala> val e: E = c
e: E = E(2)

Expresiones

La mayor parte de las transiciones posibles en Scalawk encajan con el patrón transición + valor de tipo básico.
Pero algunas de ellas reciben expresiones, identificadores o sentencias. Estos son valores de tipos creados ad-hoc para expresar instrucciones y valores internos al lenguaje AWK. Ergo, los detalles acerca de estos tipos no deberían formar parte de una guía genérica para la creación de DSLs. Aún así, las construcciones Scala detrás de estos tipos son muy comunes en el universo de los DSLs creados a partir de Scala y merece la pena mencionarlas, al menos de forma superficial.

Identificadores (internos) en Scalawk

Algunas expresiones de este DSL, como arePresentedAs, necesitan hacer referencia a variables dentro del programa AWK, declaradas previamente por medio de otra expresión del DSL. Se podrían usar cadenas de texto para representar estos identificadores internos. Tener que envolver los identificadores en comillas dobles es lanzar una jarra de agua fría sobre la experiencia del usuario, haciéndole consciente del hecho de que, realmente, está utilizando Scala en vez de un lenguaje de dominio.

Scala ofrece un mecanismo para obtener objetos únicos para cadenas iguales. Y es eso, precisamente, lo que un buen identificador interno al DSL necesita.

Si alguien escribe:

'counter

… obtendrá una referencia a una instancia de la clase Symbol. Esta clase tiene un atributo nombre gracias al cual, su usuario puede recuperar la cadena de texto que se utilizó para obtener la instancia.

De esta forma,  el usuario escribiría ‘counter y del desarrollador del DSL podría contar con obtener la cadena counter y usarla para la representación interna de, en este caso, la variable de AWK.

Sentencias

Mediante la combinación de los identificadores internos (arriba descritos) con clases ad-hoc y conversiones implícitas es relativamente sencillo expresar sentencias de asignación e incluso expresiones algebraicas.

's := 's + 1

Este artículo está empezando a alargarse y, con las pistas y trucos que hemos ido desarrollando a lo largo de las secciones anteriores, es posible entender el código de Scalawk dedicado a la construcción de este tipo de expresiones. Ese código se encuentra dentro del paquete entities. Ese paquete puede verse como otro DSL embebido en Scalawk ¡Sí! un DSL dentro de otro DSL que, por último, se encuentra embebido en Scala

Divagaciones finales

Nadie dijo que desarrollar DSLs internos fuese pan comido. Es fácil hacer que el usuario se despierte del sueño de estar usando un lenguaje diseñado desde el principio para él/ella. Esto ocurre cuando se encuentra con construcciones que, sin motivo evidente, no deberían ser ser como son si no fuese porque pertenecen al lenguaje que alberga al DSL.

scala_has_you

Es común encontrar obstáculos en el camino al intentar reproducir la máquina de estados de forma fidedigna. La tentación a abondar el modelo puede llegar a ser irresistible. Creedme cuando os digo que esta es una actividad compleja en la que, si abandonáis la calzada de adoquines que proporciona el modelo de máquina de estados, los ogros del bosque del “todo vale” devorarán vuestros corazones antes de que os deis cuenta.

Scala ha probado su valía a la hora de ofrecer soluciones a los baches y obstáculos en el camino de las máquinas de estado, salid de este e intentad sobrevivir en un campo de minas.

Como último consejo, os recomiendo que compréis pizarras, cuadernos, piedras y cinceles… cualquier cosa con la que os sintáis cómodos dibujando grafos.

Estos son dos bocetos de un incipiente Scalawk:

whiteboard

notebook

¡Pensad! ¡Dibujad! ¡Pensad de nuevo! Así seréis mejores arquitectos de DSL que este…
BE-21-architect

… y vuestros Neo(s) nunca despertarán.

Scala: One language to rule them all (I)

In the beginning, there were not programming languages, just machine code. Automatic programming came to save all us by casting power rings to rule the machines: Programming languages. They diversified, inspired new ones, constituted a rich ecosystem. Among them appeared a highly useful kind of computer languages: DSLs.

DSLs

Domain Specific (computer) Languages have been around for quite a long time. The academic description of this kind of languages states that they are centred and used for an specific application domain. DSLs are small and concise which implies that they guide their users in the process of describing actions, entities or relations within their domain of application. What’s more important, they are made to fit their purpose.

If I am doing statistics research, why should worry about memory management?!!

It isn’t surprising that a whole kingdom of these mini-languages has evolved and took over machine learning tools, scientific programming, hardware design and management, modeling…

Wasn’t this a blog on Scala? Leave the history for academics!

Yeah this might sound as the stories from the past. However, in the context of the current times, when we all seem obsessed by data acquisition, storage and analysis and taking into account that this data is usually complex to manage because of its variability. We are forced to deal with dozens of different ways of managing data and many of them pass through the use of DSLs: SQL, HiveQL, CQL, bash, grep, awk, R, C’mon! How can I even finish the list! Let’s forget of what is to come.

What if a tool would give as the power to create simple, guided and short languages to perform specific domain tasks within.  What if Scala was a kind of DSL where the D stands for the Domain of Creating new DSLs?!

When its creators named Scala they were not just thinking on its capabilities in code reuse and  potential use in horizontal concurrent environments; they also kept in mind the extensibility of the language. Some of the features in that direction are:

  • Infix notation: objectA.method(objectB) can be written as objectA method objectB
  • Lack of operators: There are no operators as separated entities, just methods. Precedence order and associativity rules are provided by the last character of each method name.This way, any method name NOT ending with `:` gets associated from left to right:
    obj1 + obj2 is the same as writing obj1.+(obj2) whereas obj1 +: obj2 is as writing obj2.+:(obj1).
    Similarly, operator precedence is provided by a priority list of method name ending characters. e.g: `*` gets a higher priority than `+`, obj1 + obj2 * obj3  is always interpreted as obj1 + (obj2 * obj3).The mentioned precedence priority list is as follows:

    • Any letter, no matter case.
    • |
    • ^
    • &
    • Symbols = and !
    • Symbols < and >
    • :
    • Arithmetic operations + and –
    • Arithmetic operations  *, / and %
    • Any other special character.
  • Advanced object oriented features: object, trait, …

These features can be combined to model and build internal DSLs within the Scala programming language.

Scala DSLs 101

Infix notation is the main feature to create our own embedded languages.

Consider the following trait:

trait MovingRobot {
  def moveForward(): MovingRobot = {
    println("Robot moved one position forward")
    this
  }
  def moveBackward(): MovingRobot = {
    println("Robot moved one position backward")
    this
  }
}

Which can be mixed in an object declaration as:

object robot extends MovingRobot

Its methods can be called using traditional dot notation:

robot.moveForward.moveBackward

But infix notation give us a more natural way to talk with this simple bot:

robot moveForward() moveBackward
robot moveForward
robot moveBackward

This is the simplest  of all possible DSLs.

State transitions

Yes, simple but rather imperative and useless.  Commands are not changing the system state besides the lateral effect behind println:

simpliest_automaton

At this point, there are two options to model the effects of the DSL instructions:

  • The mutable approach: Somehow easiest to Scala new comers from imperative languages but it is way more bug prone. This one is rather similar to the approach followed by so many builders in Java. Check Java’s StringBuilder:

    The builder state is the string that is being composed. Methods, such as append(double d), return a reference to the instance of the builder whose state has been altered by the very same method.  Hence, the same reference is always returned since is the same StringBuilder instance which is mutating call after call, sounds familiar?!mutable_state
  • The immutable one (or the path of wisdom): Do not change anything, return a new state with the attributes derived from the previous state your action. From now, this post will only cover this approach.

The beauty of the second solution is that each action returns a new state object having a 1 to 1 relation with the system state. That is, the code entities are a perfect reflection of all the changes. Moreover the state is immutable by definition.

state /steɪt/  n., adj., v., stat•ed, stat•ing. 
n.

  1. the condition of a person or thing with respect to circumstances or experiences;
    the way something is[countable; usually singular]the state of one’s health.
  2. the condition of substances with respect to structure, form, etc.[countable]Water in a gaseous state is steam.

(www.wordreference.com)

Discussing why immutability drives to way less buggy systems is out of the scope of this post, hundreds of explanations can be found by googling “immutability reduces bugs”. Even Java creators decided it was better, at least for their strings.

state_transition

Each transition returning a whole new state reduces its responsibility  to just one: To generate a new state hence simplifying the DSL design. No changes in the state are to be expected beyond explicit transition calls.

The nitty-gritties of immutable state transitions

Following the utterly complex example of our uni-dimensional robot API (at this point you must have realized that the previous Scalera Challenge included a beautiful DSL), it can be altered to make it follow the above-described functional approach:

// All states extend `RobotState`
trait RobotState {
  def position: Int
}

// Transitions which can be mixed with any state for which they
// make sense.

trait MovementTransitions {
  self: RobotState =&amp;gt;

  def moveForward(nSteps: Int = 1): RobotState with MovementTransitions

  def moveBackward(nSteps: Int = 1): RobotState with MovementTransitions

}

// States
// In this example, states only differ in the robot position so they all
// are represented by the same case class.
case class Robot(position: Int) extends RobotState with MovementTransitions {

  def moveForward(nSteps: Int = 1) =
    Robot(position + nSteps)

  def moveBackward(nSteps: Int = 1) =
    Robot(position - nSteps)

}

// Initial state
val robot = new Robot(0)

And its use:

robot moveForward(10) moveBackward() position

The code above is an oversimplification but shows the basic tricks behind Scala DSLs, namely: The use of infix notation, families of states and transitions only usable within state definitions.

A bit of theory: Really? State Machines?

Is the state machine model actually needed to implement DSLs? Yes, if you like avoiding shooting yourself in the foot.

shootfoot

Immutable state machines are easy to understand, maintain and expand.

On the other hand, DSLs are languages, formal languages with grammars with a place in Noam Chomsky’s classification, commonly, Regular Grammars and Context-Free grammar.

  • Which theoretical machine is able to recognize/process languages with regular grammars? A finite state automaton
  • In the case of Context-Free grammar languages, they can be processed by push-down automatons which (ALERT! Oversimplification ahead) can be regarded as a finite automaton enjoying the perk of making use of its own stack to place and read symbols.

The transition model described afore seems to be just made for implementing this kind of machines. A self-answered question arises as to whether DSLs’ developers should dedicate their efforts to find buggy and flimsy solutions when such a solid model is available.

– Well, I suggest you gentlemen invent a way to put a square peg in a round hole.

gene_krantz

– Sir, we have plenty of round pegs!

engineer

Coming soon…

In the next episode: A practical DSLs development step by step, no more history, theory or that silly things nobody cares about! Just…

letsdoit

Scala: Un lenguaje para gobernarlos a todos (I)

Al principio, no teníamos lenguajes de programación, sólo código máquina. Entonces, la programación automática vino para salvarnos a todos. Proporcionando anillos de poder para controlar a las máquinas: Estos fueron llamados “Lenguajes de programación”. Se diversificaron, sirvieron de inspiración para otros nuevos y acabaron constituyendo un rico ecosistema. De entre ellos surgió un nuevo y útil género de lenguajes para ordenadores: Los DSLs.

DSLs

Los lenguajes de propósito específico, Domain Specific (computer) Languages, existen desde hace bastante tiempo. La definición académica para este sub-grupo establece que han de estar enfocados en el marco de un dominio concreto de aplicación. Los DSLs son pequeños y concisos, esto implica que son de gran ayuda ya que guían a sus usuarios a través del proceso de describir acciones, entidades y relaciones dentro de su dominio de aplicación. Aún más importante es el hecho de que están hechos y diseñados para servir a un único propósito.

Si soy un matemático y estoy haciendo un estudio estadístico con mi ordenador ¿Por qué debería preocuparme acerca de la gestión de memoria?

No es de sorprender que haya surgido un gran reino de mini-lenguajes que han tomado el poder sobre actividades teles como el aprendizaje automático, la programación científica, el diseño y gestión de hardware, modelado de datos, …

¡Y a mi qué! ¿No era esto un blog sobre Scala?

¡Cierto! todo esto puede sonar a antiguas batallitas. Sin embargo, en una actualidad en la que todos parecemos estar obsesionados por la adquisición, almacenamiento y análisis de grandes volúmenes de datos que son, a su vez, de naturaleza muy variable (o lo que es lo mismo, mezclando peras con manzanas); Estamos obligados a enfrentarnos a docenas de formas de tratar estos datos y muchas de estas formas no son más que casos concretos de DSLs: SQL, HiveQL, CQL, bash, grep, awk, R… !Por favor! !Cuando va a acabar esta lista! Probablemente: Nunca. Y eso que estamos obviando lo que está por venir.

Que maravilla si hubiese una herramienta que nos ayudase, pobres humanos, a construir DSLs, un meta-DSL que, de alguna manera, nos guiase a la hora de desarrollar nuevos DSLs. Esta herramienta existe, y se llama Scala.

Cuando los creadores de este gran (no sólo por sus bondades sino por su extensión) lenguaje de programación le pusieron nombre, no sólo lo hicieron pensando en su capacidad para la re-utilización de código y potencial uso en entornos concurrentes con facilidad para escalar horizontalmente, sino que también tenían en mente su potencial para ser fácilmente expandido. Quizás sea esa la razón de su basta extensión en sintaxis y herramientas. Algunas de las características de Scala en la dirección de la expansibilidad son:

  • Notación infija: Cualquier llamada a un método que recibe un objeto como parámetro puede ser escrita de forma infija. Esto es,
    objA.metodo(objB) puede escribirse como objA metodo objB.
  • No existencia de operadores: A diferencia de otros lenguajes, carece de operadores como elementos con entidad propia. Todo son métodos cuyas reglas de precedencia y asociatividad están determinadas por el último carácter del nombre del método.Así, cualquier método cuyo nombre no acabe en el caracter `:` asocia de izquierda a derecha: obj1 + obj2 es lo mismo que obj1.+(obj2) en tanto que obj1 +: obj2 es lo mismo que obj2.+:(obj1).
    Algo parecido ocurre con la precedencia ya que existe un orden de importancia de caracteres finales, un ejemplo es la mayor importancia de `*` frente a `+` :
    obj1 + obj2 * obj3  
    es siempre igual a obj1 + (obj2 * obj3).Esta es la lista de importancia (de menor a mayor prioridad):

    • Todas las letras del alfabeto, en mayúsculas o minúsculas.
    • |
    • ^
    • &
    • Los símbolos = y !
    • Los símbolos < y >
    • :
    • Las operaciones aritméticas + y –
    • Las operaciones aritméticas *, / y %
    • Cualquier otro caracter especial.
  • Características avanzadas de programación orientada a objetos: object, trait, …

 

Estas tres características se combinan para proporcionar un entorno completo para el desarrollo de DSLs internos en el seno de Scala.

Primeros pasos

La notación infija sienta las base sobre la que construir nuestros lenguajes embebidos en Scala.

Partiendo, por ejemplo, del siguiente trait:

trait MovingRobot {
  def moveForward(): MovingRobot = {
    println("Robot moved one position forward")
    this
  }
  def moveBackward(): MovingRobot = {
    println("Robot moved one position backward")
    this
  }
}

Que puede mezclarse en la declaración de un objeto:

object robot extends MovingRobot

Podemos modelar los movimientos del un robot virtual llamando a los métodos de MovigRobot de una forma tradicional:

robot.moveForward.moveBackward

Pero el uso de la notación infija proporciona da lugar a un código mucho más cercano al lenguaje natural:

robot moveForward() moveBackward
robot moveForward
robot moveBackward

Este es el tipo más simple de los DSLs embebidos en Scala y sirve como punta de partida para enfoques más avanzados.

Transiciones de estado

Sí, simple y también imperativo además de ser de poca utilidad. Los comandos no están cambiando el estado del sistema más allá del efecto lateral que implica la impresión de caracteres por la salida estándar realizada por medio de println:

simpliest_automaton

Con DSLs de este nivel, existen dos opciones para modelar los efectos de las instrucciones del DSL:

  • El enfoque mutable: Es el más sencillo para aquellos llegados a Scala desde lenguajes imperativos pero, definitivamente, es mucho más arriesgado en lo que a la introducción de bugs respecta. La idea es muy similar a a que hay detrás de tantos builders o acumuladores de Java, por ejemplo, StringBuilder:El estado del acumulador es la cadena que se está componiendo. Métodos, como append(double d), devuelven una referencia a la instancia del acumulador cuyo estado se está modificando como consecuencia de la llamada. De esta manera, la misma referencia se devuelve llamada tras llamada ya que es la misma instancia de StringBuilder la que muta. ¿Suena familiar?

    mutable_state

  • El enfoque inmutable (o el camino a la sabiduría):  ¡No se debe cambiar nada! Hay que devolver un nuevo estado con los atributos derivados del estado anterior y la acción realizada. De ahora en adelante, sólo se tratará esta técnica.

La belleza de la segunda solución radica en que cada acción devuelve un único estado que mantiene una relación 1 a 1 con los estados reales del sistema modelado. Esto significa que las entidades del código que implementa al DSL son un reflejo exacto de los cambios y estados que se desean representar. Además, el estado es inmutable por definición.

estado
Del lat. status.Escr. con may. inicial en aceps. 6 y 7.
1. m. Situación en que se encuentra alguien o algo, y en especial cada uno de sus sucesivos modos de ser o estar.

(www.rae.es)

Explicar el por qué del hecho de que la programación basada en la inmutabilidad es más segura (en lo que la introducción de bugs respecta) que la basada en la mutabilidad de objetos está fuera del alcance de este artículo, cientos de explicaciones están al alcance de buscador. Algunas razones están muy bien resumidas en este artículo de IBM.

Incluso, los creadores de Java, decidieron que la inmutabilidad era mejor, al menos para sus cadenas de texto.

state_transition

Devolviendo en cada transición un, totalmente nuevo, estado se reduce la lista de responsabilidades del código que implementa dicha transición a sólo una: Generar un nuevo estado. Esto genera diseños mucho más sencillos para el conjunto del DSL. No pueden darse cambios inesperados fuera de llamadas explícitas a los métodos de transición.

Al grano de las  transiciones inmutables

Siguiendo el extremadamente complejo ejemplo de nuestra API para robots unidimensionales (llegados a este punto, un ávido lector de Scalera probablemente se de cuenta de que el anterior reto Scalera incluía un bonito DSL). Este API puede cambiarse para seguir el enfoque funcional arriba descrito:

// All states extend `RobotState`
trait RobotState {
  def position: Int
}

// Transitions which can be mixed with any state for which they
// make sense.

trait MovementTransitions {
  self: RobotState =>

  def moveForward(nSteps: Int = 1): RobotState with MovementTransitions

  def moveBackward(nSteps: Int = 1): RobotState with MovementTransitions

}

// States
// In this example, states only differ in the robot position so they all
// are represented by the same case class.
case class Robot(position: Int) extends RobotState with MovementTransitions {

  def moveForward(nSteps: Int = 1) =
    Robot(position + nSteps)

  def moveBackward(nSteps: Int = 1) =
    Robot(position - nSteps)

}

// Initial state
val robot = new Robot(0)

A continuación, un sencillo ejemplo de uso:

robot moveForward(10) moveBackward() position

Esto es una simplificación extrema que muestra las técnicas básicas detrás de los DSLs embebidos en Scala que puede resumirse en:

  • El uso de la notación infija.
  • De familias de estados y transiciones entre estos.
  • Limitación de las transiciones de forma que sólo pueden producirse desde un estado y resultar en otro completamente nuevo que, podría compartir, la mayoría de los atributos con su predecesor.

Un poco de teoría: Máquinas de estado ¿En serio?

¿El realmente necesario un modelo de diseño y programación basado en máquinas de estado?  La respuesta es sí, siempre que no quieras acabar dándote un tiro en tu propio pie.

shootfootLas máquinas de estados inmutables son sencillas de mantener, entender y expandir.

Por otro lado, hay que tener en cuenta que los DSLs no son otra cosa que lenguajes formales con gramáticas que tienen su lugar en la clasificación de Chomsky: Habitualmente Gramáticas regulares y gramáticas independientes de contexto.

  • ¿Qué tipo de máquina es capaz de procesar/reconocer lenguajes basados en gramáticas regulares? Los autómatas finitos.
  • En el caso de las gramáticas independientes de contexto, son los autómatas de pila los que pueden reconocer/procesar sus lenguajes. Estos (¡Ojo! Simplificación) no son más que autómatas finitos que pueden hacer uso de una pila auxiliar dónde colocar y de dónde leer símbolos que pueden determinar el resultado de una transición, junto con el estado anterior y la entrada.

El modelo de transiciones expuesto en las secciones anteriores parece encajar a la perfección con las máquinas teóricas de estado. Surge una pregunta que es, as su vez, su propia respuesta: ¿Debería un desarrollador invertir su tiempo en buscar nuevas soluciones, poco probadas y que pueden ser inestables o estar cargadas de bugs cuando tiene un modelo tan firme a su alcance?

– Bien, les sugiero caballeros que inventen una forma de encajar una clavija cuadrada en un agujero redondo.

gene_krantz

– Pero, señor, ¡Si nos sobran clavijas redondas!

engineer

En el siguiente episodio…

Desarrollo práctico de un DSL útil, paso a paso. Sin historias de guerra, teoría y sin ninguna de esas cosas “tan aburridas”. Sólo…

letsdoit