@iamemhn

EM Hernández-Novich

Ask @iamemhn

Sort by:

LatestTop

Previous

Tengo una aplicación que necesita un log visible (y no editable) por los usuarios, básicamente mostrando cada insert/update/delete a las diferentes colecciones de cosas (tablas reales o views), con hora y usuario responsable. Debería manejar eso en otra tabla de postgres? o loggear externamente?

Esta es una aplicación perfecta para el modelo objeto-relacional que ofrece PostgreSQL:
1. Creas una tabla «genérica» de auditoría que tenga al menos cuatro columnas: createdOn, createdBy, updatedOn, updatedBy. Si en tu aplicación tiene sentido: deletedOn, deletedBy. Las columnas 'On' son TIMESTAMP y las columnas 'By' son TEXT para almacenar nombres de usuario. Si estás empleando usuarios de PostgreSQL para autenticar, ya eres un ganador porque puedes usar DEFAULT CURRENT_USER.
2. Escribe un TRIGGER que dependiendo de la operación CRUD actualize las columnas antes mencionadas.
3. Para cualquier tabla que requiera auditoría haces CREATE TABLE foo ( ... ) INHERIT ( tabla_de_auditoría )
4. Para impedir que los usuarios vean el log, puedes usar los permisos granulares por columna. Eso va a requerir que tengas vistas y funciones SETOF que entreguen sólo las partes accesibles a los usuarios regulares.
Puedes aplicar el mismo concepto para tener la tabla particionada según el año y de ser necesario «limpiar» registros viejos que no tengan sentido. Puedes «engañar» al usuario diciendo que hiciste el DELETE y registrándolo en la tabla de auditoría, pero no borrar la(s) fila(s) usando una regla de reescritura de comandos.
Una alternativa es tener una tabla separada para auditoría en la cual registras la tabla cambiada. Aquí no puedes aprovechar la herencia de tablas, y vas a tener una proliferación grosera de triggers para cada tabla, a cambio de que el control de acceso a esa tabla es relativamente más fácil. Prefiero la primera solución.

View more

+1 answer Read more

¿Cuál es la mejor implementación de Prolog para usar pedagógicamente y en la industria?

SWI-Prolog (licencia BSD) es la implementación más completa para usar en la industria. En particular si pretendes interactuar con otras aplicaciones vía sockets o si te interesa tener un API REST para hacer las consultas. Si bien Prolog nunca ha tenido (y no creo que llegue a tener) un sistema de módulos razonable, la forma en que SWI Prolog separa las librerías lo hace bastante práctico.
Pedagógicamente prefiero usar GNU Prolog (licencial GPL) porque obliga a construir algunos predicados interesantes, que en SWI-Prolog vienen incluidos.
La mayoría de implantaciones Prolog comerciales se enfocan en la «experiencia visual» para el desarrollador, siendo insuficientes en buen soporte a ISO Prolog y el dialecto Edinburgh, que al final es lo que un conocedor de Prolog quiere.
Liked by: David Prieto

¿Qué tanto debe enfocarse un ingeniero de machine learning en entender la teoría? ¿En qué más se debe enfocar?

La base de cualquier ingeniería es comprender la teoría para aplicarla en la práctica, en particular de formas ingeniosas. Si sólo tienes práctica en algo que no sabes cómo funciona, te quedas en la técnica, que no es nada malo, pero te va a costar bastante más justificar algunas decisiones así como darle la vuelta a las dificultades.
Hay quienes tienen mucha práctica y les va muy bien, pero tienen que cederle el paso al que sabe cuál tuerca apretar en un momento dado, para estupefacción de los agobiados por el problema. Y «sabe» cuál y cuánto apretar, porque ve la máquina como la consecuencia inevitable de la teoría que domina. Más aún, al dominar la teoría tienes mejores argumentos para comparar y criticar las herramientas que «el práctico» está usando a falta de mejor criterio.
Así que dedicarle tiempo a la teoría es tan importante como practicarla. Además de comprender mejor lo que haces, te garantizo que tendrás más de un momento en que pensarás «cuánto tiempo desperdicié por no haber leído esto antes». Aparte, te abre la maravillosa oportunidad de educar a aquellos que quieren ir más allá de la práctica.
Con la práctica puedes resolver y probablemente puedes describir los pasos que seguiste. Con la teoría puedes explicar todo el proceso, refinarlo a paso seguro, y reducir incertidumbre. Respeto ambos, y personalmente aprecio más al segundo, sobre todo en tópicos en los cuales no tengo conocimiento teórico suficiente: se nota cuando una persona domina la teoría en comparación con otra que sólo tiene práctica.
«La práctica sin teoría es ciega y la teoría sin práctica es estéril» — Kant

View more

Related users

Hi professor. Any advantage of using the Clean functional language over Haskell? If I understand well, they don't use monads. Any comment? Thanks

Clean doesn't have fist-class Monads, much less Monad Transformers. That would be very limiting for a Haskell programmer and somewhat intrusive in the way to code composability, the latter being the means and purpose of the happy and successful programmer.
Clean's solution for isolating mutability is similar to Rust's in that there's one, and only one, way to access a mutable name at any given time (with a twist). Your program runs in a single, immutable IO flow, and your functions have either pure arguments or arguments that must be proven by the typesystem to be the single reference to a mutable value.
Once you grok monad transformers, not having them is never an option you'd choose without coercion (pun intended). So, it would be «hell to the no» for me.
You'll quickly realize that Clean's libraries are lacking, a lot, when compared to Haskell's. I don't suffer from that pitiful and annoying malady known as «not written here» oh so popular with kids (and comrades) nowadays, so I rather re-use than re-implement.
Finally, even though people call it «Concurrent Clean», there's actually no support for traditional threads, much less for Software Transactional Memory nor parallelism that have been a standard part of Haskell for almost a decade now.

View more

Qué recomienda a alguien que nunca ha programado, para aprender a usar R para análisis de datos?

El grueso del análisis de datos (y «machine learning») es hacer álgebra lineal. Si sabes hacer álgebra lineal con papel y lápiz, lo mismo que escribes se traduce a R *directo* sin tener que «reinterpretar» con algoritmos iterativos (como ocurriría en otros lenguajes). Entonces, siéntate a hacer tutoriales de R, y resolver problemas concretos, que la práctica te hará mejor.
Mi recomendación profesional es que aprendas y fomentes el uso de Julia [1], que es notablemente superior en cualquier arista [2] a Python o R.
Dominar muchos lenguajes de programación, incluso si son para el mismo nicho, te hace más competente, tanto en la manera de pensar,como en la manera de expresar soluciones.
[1] http:/julialang.org
[2] Excepto popularidad. Pero es muy diferente ser el mejor, que ser el más popular.

View more

Es mejor un sistema de archivos que soporte "hard links" o basta con los "soft links"?

Es conveniente tener ambos.
Los enlaces duros no agregan costo ni en espacio ni en tiempo para acceder a los contenidos de los archivos. Los enlaces suaves requieren espacio adicional para almacenar el nombre destino, y una indirección adicional para acceder al archivo final.
Por otro lado, si mueves un enlace duro de un directorio a otro, siempre estará apuntando al mismo archivo, así que no hay riesgo de «romper» la conexión. Eso si ocurre con los enlaces suaves: si mueves el archivo destino, el enlace suave «se rompe» porque está apuntando a un nombre que ya no está.
Los enlaces duros existen antes que los enlaces suaves (al menos en Unix, en Windows nunca los hubo). La idea es que haya varios nombres para un mismo archivo, cosa que tiene utilidad en algunos escenarios de seguridad y/o ahorrar espacio compartido. Pero los enlaces duros solamente funcionan dentro del mismo sistema de archivos.
Los enlaces suaves se inventaron (en Unix, Windows se los copió después) para poder tener enlaces que crucen la frontera de sistemas de archivos. No pueden garantizar la inmutabilidad del destino, sólo agregan flexibilidad para «redirigir» ubicaciones (fundamentalmente directorios).

View more

Liked by: Marcos Mora
+1 answer in: “Prefieres un sistema de archivos case sensitive o insensitive?”

Tengo una thinkpad que funcionaba con Debian 9, linux 4.18 y bumblebee (ambos necesarios). Recientemente actualicé a Debian 10, que por defecto usa linux 4.19. Todo bien excepto que el resume from suspend no funciona con 4.19. Qué debería hacer para repararlo o para ayudar a la comunidad a hacerlo?

Verifica que el BIOS/UEFI de la máquina esté actualizado hasta la última versión.
Después (y sólo después) prueba cambiar en el BIOS el modo de suspend, entre S1 y S3. Esa no es una solución sino un rodeo que puede darte evidencia interesante.
El asunto con suspender a RAM (S3) requiere quitarle potencia a todos los dispositivos, excepto la RAM y el CPU. Cuando despiertas la máquina, hay que despertar al resto de los dispositivos, y es esa la causa usual de problemas (WLAN, BT, USB, u otros dispositivos que requieren inicialización especial). Si cambiar de S3 a S1 hace diferencia, entonces es posible que tengas que modificar los scripts de suspend de tu máquina para que remueva/reinserte los controladores de los dispositivos en cuestión.

Cuando se debería usar Error o Exception?

Supongo que la pregunta es en el contexto de JavaScript, que es un lenguaje que no uso.
En cualquier lenguaje hay dos tipos de excepciones: síncronas y asíncronas.
Las excepciones síncronas son las que el programador competente adulto responsable está en deber de detectar y tener métodos para recuperarse (cliente no existe, no me pude conectar). En ese caso, el lenguaje debe tener al menos un mecanismo para representar la condición, dispararla, y capturarla.
En Haskell, puedo usar Maybe (resultado o no) o Either (resultado o detalle del error en forma de tipo). Esto me permite escribir código puro capaz de reaccionar ante errores síncronos. Si necesito código con efectos, puedo usar el Monad Except con tipo(s) de datos adicionales para aumentar la información de contexto.
Las excepciones asíncronas son las generadas por el ambiente de ejecución (división por cero, stack overflow, no hay espacio en disco, señales, detección de deadlock). Son complejas porque pueden llegar en cualquier momento cuando el código «no está preparado». El Monad Except incluye funcionalidad para atrapar estas excepciones también.
Haskell tiene una función `error` que es una excepción genérica que indica el error con un String... Obviamente, es una excepción de pobre. En cambio, usando `throw` puedo «lanzar» un valor de un tipo de datos que «explica» mejor la excepción, y si el código está dentro del Monad Except puedo atraparlo con `catch` y reaccionar apropiadamente.
Creo que en JavaScript no hay diferencia semántica entre Error y Exception, al menos con la riqueza expresiva que describí más arriba. Básicamente porque en JavaScript todas las excepciones son asíncronas dado el modelo de ejecución del lenguaje y porque su sistema de tipos es más bien un tipo de Sistemas. Creo que usar Error o Exception no es más que una convención de usanza que el lenguaje no tiene manera de condicionar: Error es para «lo hiciste mal» y Exception es para «todavía no podías hacer eso». Sé lo suficiente de JavaScript para no ahondar más.

View more

Profesor, no entiendo bien cuando menciona que string y json son el tipo del pobre. Quiere decir que si voy a guardar una dirección de email debe existir un tipo "email" que haga validaciones sobre el texto? Yo veria eso como un objeto con una propiedad y un método como validaEmail más que un tipo.

Manejar «todo» como un string y luego escribir código para convertirlo implícitamente o explícitamente con reconocedores/traductores, es MUCHO más trabajo y MUCHO más propenso a errores, que usar el sistema de tipos del lenguaje.
SI voy a guardar (almacenar en una base de datos) un email, defino en la base de datos un tipo EMAIL que se *basa* en TEXT pero que NO ES compatible con TEXT (así no puedo mezclar TEXT diferentes), le agrego una condición en la base de datos, para que tenga la forma de un EMAIL. De ese modo, si alguien quiere insertar un EMAIL, la base de datos se protege. Me tiene sin cuidado lo que hagan los programas *afuera*. Lo que *nunca* haría sería usar TEXT y confiar en que un programa afuera va a hacer todas las validaciones bien.
Lo mismo ocurre si estoy programando cosas que no «guardan», pero que pueden confundir. Por ejemplo, una aplicación científica que maneja centígrados y fahrenheit. Si tus funciones sólo usan Float, tienes que trabajar *mucho* para que no se «confundan» los tipos de grados
add :: Float -> Float -> Float
(que es una vulgar suma, por supuesto)
¿Cómo la escribes para que no haya confusión? ¿Cómo garantizas que no vas a restar celsius de fahrenheit, o viceversa? Si la respuesta es «abstraigo la temperatura» a clases y objetos, has bebido demasiado KOOL-aid. Yo hargo
newtype Celsius = C Float
newtype Fahrenheit = F Float
Ahora, Celsius no se mezcla con Fahrenheit, ni tampoco se mezclan con «vulgares» Float. La posibilidad de que cometa un error por estúpido o por audaz se reduce mucho. Ahora me veo obligado a tener
addC :: Celsius -> Celsius -> Celsius
addF :: Fahrenheit -> Fahrenheit -> Fahrenheit
pero como el sistema de tipos es superior, puedo hacer
instance Num Celsius where
(+) = addC
instance Num Fahrenheit where
(+) = addF
y ahora, el *COMPILADOR* me va a permitir escribir
t0 + t1
y *verifica* que t0 y t1 sean el mismo tipo de temperatura, y llama a la funcion correcta. Y no compila si «mezclé» temperaturas.
Si a alguien le parece mejor usar Strings, conversiones implícitas, abstracciones innecesarias para «darle la vuelta», y estar pendiende de excepciones a tiempo de ejecución para «ver dónde se peló», pobre.

View more

¿Por qué los User Agents tienen un formato tan confuso? ¿Por qué no es un simple string o incluso JSON que contenga la información necesaria?

El formato de los User Agent está definido desde el RFC-1945. Quizás te parece confuso porque no estés familiarizado con la manera de expresar gramáticas.
Una explicación simplista es que se trata de una lista de uno o más grupos separados por espacios en blanco. Cada grupo es una «pareja» de cadenas separadas por una barra (/). La primera parte es un token y la segunda es un número de versión. Esa forma es muy fácil de procesar con un reconocedor recursivo descendiente.
Los reconocedores recursivos descendientes son los más fáciles de escribir tales que sean eficientes en espacio y tiempo, y a la vez autocontenidos en el lenguaje anfitrión.
El mismo RFC indica que la lista debe ir ordenada de mayor a menor «importancia». Eso se interpreta como que primero va la del cliente concreto (digamos "w3m") y luego la de librerías adicionales (digamos "libcurl" y "libssl"). Esa especificación, hace que el reconocedor pueda tener atajos si se desea (reconocer sólo lo que hace falta -- digamos uno o dos tokens).
La razón por la cual el RFC define ese encabezado de esa forma es, precisamente, porque está escrito con conocimientos sobre ese tema.
Una cadena simple y «free form» sería demasiado bajo nivel (porque string es el tipo de datos del pobre) y haría más difícil la noción de «importante» que describí más arriba. Un objeto JSON sería demasiado alto nivel, pues tendrías que escribir un reconocedor de permutaciones (dado que JSON no impone orden a las claves, y si «tú» lo impones/convienes, lo estás haciendo mal). En cualquiera de estos dos casos, estás trabajando «de más» por usar la representación incorrecta.
Si comprender lenguajes es difícil, diseñar buenos lenguajes lo es aún más, porque hay que mirar problemas que casi nunca son evidentes a los lectores del lenguaje.

View more

Como haces cuando necesitas cruzar info de varias base de datos ya sean del mismo tipo o de diferentes e.g postgresql, mysql y ldap

Escribo un programa adaptado al escenario.
Para consultas «en línea», generalmente PostgreSQL es el centro de operaciones, y se puede conectar con otras bases relacionales. Si es con otras PostgreSQL en otros servidores distintos al destino, uso PostgreSQL DBLINK [1]. Si es con otras que no son PostgreSQL, entonces usando PostgreSQL Foreign Data Wrappers [1]. De ese modo, escribo consultas SQL que se transmiten y traducen, a los dialectos de otras bases de datos, con los resultados presentados en PostgreSQL.
Para procesos ETL, Perl DBI [3] porque tiene «backend» drivers para cualquier base de datos que te imagines (Pg, SQLite, MySQL, Oracle, DB2, Informix, SQL-Server, cualquiera que hable ODBC). Incluso existen «backends» que permiten usar SQL para consultar archivos Excel, CSV, y por supuesto directorios LDAP; hay limitaciones de desempeño porque estas tres últimos no fueron concebidas para usar SQL. Sin embargo el punto es que puedes extraer los datos de manera uniforme, que al final es lo que uno necesita en ETL.
Perl también tiene librerías para conectarse a bases de datos no-relacionales. Aquí hay que trabajar un poco más para el masajeo de los datos.
[1] https://www.postgresql.org/docs/9.6/dblink.html
[2] https://wiki.postgresql.org/wiki/Foreign_data_wrappers
[3] https://metacpan.org/pod/DBI

View more

Liked by: Marcos Mora

Qué opina de la decisión de Ubuntu de no sacar su distribución para 32bits?

Canonical es una empresa privada y puede decidir lo que le venga en gana según sus intereses.
Hay gente que se queja (frívolamente) porque ya no van a poder usar Wine (para emular Windows), Steam (para jugar), o aplicaciones privativas con versiones Linux para i386.
Hay gente que se queja (manipulando la razón) de que en países del tercer mundo hay muchas máquinas con 2Gb de RAM o menos, en las cuales una distribución de 64bits funciona mal porque consume más memoria, o directamente no funciona según el chipset. Eso es verdad, pero no es culpa de Canonical, ni es su responsabilidad subsidiar esa crisis.
Ahora bien, no es menos cierto que si tanto quieren Ubuntu, pueden seguir usando 18.04 que es LTS hasta 2023. No es menos cierto que si tanto quieren Software Libre, pueden seguir usando Debian 9 y 10, quizás 11 , o cualquier otra distribución que soporte 32-bits.
Pero si la verdad es que se están quejando porque perdieron la comodidad de Ubuntu, les recuerdo que uno «paga» por comodidad de una u otra forma. Paguenle a Canonical o inviertan su tiempo en hacer otra distribución cómoda según sus preferencias.
Dejen el llanto, que están usando Software Libre, y si necesitan adaptarlo, son libres de hacerlo o pagarle a alguien que lo sepa hacer. Si el proveedor de Software Libre no ofrece lo que necesitas, cambia de proveedor. Punto.

View more

Liked by: Marcos Mora

Usas Org mode en emacs? Si no, que tool usas como to-do tool o trackear Las cosas que tienes pendiente?

No uso EMACS.
Herramientas convencionales, como papel y lápiz, para algunas cosas, de trabajo y personales. Experimenté con herramientas digitales, y no me funcionaron.
Herramientas centralizadas controladas por mi, como Request Tracker, para cosas de trabajo en la cual es necesario registrar la historia, e interactuar con terceros.

¿Por qué algunos lenguajes que permiten recursión no tienen optimización de recursión de cola?

La explicación más simple es flojera de los implementadores, y usualmente es así.
Mejorar recursión de cola propia (la función se llama a sí misma) es un ejercicio *trivial*, en cualquier tipo de lenguaje. Si no lo hacen, es porque no quieren.
Mejorar recursión de cola mutua (la función A llama a B, que llama a C, que llama a A, todas las llamadas son de cola) es más complicado, sobre todo en lenguajes imperativos.
En lenguajes que tienen recursión y usan la pila por hardware para almacenar argumentos y locales, es posible que el registro de activación del llamado sea más complejo que aquel del llamador, entonces es necesario incluir limpieza adicional. En estos casos, mejorar la recursión de cola de la misma función sigue siendo trivial, pero proveer un mecanismo generalizado para mejorar recursión de cola entre funciones diferentes, es suficientemente complicado como para no hacerlo. Tanto GCC como CLANG (LLVM en realidad) pueden hacer mejoramiento de recursión de cola propia, y hacen la mutuamente recursiva cuando pueden demostrar que no hay que hacer cambios de *tamaño* en el registro de activación.
Lenguajes que no usan la pila por hardware, pero dependen de una pila simulada, como la JVM, también entran en esta categoría. Scala es capaz de hacer la (trivial) eliminación para recursión de cola propia, pero no lo hace para rercursión de cola mutua; sin embargo Java (hasta 8) no hace ninguna optimización.
El mejoramiento de recursión de cola es obligatorio en la especificación de Scheme, así que toda su descendencia lo incluye. En otros lenguajes el compilador/intérprete no lo hace, a menos que el programador lo indique con una palabra reservada; en este caso «se hace lo que el programador indica».
Haskell no usa la pila del sistema para la secuencia de llamada, y su mecanismo para invocar funciones es diferente al de los lenguajes imperativos. Entre las ventajas que trae esa diferencia, es que es más fácil hacer el mejoramiento de cualquier tipo de recursión de cola.
En todo caso, el programador tiene que saber expresar el problema con recursión de cola.
Finalmente, está la postura lamentable de diseñadores de lenguajes que dicen que es mejor no incorporar mejoramiento de recursión de cola, porque entonces es más difícil hacer debugging y el programador se confunde. I pity the fool.

View more

Liked by: Marcos Mora

One thing that differentiates you from other people??

I'm exponential. Nothing differentiates me, and it's hard for me to integrate.

Como haces para convivir con Systemd?

Leer el manual. Escribir los units. Usar lo mínimo indispensable. Han avanzado mucho, pero igual están muy lejos, y son sumamente necios en sus posiciones. Paciencia.

de verdad afecta tanto un SELECT * FROM table a la db? gracias

Si traes más columnas de las que necesitas:
1. En algunas circunstancias puedes requerir más I/O para recuperar los datos del disco. Por ejemplo cuando hay campos grandes (BLOB, BYTEA, TEXT) externos, o filas tan grandes que exceden una página.
2. Requieres más espacio en memoria para almacenar las páginas en caché. Más aún, es posible que por haber hecho SELECT * tengas que vaciar el caché de otras páginas que eran útiles, sólo porque te trajiste más columnas de las necesarias.
3. Si fuiste sensato, y escribiste todo dentro de la base de datos, el ambiente de ejecución del lenguaje procedural va a tener que hacer más copias en memoria de valores que quizás no vayas a utilizar. Si fuiste insensato, y escribiste todo fuera de la base de datos, entonces hay que copiar todos esos datos hacia el programa que los procese; si ese programa está en otra máquina a través de la red, vas a requerir más ancho de banda.
Si solamente necesitas algunas columnas, es mucho mejor indicarlas explícitamente. En particular, si sólo necesitas columnas que forman parte de índices, PostgreSQL es capaz de satisfacer tu columna solamente empleando el índice, cosa que es bastante más rápida que hacer el recorrido sobre las páginas de datos, que casi nunca están contiguas.
Más aún, si tu aplicación hace uso correcto de la base de datos, y tienes restricciones de acceso (GRANT/REVOKE) sobre *columnas* de la base de datos, un SELECT * puede terminar en enigmático «permission denied».
Siempre es mejor especificar las columnas deseadas. Ahora bien, si construiste una vista que hace la sumarización y consolidación *exacta* de tu reporte/consulta, seguramente SELECT * es lo que quieres -- y por eso se diseñan las vistas con cuidado para precisamente eso. Hacer un SELECT * para traer todo a cualquier lenguaje fuera de la base de datos y «masajearlos» allí, es incivilizado para una aplicación en línea.

View more

A quien votas? a la pareja de Fernandez? a MacriCat? Lo que salga de la alternativa federal o a Espert?

Por peronistas, nunca. Ni por grupetes con mote de «socialista», «comunista», «patriota», y todos esos eufemismos por «sinvergüenza».
Por Macri o por Alfonsín, para que sigan matando el comunismo.

Alguna aplicación que recomiendes para administrar pequeñas CA's? Actualmente hago mis certificados digitales a mano.

También administro mi CA manualmente con `openssl`. Conozco mcuhas personas que defienden `tinyca` con fiereza, simplemente porque `openssl` se los comió...
Liked by: Sizz Lorr

Creo que pregunte mal, en que caso usarias *BSD (en cualquiera de sus familias) antes que Linux?

Si quisiera tener diversidad de plataformas para algún servicio, no como alternativa, sino como complemento.
Por ejemplo, en una plataforma DNS, tendría algunos secundarios con Debian GNU/Linux y otros con FreeBSD. El primario seguro estaría con Debian GNU/Linux.
No encuentro ningún beneficio notable en términos de desempeño usando FreeBSD, y si una diferencia importante en simplicidad de despliegue cuando uso Debian GNU/Linux. No tengo intenciones de usar Debian GNU/kFreeBSD.
Liked by: Sai Yan Marcos Mora

En que use cases recomendarias usar OpenBSD como servidor?

No es que lo recomendaría, sino que no lo vería mal siendo usado como firewall o bastión SSH/VPN/KDC, pero hasta ahí.
Eso también se puede hacer con Linux o FreeBSD. Sería la tercera opción.

Next

Language: English