Evaluación de expresiones aritméticas en notación polaca inversa (RPN)

Detalles

rpnLas expresiones aritméticas que utilizamos habitualmente en la actualidad se basan en una serie de operadores (la suma '+', la resta '-', y algunos otros que habitualmente colocamos entre dos operandos. Por ejemplo, si queremos sumar 3 y 7, lo expresamos como 3+7. Es lo que llamamos notación infija.

Cuando combinamos varias operaciones de éste tipo en una expresión, nos surgen algunas peculiaridades de ésta notación. Por ejemplo, si queremos sumar 3 al resultado multiplicar 7 por 2 (denotaremos la multiplicación con el signo '*'), lo podemos escribir como 3+7*2, pero debemos establecer unas prioridades en las operaciones. Primero se hacen las multiplicaciones y divisiones de izquierda a derecha, y luego las sumas, de tal manera que primero deberíamos evaluar 7*2=14, y finalmente 2+14=16. Por convención, mantenemos siempre este orden.

Sin embargo, si quisiéramos sumar 3+7, y el resultado multilicarlo por 2, nos vemos obligados a introducir paréntesis: (3+7)*2.

Bueno... no supone un gran problema. Conocemos varias técnicas que nos permiten evaluar una expresión de éste estilo, aun con las prioridades y los paréntesis.

No obstante, existe una alternativa bastante curiosa. Hay una forma de escribir expresiones con el operador al final. Es decir, primero se escriben los operandos, y finalmente el operador, de tal manera que sumar 3 y 7 se expresa 3 7 +.

Análogamente, 3+7*2 se expresa 3 7 2 * +, y (3+7)*2 se expresa 3 7 + 2 *. Es la llamada notación polaca Inversa (abreviado RPN -Reverse Polish Notation-).

Ésta forma de escribir las operaciones se utilizó en algunas calculadoras electrónicas, incluyendo la famosa Hewlett Packard 35, de los años 70, considerada la primera calculadora científica de bolsillo.Tiene dos características curiosas: no necesita precedencias de operadores ni paréntesis, y para evaluar expresiones sólo necesitaremos una estructura de tipo LIFO (una pila) junto con un sencillo algoritmo.

Así que en este artículo nos dedicaremos a construir un algoritmo para evaluar este tipo de expresiones.

El nombre

HP-35
HP 35
La calculadora HP 35, considerada la primera calculadora científica de bolsillo, utilizaba RPN.
[imagen de wikipedia]

Aunque suele conocerse como notación postfija, el sobrenombre de notación polaca inversa (en adelante la abreviaremos como RPN) parece que deriva del hecho de que el matemático polaco Jan Łukasiewicz utilizaba en los años 1920 una notación en la que los operadores se colocaban delante de todos sus operandos (apodada notación polaca y también llamada notación prefija), y que tampoco necesitaba de paréntesis ni prioridades en los operadores.

En los años 50-60 se recupera la idea, pero con los operadores detrás de los operandos, y por extensión, como en RPN se colocan los operadores detrás, se le llamó notación polaca inversa. Tuvo un cierto auge en los 60 debido a que es cuando realmente empieza a establecerse la algoritmia computacional como disciplina. La RPN ofrecía un algoritmo sencillo para evaluar expresiones complejas.

Hewlett-Packard la utilizó para varios modelos de sus calculadoras científicas en los años 60 y 70, y todavía hoy algunos modelos la utilizan también.

Comprendiendo la notación

Jan Łukasiewicz
lukasiewicz

La idea de ésta notación es mucho más sencilla de lo que parece, pero normalmente en un primer contacto cuesta adaptarse. Vamos a utilizar, por ejemplo, los cuatro operadores aritméticos (+, -, multiplicación con un asterisco * y división con una barra / )

Cada uno de estos operadores es binario. Es decir, cada uno de ellos necesita dos operandos. Para sumar dos números cualesquiera, en la notación infija  colocamos el operador entre los operandos. En notación postfija se coloca detrás. Además, separamos operandos y operadores con espacios. Esa idea es simple.

  • Ej: 2+4 lo expresamos 2 4 +.

Ahora bien, el caso es que cada uno de los operandos puede ser, a su vez, el resultado de otra operación, que también se expresa en RPN.

  • Ej: 2+(3*5) es una suma de dos operandos: el 2 y (3*5)2 3 5 * + Fijémonos en la expresión. El primer operador que encontramos es *. Esa es la operación a realizar en primer lugar, y sus operandos son los dos valores inmediatamente anteriores. El resultado de 3 5 * es  15 , así que, resolviendo la expresión, ese trozo lo podemos sustituir por su valor 15, quedando como 2 15 +, es decir, 17.

Asi, podemos construir expresiones más complejas, prescindiendo de paréntesis y prioridades.

  • Ej:  14*(7+4+11/5)-9 → 14 7 4 + 11 5 / + * 9 -

Extendiendo la notación

Nosotros vamos a construir una pequeña calculadora que resuelva expresiones en RPN. Además de los cuatro operadores aritméticos podemos incluir las operaciones numéricas que consideremos oportuno... el límite son las ganas que tengas de ampliar el algortimo.

Por ejemplo, si deseasemos implementar en nuestra calculadora una operación potencia representada como un operador con el símbolo ^, de tal manera que 37 lo escribieramos en RPN como 3 7 ^, pues sería relativamente sencillo de implementar.

Lo mismo ocurre con funciones trigonométricas (como seno, coseno, tangente), raíces... o cualquier otra cosa. Sólo hay que tener un concepto claro: la aridad de la operación.

La aridad es el número de operandos que necesita una operación. Por ejemplo, la suma es una operación de aridad 2 (por eso decimos que la suma es un operador bin-ario).
Una función trigonométrica, como el coseno tiene una aridad 1, porque se aplica sobre un solo valor (por eso decimos que es una operación un-aria).

Podemos incluso considerar una operación que no tome argumentos, pero que aun así devuelva un valor (serian de aridad 0, o 0-arias), y podríamos utilizarlas para devolver constantes interesantes, como pi.

Es decir, la calculadora es nuestra, así que a nosotros nos toca decidir qué operaciones implementará y cuál sera el operador que utilicemos (teniendo clara la aridad de la operación), que no necesariamente tiene por qué ser un símbolo. Por ejemplo, podemos decidir que la palabra cos representa a la operación unaria de obtener un coseno, y la palabra pi representa a una operación 0-aria que devuelve el valor de pi.

De este modo, una expresión como cos(2*π3)+5 la expresaríamos para nuestra calculadora RPN como 2 pi 3 ^ * cos 5 +. Se acostumbra uno en seguida.

El algoritmo

Evaluar una expresión RPN es sencillo comparado con las expresiones en notación infija. Separaremos tanto operadores como operandos con espacios. Con esto se simplifica la obtención de los distintos elementos que componen la expresión.

Cada uno de estos elementos debe ser, o bien un número, o bien un operador.

El algoritmo
Utilizaremos una estructura LIFO, una pila para almacenar valores. Ésta estructura nos ayudará a operar. El algoritmo es, a grandes rasgos como sigue:

  • Iremos repasando la expresión de izquierda a derecha, obteniendo cada uno de sus elementos... de tal manera que
    • si topamos con un número, lo metemos en la pila
    • si topamos con un operador, sacamos sus operandos de la pila, operamos y guardamos el resultado en la pila

Cuando terminemos, si todo ha ido bien, debe haber un único valor en la pila: es el resultado de resolver la expresión.

Los errores
Sin embargo, podemos encontrarnos con algún inconveniente. Vamos a enumerarlos para tenerlos en cuenta

  • Alguno de los elementos de la expresión podría no ser ni un número ni ninguno de los operadores que hemos decidido utilizar. En ese caso, tenemos un error sintáctico... algo que nuestro programa no puede entender.
  • Podría ocurrir que si encontramos un operador (por ejemplo, de suma '+'), a la hora de sacar sus dos operandos de la pila (porque la suma tiene aridad 2, es una operación binaria) no tengamos suficientes operandos en la pila. Eso denotaría que faltan operandos. Por ejemplo, la expresión 2 + 
  • Finalmente, terminada la evaluación de toda la expresión, debemos asegurarnos de que queda un único valor en la pila. Si hay más de uno, significa que sobran operandos.

[div class="alert" class2="typo-icon"]Operaciones no conmutativas

Ojo: algunas operaciones no son conmutativas... es decir, alterar el orden de los operandos altera el resultado. Por ejemplo, la suma es conmutativa 2 3 + da como resultado lo mismo que 3 2 +. Sin embargo, la resta o la división no lo son.

En RPN se conserva el orden de los operandos. Eso significa que 10 5 / es 10 dividido entre 5, y no 5 dividido entre 10. En el algoritmo, si intentamos evaluar esta última expresión, al encontrar el 10 se mete en la pila. Al encontrar el 5 también. Al encontrar el operador / sacaremos un operando de la pila (y saldrá el 5, porque la pila es una estructura LIFO -el último que entró es el primero que sale-) luego sacaremos el otro operando de la pila (y saldrá el 10). Debemos dividir el segundo que salió (el 10, que es el primero que entró) entre el primero que salió (el 5, que en realidad es segundo que entró)
La pila "vuelve del revés el orden" de los operandos;-)[/div]

Manos a la obra (en C#)

Vamos a implementar toda la funcionad en una sola clase, que llamaremos RPN.

La implementación que vamos a utilizar no es la mejor del mundo, seguro... Pero vamos a intentar primar la claridad del código sobre obre otros criterios.

Debemos tomar algunas decisiones de diseño. Evaluar una expresión, no siempre conduce a un resultado. Podríamos encontrar errores en la evalución, y no obtener un resultado válido.

Es decir, para realizar el trabajo de evaluar una expresión RPN, tenemos:

  • Datos de entrada:
    • Una expresión, que podemos aceptar con una cadena (string)
  • Datos de salida
    • Un posible resultado numérico o...
    • Una indicación de error

Podíamos haber optado por utilizar excepciones para indicar las situaciones de error, pero en este caso, hemos optado por no hacerlo (no hay una buena razón para ello... probablemente es algo personal, siento un cierto hastío cada vez que me toca lidiar con excepciones, así que prácticamente las voy dejando para errores realmente excepcionales).

Aprovechando que C# tiene parámetros sólo de salida hemos decidido utilizarlos.

En nuestra clase, en la parte pública vamos a encontrar tres cosas:

  • Un método público, que será el punto de entrada para evaluar una expresión, con ésta signatura: public TipoError Eval(String expresion, out decimal resultado), que acepta una expresión RPN como primer parámetro, y una variable de tipo decimal como segundo parámetro que conendrá el valor del resultado.
    Como retorno, se devuelve un valor del enumerado TipoError
  • El enumerado TipoError tiene cuatro posibles valores: noError, faltaOperando, demasiadosOperandos, sintactico, que corresponden a los tres posibles errores que podemos encontrar, y otro valor más para indicar la ausencia de error. Es decir, después de invocar al método Eval, éste devolverá un valor del enumerado. El resultado será válido sólo si el retorno es noError.
  • Es posible que nos interese saber, en el caso de que se produzca un error en la expresión, por dónde anda éste... así que hemos incluido una propiedad pública de sólo lectura public string SimboloError que nos dará el elemento que se estaba analizando cuando se encontró el error.

En la parte privada tenemos

  • Una pila. Imprescindible (System.Collections.Generics.Stack), como variable de instancia. 
  • Una variable de instancia de tipo TipoError llamada globalError, que contiene el último tipo de error encontrado. En cada evaluación se inicializa a noError, y se cambia su valor al encontrar errores.
  • Una variable de instancia de tipo string  llamada globalErrorSymbol, que contendrá el elemento que generó el último error. Es la variable que contiene el valor de la propiedad SimboloError
  • Un método private bool Operador(string tok). Lo llama Eval cuando encuentra un elemento que no es un número (se supone que será un operador). Si en efecto lo es, éste método opera (sacando sus operandos de la pila) y mete en la pila el resultado -y en ese caso, termina devolviendo true-. Si no se puede operar, se devuelve false. Éste método lo hemos programado para que sea fácilmente extensible, y que tú puedas añadir tus propios operadores de manera sencilla.

[div class="note" class2="typo-icon"]Estamos utilizando una pila con elementos de tipo decimal, en lugar de double. Es más adecuado para representar valores decimales, debido a su precisión fija. Además, double produce una irritante pérdida de precisión en SilverLight. Eso condiciona que todas las operaciones son con decimal. La clase Math trabaja con valores double, lo que nos obliga a realizar typecast.[/div]

El método principal, Eval tiene un comportamiento sencillo. En primer lugar, parte la expresión de entrada en distintos elementos (dado que están separados por espacios) utilizando el método split de la clase string. De esta manera, obtenemos un array de cadenas, con cada uno de los elementos en una posición.

Se inicia un bucle while que va recorriendo ese array.

  • Si se encuentra un número (lo detectamos con el método TryParse de la clase decimal), lo mete en la pila
  • Si se encuenta algo que no es un número, se supone que será un operador, así que se lo pasa al método Operar.
  • El método Operar devolverá false si no se pudo operar, bien porque lo que se le pasó no es un operador (en cuyo caso tenemos un error sintáctico), o bien porque sí era un operador, pero no había suficientes parámetros en la pila, (en cuyo caso tenemos un error de falta de operandos). Si es así, el bucle while se detiene.

[div class="note" class2="typo-icon"]El método TryParse intenta obtener un valor decimal a partir de una cadena. Distintos paises tienen distintas formas de escribir los decimales. Por ejemplo, en España se suele utilizar la coma para separar los decimales (ej: 3,14), es una costumbre un tanto minoritaria, mientras que en otros paises se utiliza el punto (ej: 3.14). Nosotro hemos optado por forzar ésta última notación. C# utiliza el concepto de cultura con estas conversiones. Utilizamos la cultura neutra, es decir, la que usa el punto como separador de decimales por estar más extendida, al menos, en el ámbito de las calculadoras. [/div]

Una vez terminado el bucle, si no ha sido por errores, queda comprobar una última cosa: en la pila debe haber exáctamente un único valor. Si no es así, se han introducido demasiados operandos (o pocos operadores), lo cual, constituye también un error.

Eso es básicamente todo. Aquí tienes la clase RPN. En éste código sólo hemos puesto unos pocos operandos en el método Operador, para que tú puedas experimentar añadiendo los que consideres oportunos.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
 * Clase en C# para evaluar expresiones aritméticas en notación polaca inversa.
 * 
 * Puedes usar, modificar y distribuir libremente éste código, teniendo en cuenta
 * que se distribuye "tal cual", sin garantía ni responsabilidad de ningún tipo.
 * Si te resulta util, se agradece un link a "http://latecladeescape.com".
 * 
 * Éste código se acoge los términos de la licencia descrita a continuación en inglés
 * y su utilización de cualquier tipo supone la aceptación de dichos términos.
 * 
 * **************************************************************************
 * Copyright © 2011 by La tecla de ESCAPE <latecladeescape.com>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 *  documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ****************************************************************************
 * @version 1.0
 * 
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleRPN
{
 
    /// <summary>
    /// Clase para evaluar expresiones en notación polaca inversa (RPN)
    /// </summary>
    class RPN
    {
        /// <summary>
        /// Enumeración que representa a los tres tipos de error y a la ausencia de errores
        /// </summary>
        public enum TipoError
        {
            noError, faltaOperando, demasiadosOperandos, sintactico
        }
 
        //Contiene el último error producido en una evaluación
        private TipoError globalError;
 
        //Contiene el último token que se evaluaba cuando se detectó un error
        private string globalErrorSymbol;
        
        /// <summary>
        /// Propiedad para poder consultar el token en el que se produjo el
        /// último error desde el exterior de la clase. Sólo lectura
        /// </summary>
        public string SimboloError
        {
            get
            {
                return globalErrorSymbol;
            }
        }
 
        //la pila. Utilizamos el tipo decimal porque es de precisión decimal fija,
        //y porque se comporta mejor que double en SilverLight
        private Stack<decimal> pila = new Stack<decimal>();
 
        /// <summary>
        /// Intenta realizar la operación que se le pase como parámetro, utilizando
        /// como operandos los valores numéricos de la pila.
        /// Modifica la pila y la varible globalError si se encontró un error.
        /// </summary>
        /// <param name="tok">El operador</param>
        /// <returns>true si se pudo operar con éxito. false si se produjo un error, y en ese caso,
        /// se indica en la variable globalError</returns>
        private bool Operador(string tok)
        {
            bool retorno=true;
            decimal resultado=1;
            decimal dAux; //auxiliar
            try 
            {
                switch(tok.ToLower())
                {
                    case "+":   resultado=pila.Pop()+pila.Pop(); //suma. binario
                                break;
                    case "-":   resultado=-pila.Pop()+pila.Pop(); //resta. binario
                                break;
                    case "*":   resultado=pila.Pop()*pila.Pop(); //multiplicación, binario
                                break;
                    case "/":   //división. binario. Utilizamos una variable auxiliar para
                                //sacar los operandos en orden de la pila
                                dAux=pila.Pop();
                                resultado=pila.Pop()/dAux;
                                break;
                    case "mod":  //resto de la división entera. binario
                                dAux=pila.Pop();
                                resultado=pila.Pop()%dAux;
                                break;
                    case "^":   //potencia. binario.
                                dAux = pila.Pop();
                                resultado = (decimal)Math.Pow((double)dAux, (double)pila.Pop());
                                break;
                    case "pi":  // 0-ario: simplemente, introduce un valor en la pila
                                resultado=(decimal)Math.PI;
                                break;
                    case "sqrt": //raíz cuadrada. unario: utiliza un único operando
                                resultado = (decimal)Math.Sqrt((double)pila.Pop());
                                break;
                    case "sin": //seno. unario: utiliza un único operando
                                resultado = (decimal)Math.Sin((double)pila.Pop());
                                break;
                    case "cos": //coseno. unario: utiliza un único operando
                                resultado = (decimal)Math.Cos((double)pila.Pop());
                                break;
                    //****************************************************
                    //*** Añade los operadores que consideres oportuno ***
                    //****************************************************                    default:
                                retorno=false;
                                break;
                }//switch
            } //try
            catch (InvalidOperationException )
            {
                globalError=TipoError.faltaOperando;
                retorno=false;
            }
            if (retorno) //Si se ha podido realizar la operación
            {
                pila.Push(resultado); //dejamos el resultado en la pila
            }
            return retorno;
        }
 
 
        /// <summary>
        /// Método para evaluar una expresión RPN
        /// </summary>
        /// <param name="expresion">Una cadena que contiene la expresión RPN</param>
        /// <param name="resultado">El resultado de la evaluación de la expresión.
        /// Sólo es válido si el retorno del metodo es TipoError.noError</param>
        /// <returns>Devuelve un valor del enumerado TipoError</returns>
        public TipoError Eval(string expresion, out decimal resultado)
        {
            //partimos la cadena por los expacios
            string[] tokens=expresion.Split(new char[]{' '},StringSplitOptions.RemoveEmptyEntries);
            pila.Clear(); //limpiar la pila
            int indice=0; //un índice para movernos por los elementos
            globalError = TipoError.noError; //Antes de evaluar no hay error
            globalErrorSymbol = null; //ni símbolo en el que se produce el error
 
            //recorremos los elementos, y paramos si se produce un error
            while(indice<tokens.Length && globalError==TipoError.noError)
            {
                decimal valor;
                //intentamos ver si el elemento es un valor decimal.
                //TryParse intenta obtener un valor decimal de una cadena. Si delvuelve
                //false es que no es un valor decimal.
                //Utilizamos una cultura neutra, para aceptar números con un "." como
                //separador decimal.
                if (decimal.TryParse(tokens[indice],
                       System.Globalization.NumberStyles.Number,
                       new System.Globalization.CultureInfo(""),
                       out valor))
                {
                    pila.Push(valor); //si es un valor decimal, va a la pila.
                }
                else //si no
                {
                    //supondremos que es un operador e intentamos evaluarlo.
                    bool esUnOperador=Operador(tokens[indice]);
                    if (!esUnOperador) //Si no se ha podido evaluar
                    {
                        if (globalError == TipoError.noError)
                        {
                            //entonces es un elemento no reconocido
                            globalError = TipoError.sintactico;
                        }
                    }
 
                }
 
                //finalmente, si no se han producido errores en la evaluación de 
                //éste elemento, pasamos al siguiente, haciendo avanzar el bucle while
                if (globalError == TipoError.noError)
                {
                    indice++;
                }
                else
                {
                    //si se han producido errores, tomamos nota del elemento en el que
                    //se ha producido el error
                    globalErrorSymbol = tokens[indice];
                }
            } //fin while
 
 
            //Una vez terminado el recorrido por los elementos de la expresión
            //queda comprobar que queda un solo valor en la pila.
            if (globalError==TipoError.noError && pila.Count != 1)
            {
                globalError = TipoError.demasiadosOperandos;
                globalErrorSymbol = "final de la expresión";
 
            }
            //el valor de la pila es el resultado de la evaluación
            resultado = pila.Pop();
            return globalError;
        }
 
    }
}

Minidemo

Puedes probar el código en acción con esta minidemo. Acepta los operadores aritméticos +, -, *, / y sqr para la raiz cuadrada (unario), mod para el resto de la división entera (binario), pi para el valor aproximado de pi (0-ario), las operaciones trigonométricas unarias sin y cos, y la potencia ^.

 



 

Curiosidades: el lenguaje de programación Forth

Si la notación polaca inversa te ha llamado la atención o ha despertado tu curiosidad, te recomiendo, si todavía no lo conoces, echar un vistazo al lenguaje de programación Forth. Se trata de un lenguaje fascinante, en el que toda su expresión se lleva a cabo sobre una pila. La notación polaca inversa desempeña un papel crucial en este lenguaje. Forth es prácticamente único en su especio y no se parece a nada.

Fue creado en los años 70 por Charles H. Moore y Elizabeth Rather, en principio para guiar a los grandes radiotelescopios, con un consumo mínimo de recursos. Aunque no es un lenguaje de uso mayoritario, 40 años después sigue causando cierta fascinación, y puede considerarse prácticamente un lenguaje "de culto".

Más:

 

   

Síguenos  

   

¿Dónde estoy?  

Estás en La tecla de ESCAPE, un sitio web personal en el que nos gusta hablar de algoritmos, metodología de la programación, personajes de informática, tecnología, ingeniería del software, internet, y cualquier otra tontería que se nos ocurra.

[Leer más / Términos de uso (ToS)]

   

¿Quién está en línea?