3º. 2º cuatrimestre. Itinerario de Computación. Grado en Ingeniería Informática. ULL
A estas alturas la máquina Egg puede manejar promesas por cuanto que es posible en Egg llamar a los métodos de los objetos JavaScript y las promesas no son otra cosa que Objetos JS.
Supongamos que extendemos Egg con un objeto fetch
que implementa la API fetch de JS:
topEnv['fetch'] = require('node-fetch');
Inmediatamente podemos escribir programas Egg como este:
[~/.../egg/crguezl-egg(private2019)]$ cat examples/fetch.egg
do{
fetch("https://api.github.com/users/github")
.then(->{res, res.json()})
.then(->{json,
print(json)
})
.catch(->{err,
print(err.message)
})
}
Al ejecutarlo obtenemos:
[~/.../egg/crguezl-egg(private2019)]$ bin/egg.js examples/fetch.egg
{
login: 'github',
id: 9919,
node_id: 'MDEyOk9yZ2FuaXphdGlvbjk5MTk=',
...
created_at: '2008-05-11T04:37:31Z',
updated_at: '2020-02-07T13:08:07Z'
}
Veamos un ejemplo de asíncronía en Egg con callbacks. Extendamos Egg con un objeto que provee acceso al sistema de archivos:
topEnv['fs'] = require('fs');
Me he encontrado con algunos problemas cuando probé a escribir este programa:
[~/.../egg/crguezl-egg(private2019)]$ cat examples/fs.egg
do {
fs.readFile("examples/no-existe.egg", "utf8",
fun{err, data,
if[==(err, null), print(data), print(err)]
}),
fs.readFile("examples/fs.egg", "utf8",
fun{err, data,
if[==(err, null), print(data), print(err)]
})
}
El problema es que JS llama a la callback
con un solo argumento err
cuando se produce un error y con dos
(err, data)
cuando la operación tiene éxito.
Esta conducta de JS da lugar a que la versión actual de la máquina virtual Egg proteste por cuanto espera que el número de argumentos coincida con el número de parámetros declarados. Desafortunadamente, cuando hay error JS llama a la Egg-callback con un número de argumentos diferente de aquel con el que fue declarada.
La cosa tiene varias soluciones, pero en este momento he optado por la mas rápida que ha sido que Egg no proteste ante llamadas con número de argumentos menor que los que le fueron declarados.
Otro asunto en este ejemplo es que Egg carece del objeto null
de JS y
la convención es que JS llama a la callback con cb(null, data)
para indicar la ausencia de error. De nuevo hay númerosas formas de abordar este asunto, pero una sencilla es advertir a la máquina virtual Egg de la existencia de null
para que no proteste:
topEnv['null'] = null;
topEnv['true'] = true;
...
Sigue un ejemplo de ejecución:
[~/.../egg/crguezl-egg(private2019)]$ bin/egg.js examples/fs.egg
[Error: ENOENT: no such file or directory, open 'examples/no-existe.egg'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'examples/no-existe.egg'
}
do {
fs.readFile("examples/no-existe.egg", "utf8",
fun{err, data,
if[==(err, null), print(data), print(err)]
}),
fs.readFile("examples/fs.egg", "utf8",
fun{err, data,
if[==(err, null), print(data), print(err)]
})
}
La idea es introducir una función use
que es parecida a require
pero con la diferencia de que extiende el lenguaje Egg-aluXX
mediante una librería escrita en JavaScript.
Esto es, alguien del mundo mundial, un programador llamado Y entusiasmado por tu lenguaje Egg-aluXX
extiende el lenguaje egg-aluXX
con una librería llamada egg-aluXX-tutu
que publica en npm.
Y lo ha hecho añadiendo en specialForms
y topEnv
nuevas funcionalidades. Puede hacerlo porque importa tu módulo en el que tu exportas los hashes specialForms
y topEnv
.
Una sentencia como use('tutu')
debe hacer que el intérprete egg
haga un require
de egg-aluXX-tutu
(que se supone ha sido previamente instalada en node_modules/
) y que las funcionalidades exportadas por egg-aluXX-tutu
estén disponibles al programa Egg.
Como posibles ejemplos de uso, véanse las siguientes secciones
La idea general es extender el lenguaje Egg con funcionalidades para la manipulación de GitHub
do {
use('github'),
Org("ULL-ESIT-PL-1920").then(
->(org, # Object describing the org
do {
People(org).then[
->(people, # Result is an array of objects with the people in the org
print(people)
)
]
}
}
Para implementar la extensión github
podríamos hacer uso de alguna librería asíncrona como octokit/rest.js, github-api, octonode o similar.
Todas las librerías de JavaScript para comunicaciones suelen ser asíncronas y esto casa mal con la naturaleza de Egg, hasta ahora bastante síncrona.
Una excepción es sync-request
:
Usando sync-request podemos diseñar una sintáxis mas simple:
do{
use("../lib/github"), # Carga el módulo para trabajar con la Api de GitHub
# setToken(".eggtoken"), # Token Obtenido en la web de GitHub https://github.com/settings/tokens
def(me, whoami()),
print("Teacher: ",me.name),
print("Teacher's blog:",me.blog),
:=(pl, org("ULL-ESIT-PL-1920")),
# print(pl),
print("Total number of repos in ULL-ESIT-PL-1920: ",pl.total_private_repos),
print("Number of collaborators in ULL-ESIT-PL-1920: ",pl.collaborators),
:=(membersPL, members(pl)),
print("Total members in PL: ",membersPL.length),
:=(collaboratorsPL, collaborators(pl)),
print("Total collaborators in PL: ",collaboratorsPL.length),
:=(inside,
membersPL.map{->(cv, i, a,
array[cv.login, cv.url]
) # end function
} # end map
),
print("First and last Members: ", inside[0], element(inside,-1)),
def(lastCol, element(collaboratorsPL, -1)),
print("Last collaborator: ", lastCol.login, lastCol.url)
Cuando se ejecuta obtenemos:
Teacher: Casiano Rodriguez-Leon
Teacher's blog: https://crguezl.github.io/quotes-and-thoughts/
Total number of repos in ULL-ESIT-PL-1920: 829
Number of collaborators in ULL-ESIT-PL-1920: 54
Total members in PL: 25
Total collaborators in PL: 29
First and last Members: [ 'Alien-97', 'https://api.github.com/users/Alien-97' ] [ 'victoriamr210', 'https://api.github.com/users/victoriamr210' ]
Last collaborator: sermg111 https://api.github.com/users/sermg111
que nos informa que el Sábado 16/05/2020 tenemos 54 personas y 820 repos en la organización.
Por supuesto es necesario configurar la extensión con un token. En esta solución hemos optado por poner el token en un fichero de configuración para Egg:
[~/.../PLgrado/eloquentjsegg(async)]$ tree ~/.egg/
/Users/casiano/.egg/
└── config.json
0 directories, 1 file
[~/.../PLgrado/eloquentjsegg(async)]$ cat ~/.egg/config.json
{
"github" : {
"token": "badbadbadbadbadbadbad..."
}
}
Una manera de simplificar todo el manejo de la asincronía en Egg
es modificar la forma en la que todo se evalúa: Cambiar todos los métodos
evaluate
para que sean funciones async
y se haga un await
en todas
las llamadas a las evaluaciones.
Si consigue hacer esta variante, los programas asíncronos se ven altamente simplificados.
Vea como reescribimos nuestro anterior ejemplo de fetch
:
[~/.../src/egg(async)]$ pwd -P
/Users/casiano/local/src/javascript/PLgrado/eloquentjsegg
[~/.../PLgrado/eloquentjsegg(async)]$ cat examples/fetch.egg
do{
:=(res, fetch("https://api.github.com/users/github")),
:=(json, res.json()),
print(json)
}
Veamos el resultado de una ejecución:
[~/.../PLgrado/eloquentjsegg(async)]$ bin/egg.js examples/fetch.egg
{
login: 'github',
id: 9919,
node_id: 'MDEyOk9yZ2FuaXphdGlvbjk5MTk=',
avatar_url: 'https://avatars1.githubusercontent.com/u/9919?v=4',
gravatar_id: '',
url: 'https://api.github.com/users/github',
...
created_at: '2008-05-11T04:37:31Z',
updated_at: '2020-02-07T13:08:07Z'
}
Esta extensión es un reto difícil. Con esta versión el diseño de DSLs que extiendan Egg mediante llamadas a librerías asíncronas (como es el caso de accedera a las APIs de GitHub, YouTube, Google Maps, etc.) quedan simplificadas.
Podría ser mediante un método child
como este:
do(
def(x, object (
"c", 0,
"gc", ->{element[this, "c"]},
"sc", ->{value, =(this, "c", value)},
"inc", ->{=(this, "c", +(element[this, "c"],1))}
)),
def(y, child(x)),
print(y.sc(5)),
print(y.c)
)
La declaración def(y, child(x))
hace que el objeto y
herede las propiedades y métodos del objeto x
Podría tanto en el lenguaje de infijo como en Egg considerar la posibilidad de introducir clases. Sigue un posible ejemplo:
class Math
begin
constructor(x, y)
begin
this.x = x;
this.y = y;
end;
method sum();
begin
this.x + this.y;
end;
end
begin /* main */
let a = new Math(2,3);
print(a.sum()); // 5
end;
Esta extensión consiste en añadir la posibilidad de que los últimos parámetros de una función tengan valores por defecto y puedan ser omitidos en la llamada:
do {
def(f, fun(x, default(y, 3)), default(z, 2),
do {
print(x+y+z)
}
),
f(3), # 8
f(3, 5), # 10
f(3, 1, 9) # 13
}
Puede resultarte útil leer este tutorial JavaScript Default Parameters si decides abordar esta extensión.
Se trata de añadir a Egg un operador spread
que funcione como el de JS
permitiendo que un spread(array)
sea expandido en llamadas a funciones donde se esperan múltiples elementos y al revés: que los múltiples argumentos de una función sean colocados en un array dentro del cuerpo de la función.
Sigue un ejemplo:
do {
def(f1, fun(x, y, # f1 espera dos argumentos
do {
+(x,y)
}
)),
def(z, array(1,4)),
print(f1(spread(z))), # Lo llamamos con un array. Resultado: 5
def(g, fun(a, spread(x), # g espera uno o mas argumentos
do {
+(x[0], x[1])
}
)),
print(g(1, 4, 5)) # a es 1 y x es [4, 5]. Resultado: 9
}
Las posibilidades son infinitas, tanto para Egg como para el lenguaje de Infijo. Puede añadir funcionalidades que faciliten la escritura en determinados dominios: algoritmos evolutivos, redes neuronales, estadística, etc.
Un ejemplo simple es extender el lenguaje Egg con funcionalidades para el cálculo vectorial
do {
use('science'),
:=(v1, arr(4, 5, 9)),
:=(v2, arr(3, 2, 7)),
:=(s, *(+(v1, v2),v2)),
print(s)
}
La idea general es extender el lenguaje Egg con funcionalidades para la descripción de tareas. Este código sería el contenido de un fichero eggfile.egg
:
tasks {
use('tasks'),
task(compile: sh("gcc hello.c"), depends: "mylib"),
task(mylib: sh("gcc -c -o mylib.o mylib.c")),
task(default: "compile")
}
La idea general es extender el lenguaje Egg con funcionalidades para procesar los argumentos dados en línea de comandos (similar a lo que es commander para Node.js):
Por ejemplo para una ejecución como esta:
$ example.egg -vt 1000 one.js two.js
Tendríamos que escribir example-egg
siguiendo un patrón como este:
do {
use('command-line'),
:=(optionDefinition, arr [
map { name: 'verbose', alias: 'v', type: Boolean },
map { name: 'src', type: String, multiple: true, defaultOption: true },
map { name: 'timeout', alias: 't', type: Number },
map { name: 'help', alias: 'h', type: Boolean },
]),
:=(options, parseArgs(optionDefinitions)),
print(options)
/* options es un map como este:
{
src: [
'one.js',
'two.js'
],
verbose: true,
timeout: 1000
}
*/
}
Se trata de añadir al compilador de Egg una fase de optimización que haga plegado de constantes.
Por ejemplo, cuando se le da como entrada un programa como este:
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ cat examples/optimize.egg
do {
:=(x, +(*(2, 3), -(5, 1))) # 2 * 3 + (5 - 1) == 10
}
Si se compila con la opción --optimize
de lugar a un plegado de constantes (o en inglés constant folding)
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ bin/egg.js --optimize -c examples/optimize.egg
El código resultante produce un programa equivalente a := (x, 10)
:
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ cat examples/optimize.egg.evm
{
"type": "apply",
"operator": {
"type": "word",
"name": "do"
},
"args": [
{
"type": "apply",
"operator": {
"type": "word",
"name": ":="
},
"args": [
{
"type": "word",
"name": "x"
},
{
"type": "value",
"value": 10
}
]
}
]
}
Aunque el lenguaje Egg dispone de ámbitos, los errores de ámbito (variables no declaradas) solo se detectan en tiempo de ejecución:
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ cat examples/set-error-compile.egg
set(x, 4)
Si lo ejecutamos nos da un run-time error:
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ bin/egg.js examples/set-error-compile.egg
ReferenceError: Tried setting an undefined variable: x
De lo que se trata aquí es de detectar los errores lo mas temprano posible, antes de que se ejecute el programa recorriendo el AST y buscando los nodos de usos de words que no han sido definidos en un ámbito superior:
[.../TFA-04-16-2020-03-22-00/davafons(casiano)]$ bin/egg.js -c examples/set-error-compile.egg
ReferenceError: Trying to use the undefined symbol x
En esta variante de Egg la opción -c
usada compila el programa pero no lo ejecuta.
En esta fase de análisis de ámbito también se pueden comprobar algunos otros tipos de errores de uso. Por ejemplo si extendieramos Egg con declaraciones const
para constantes,
podemos recorrer el AST comprobando que no se hace ningún intento de modificación (set
) de esa variable en su ámbito de declaración.
Proveer Syntax Highlight en Visual Code para Egg. Véase Syntax Highlight Guide
Escribir un traductor (no un intérprete) desde Egg a JavaScript.
A continuación un ejemplo borrador de como podría ser el esquema de traducción:
Supongamos la entrada:
do(
def(x, *(4,2)),
print(+(x,1))
)
Daría una salida como esta
let {sf, te} = require('egg-run-time');
// te es el "topenv" previamente poblado con constantes y variables como "true", "false", etc.
let ce = Object.create(te); // current environment
sf["do"](
sf["def"]("x", sf["*"](4, 2, ce), ce),
sf["print"](sf["+"]("x", 1, ce), ce),
ce
)
do(
def(f, fun(x, +(x,1))),
f(4)
)
let {sf, te} = require('egg-run-time');
let ce = Object.create(te); // current environment
sf["do"](
sf["def"]("f",
(x, e) => {
let le = Object.create(e);
le["x"] = x;
return sf["+"]("x", 1, le)
}, ce)
, ce),
sf["f"](4, ce)
, ce)