¿Cuáles son algunos conceptos avanzados en programación que la mayoría de los programadores promedio nunca han escuchado?

La simple verdad es que la mayoría de las personas que se ganan la vida tienen una comprensión muy incompleta de la teoría. Hoy en día es común entender OO a nivel práctico, pero incluso aquellos que han estudiado ciencias de la computación a nivel de pregrado y tuvieron que estudiar, digamos, los patrones de diseño la han olvidado en su mayor parte a los pocos años de graduarse, si eso es así. Si le digo a un programador profesional “por qué no usar el patrón de observador” o “qué pasa con el uso de un generador”, la gente está desconcertada.

Las personas pueden programar regularmente utilizando bases de datos relacionales y no saber de qué se trata ACID, y dudo que la mayoría de ellos hayan oído hablar de álgebra / cálculo relacional. Un número alarmante no sabe qué es la normalización.

Los programadores no son, en general, inmensamente conocedores de la programación en su conjunto. Saben cómo hacer las cosas con las que trabajan regularmente y aprenden cosas nuevas según lo necesiten. Si son buenos, se mantienen al día con los nuevos desarrollos para descubrir nuevas y mejores formas de hacer las cosas.

Los que conocen innumerables paradigmas, definiciones formales, etc., son nerds entre los programadores. No me refiero a eso peyorativamente, solo digo que eso es lo que somos.

Aquí están algunos:

  1. Análisis predictivo: este es un nuevo enfoque para la inteligencia empresarial y la computación estadística donde los algoritmos utilizados para extrapolar o predecir cosas se agrupan cuidadosamente para crear un cuerpo de conocimiento, llamado análisis predictivo.
  2. Computación distribuida y escalable: el almacenamiento de archivos distribuido y escalable, la computación escalable y las bases de datos escalables rara vez se enseñan a nivel de pregrado y las áreas de conocimiento son tan nuevas que tomará un tiempo antes de que los programadores promedio conozcan estos detalles.
  3. Arquitecturas multiproceso: Antes de los días de C ++ y Java, solía haber ocasiones en que las personas solían escribir programas en C y hacían un uso intensivo de arquitecturas multiproceso en lugar de multiprocesos (en algunos casos, además de los roscados también). Esto significó una gran cantidad de otras tecnologías como colas de mensajes y memoria compartida para ser utilizadas también. Muy rara vez he encontrado programadores que sepan sobre el uso de la memoria compartida o las colas de mensajes basadas en Unix. Estas tecnologías aún están disponibles y están siendo utilizadas por aquellos que lo conocen.
  4. Conceptos de seguridad: escribir código para estar seguro es una ciencia en sí misma. Requiere un poco de aprendizaje para comprender cómo diversas prácticas de programación comunes conducen a lagunas de seguridad y cómo se pueden tapar para siempre.

No estoy seguro de llamarlo avanzado , pero he conocido a pocas personas que sabían acerca de la Seguridad de capacidad de objetos (OCS): la mayoría de los programadores de OO que he visto piensan en el control de acceso en términos de cosas como “privado / protegido / public / etc “. Esto es bastante triste porque nuestro software podría ser mucho más seguro si seguimos patrones similares a los de OCS.

Entonces, ¿qué es la seguridad de capacidad de objetos y por qué debería importarle?

OCS es un modelo de computación segura. En el núcleo de este modelo se encuentran las “capacidades”, que indican lo que se puede hacer en el sistema (por ejemplo, ” escribir en el sistema de archivos ” podría ser una capacidad, ” hacer solicitudes de red ” podría ser otra). Un objeto solo puede hacer cosas para las que tiene capacidad, y dado que las capacidades son imposibles de olvidar, se deduce que un objeto solo puede hacer cosas que usted explícitamente le permite hacer.

Esta es una propiedad muy importante en las aplicaciones que incluyen código no confiable (es decir, cada aplicación que la gente escribe en estos días, cada biblioteca de terceros es un código no confiable). Esto significa que si incluye una biblioteca A, las únicas cosas que A puede hacer son las cosas que deja explícitamente que A haga, y nada más.

nota: Si ha oído hablar del Principio de Privilegio Mínimo en seguridad: ¡esto es exactamente eso! (si no lo hizo, le recomiendo que lea sobre algunos de los principios básicos que guían la seguridad de la información)

En un modelo OCS, las capacidades son, naturalmente, objetos. Los objetos son entidades inolvidables: solo puedes poner tus manos en una si tienes una referencia a ella, no puedes fabricar ese objeto de la nada tú mismo. Los objetos solo se comunican mediante el envío de mensajes (o métodos de llamada), por lo que los mensajes se convierten en la forma natural en que se proporcionan capacidades a otros objetos en el sistema.

La robusta composición de Mark S. Miller: hacia un enfoque unificado para el control de acceso y el control de concurrencia, así como todas las cosas en su sitio web sobre el lenguaje E pueden ser buenos lugares para aprender más sobre esto.

( Edición: parece que el capítulo de Pony sobre Capacidades podría ser una introducción más accesible que la tesis de Miller, si tienes menos experiencia en el campo académico de los lenguajes de programación. Algunos de los conceptos son específicos de Pony, pero la mayoría es bastante general)

Los lenguajes OO comunes tienen algunos problemas que impiden que funcione un modelo OCS. Por ejemplo, en Java puede acceder directamente a los campos de un objeto (¡esas no son llamadas a métodos!), Y hay un espacio de nombres global al que cada objeto tiene acceso, por lo que cualquier cosa que incluya puede acceder a su sistema de archivos, por ejemplo. Java en particular trata de mitigar estos problemas con la inspección de la pila [1] para verificar algunos privilegios, pero eso es muy costoso y no general, lo que significa que la mayoría de las personas no lo usan.

¿Por qué no se ha puesto de moda la Seguridad de capacidad de objetos?

También es una pregunta interesante. Desde el exterior, OCS parece algo bueno, entonces ¿por qué no hay más personas usándolos? Pero, bueno, al mirar los lenguajes de programación en sí mismos, casi ningún lenguaje incluye incluso los elementos básicos esenciales para la modularidad y la integridad de los datos, y mucho menos la seguridad, por lo que creo que toda nuestra forma de ver el software podría ser un poco preocupante aquí.

Pero de todos modos, si bien es en su mayoría viejos e idiomas de investigación que entraron completamente en OCS, estamos obteniendo algunas cosas nuevas que van en esa dirección. El antiguo proyecto Caja de Google proporcionó este concepto para aplicaciones JavaScript. Pony es un nuevo lenguaje basado en actores que es completamente seguro para la capacidad de los objetos, y el nuevo sistema operativo de Google, Fuchsia, también es seguro para la capacidad.

Partes de los sistemas populares existentes también usan este concepto. Por ejemplo, la seguridad de JavaScript de Firefox se realiza con seguridad de capacidad a través de membranas.

Notas al pie

[1] http://sip.cs.princeton.edu/pub/

Procesamiento de vectores y algoritmos vectorizados.

Las CPU modernas de nivel de consumidor (durante los últimos 15 años) han sido capaces de procesar vectores, pero la mayoría de los programadores solo usan las capacidades de procesamiento escalar de las CPU. Esto es, IMO, principalmente porque todos los lenguajes populares actuales se diseñaron en torno a arquitecturas escalares, no hay lenguajes de nivel superior que admitan extensiones de lenguaje vectorial robustas o fáciles, y debido a que la mayoría de los programadores están resolviendo problemas que están sujetos a la entrada del usuario, el rendimiento no es primordial. Aumenta la velocidad de algunos tipos de matemática en múltiples órdenes de magnitud, y las CPU de baja potencia en teléfonos y tabletas no podían reproducir y grabar MP4 y códecs de medios similares sin él. Actualmente se usa más comúnmente en el procesamiento de señales digitales y la síntesis de imágenes. Las GPGPU son probablemente la única plataforma de desarrollo en la que es inevitable saber cómo escribir código vectorizado. El desarrollo de la consola de videojuegos solía ser otro, aunque no sé cuán útil es para las consolas de generación actuales (PS4, XB1) fuera del desarrollo de motores de bajo nivel.

Uso de la colección del compilador GNU (GCC) – Extensiones vectoriales

Programación CG / Operaciones de vectores y matrices


Carga dinámica

La carga dinámica le permite intercambiar en caliente el código compilado en un ejecutable en ejecución. Otra característica es la capacidad de mod sin liberar la fuente. También un uso de recursos potencialmente menor y una ejecución más rápida, por ejemplo: MAME tiene una tonelada (¿100+?) De núcleos de emulación vinculados con o en el binario. Para ejecutar un juego solo se necesitan un par. Cada vez que se ejecuta MAME, carga todo lo que necesita para cada juego. Si los núcleos de emulación se cargaran dinámicamente en función del juego seleccionado, el tiempo de carga y el uso de memoria serían significativamente menores.


Mesa de la rama

Probablemente esto sea mejor conocido, pero rara vez lo veo en el código. Es una técnica de optimización. Si puede reducir cualquier prueba de ramificación de múltiples vías a un índice, puede optimizar el control de flujo utilizando una tabla de ramificación. Algunos compiladores reducirán una declaración de cambio a una tabla de bifurcación en las condiciones correctas, pero solo lo he visto una vez y con ICC. Es una optimización bastante difícil para un compilador. (Parece que muchos desarrolladores hoy suponen que probar la igualdad de cadenas es gratis o tan barato como lo es con primitivas, y un compilador no tiene la oportunidad de crear una tabla de ramificación con cadenas). Funciona creando una matriz de funciones, luego usando la variable de prueba como un índice en esa matriz para ramificar. Entonces, por ejemplo, en pseudocódigo:

Normal:

a = getDropDownMenuSetting ();
if (a == “helado”) icecream.serve ();
más
if (a == “pastel”) cake.serve ();
más
if (a == “mousse”) mousse.serve ();
// etc … más más / if declaraciones

o:

a = getDropDownMenuSetting ();
cambiar a:
caso “helado”:
icecream.serve ();
rotura;
caso “pastel”:
cake.serve ();
rotura;
// etc …

Una tabla de bifurcación optimiza la bifurcación de la siguiente manera:

const serveDessertFunctionArray [] = [
helado de hielo,
cake.serve,
mousse.serve,
// … etc.
]

a = getDropDownMenuSettingAsIndex ();
serveDessertFunctionArray [a] ();

Tantas respuestas, todas diferentes. Y a todos les falta el ingrediente que me ha mantenido empleado hasta los 70: Hard Real Time. Lo que significa que la máquina debe ser capaz de procesar una entrada de evento y producir la salida correcta dentro de un tiempo objetivo específico. En un entorno de x tales eventos / seg simultáneamente. Si falla, el sistema también fallará. Choque un brazo robot en sus soportes o una persona; produce lo que yo llamo Bad DSP Noise a los altavoces y potencialmente los destruye. Maltratar o perder un paquete de comunicaciones de datos. Varios otros estoy seguro si me detengo en los detalles; te dan la imagen. Y la regla fundamental en este mundo: simplemente funciona. Nunca se bloquea. Puede permanecer invisible en los armarios de cableado durante décadas y en los ciclos de rotación del personal de TI hasta que alguien lo encuentre y no sepa lo que hace hasta que lo apague. Nunca arruina el tiempo en medio de un concierto de música de alto perfil y se produce un motín. La mayoría de las llamadas de servicio son por error del usuario o llamadas de ayuda (¡así que actualice el manual!). Iow: cosas de misión crítica.

Muchas aplicaciones tienen objetivos de HRT en el rango de microsegundos, muchas más en milisegundos de un solo dígito. Y es el caso normal que cualquier sistema operativo de propósito general que intente alojar tales aplicaciones simplemente caiga sobre sus pies: tienen demasiada sobrecarga siendo “segura” y demasiado sobrecarga tratando de ejecutar tareas no relacionadas al mismo tiempo. Incluso si su silicio ejecuta código de 64 bits a velocidades de reloj de GHz. Hay problemas que no se pueden resolver simplemente lanzándoles hardware más rápido o más paralelo. Toman pilas de software cuidadosamente diseñadas desde el metal hacia arriba, y en mi humilde opinión, incluso toman arquitecturas e interfaces cuidadosamente diseñadas dentro del metal. He trabajado en ambos lados de esa división en el pasado, aunque no siempre (debido a la extraña y frecuente idea de que son conjuntos de habilidades distintas que no son accesibles para cerebros individuales) en ambos lados del mismo sistema.

Entonces, para las aplicaciones HRT, ¿cuáles son las técnicas que importan y qué se interpone en el camino?

Ponte en el camino: hilos. Recolección de basura. Cualquier algoritmo peor que O (n), preferiblemente O (1). Preámbulos y postambles de funciones complejas. Implementaciones de lenguaje interpretado o de código P. Mecanismos de interrupción global que requieren análisis de software. bibliotecas de terceros sin disponibilidad de código fuente. Re que, GPL también está en el camino, por lo general. Y uno que puede sorprenderle: depuración de puntos de interrupción de estilo gdb o VC ++. Útil para choques duros, pero no utilizable para las anomalías de comportamiento más difíciles. Finalmente, cualquier ciclo no gastado en calcular las salidas se desperdicia (sobrecarga si lo prefiere). A veces no hay otra opción, y algunas de esas veces hay soluciones efectivas, y a veces solo se hace por la piel de los dientes debido a ellos. Y a veces solo tiene que negociar la parte de rendimiento de la especificación de requisitos. Sin embargo, creo que eso solo me pasó una vez.

En cambio: las funciones de alto orden devuelven funciones con la misma firma. Puede construir máquinas de estado increíblemente rápidas a partir de estos. Programación cooperativa para multiplexar esos FSM. Las continuaciones y los cierres pueden ser útiles si hay un poco de holgura para mantener el estado. Las corutinas no son tan buenas porque requieren más apilamiento local, pero tienen su lugar. Capacidad para ejecutar submáquinas (versiones FSM de subrutinas). Si tiene libertad, el análisis de interrupciones de hardware presenta un vector único para cada posible fuente de interrupción y razón, con round robin o prioridades de preajuste completo. Nuevamente, si tiene libertad, duplique las pilas de registros o un núcleo reservado para el manejo de interrupciones (la sincronización entre núcleos es más complicada, pero solo necesita hacerlo una vez, en el programador, y hacerlo allí mismo).

Probablemente algunas técnicas más. Desarrollé las bases de este conjunto diseñando un statmux de comunicaciones de datos compuestas de 8 entradas a 1 usando un Z80 con 4K ROM y 4K RAM cuando la visión predominante era que solo se podía hacer con 5 Z80 y 16K. Continúo desarrollando y refinando estas ideas hasta el día de hoy, a pesar de que ahora tengo relojes GHz y líneas de caché de 128 bits, etc. Porque los escritores de juegos lo necesitan todo y aún más.

Macros

(No, no las macros C).

Las macros de raqueta le permiten realizar una metaprogramación en tiempo de compilación, escribir código que escribe código (o código que escribe código que escribe código, hasta el infinito). La capacidad de manipular las características del lenguaje que no son de primera clase le permite eliminar cada pieza de repetitivo que no puede con las funciones. Dicho de otra manera, mientras que las funciones se resumen sobre los valores, las macros se resumen sobre la sintaxis. Puede crear nuevas construcciones de lenguaje y llevar el lenguaje de programación al nivel de abstracción de su problema.

Por ejemplo, aquí hay una macro Racket simple para “factorizar” la memorización:

(define-syntax-rule (define/memoized (fx ...) e ...)
(define f
(let ([m (make-hash)])
(λ (x ...) (hash-ref! m (list x ...) (λ () e ...))))))

Aquí hay una definición normal de Fibonacci en Racket:

(define (fib n)
(if (< n 2) n
(+ (fib (- n 1)) (fib (- n 2)))))

Para memorizarlo, ahora solo cambiamos define a define/memoized :

(define/memoized (fib n)
(if (< n 2) n
(+ (fib (- n 1)) (fib (- n 2)))))

(tiempo (fib 1000))
; resultado:
; tiempo de CPU: 0 tiempo real: 0 tiempo gc: 0
; 434665576869374564356885276750406258025646605173717804024817290895365554179490
; 518904038798400792551692959225930803226347752096896232398733224711616429964409
; 06533187938298969649928516003704476137795166849228875

También puede usarlo para funciones con cualquier número de argumentos:

(define/memoized (ackermann mn)
(match* (mn)
[(0 _) (+ n 1)]
[(_ 0) (ackermann (- m 1) 1)]
[(_ _) (ackermann (- m 1) (ackermann m (- n 1)))]))

(tiempo (ack 4 1))
; resultado:
; tiempo de CPU: 204 tiempo real: 203 gc tiempo: 40
; 65533

Las macros de raquetas son higiénicas: aunque la macro anterior introduce una nueva variable m , no interferirá de ninguna manera con el usuario macro incluso si el usuario tiene una variable diferente también llamada m .

Con las macros, cualquiera que sea la característica que desee tener en su idioma, no necesita esperar a que el implementador del lenguaje agregue: es solo otra macro que puede agregar usted mismo (y compartir con otros como biblioteca si lo desea). De hecho, las macros Racket son mucho más potentes que una simple transformación de código como el ejemplo anterior: pueden realizar cálculos arbitrarios, incluidos los efectos secundarios.

Racket en sí es un lenguaje pequeño. Todas las características del lenguaje aparentemente fundamentales, como la coincidencia de patrones, el sistema de objetos o los módulos de primera clase son solo macros. Los dialectos de Racket como Typed Racket, Lazy Racket y Scribble, todos se expanden de forma macro a Vanilla Racket, y un programa puede estar compuesto de diferentes módulos escritos en diferentes idiomas (o su propio DSL), lo que considere más apropiado.

Puede encontrar más información sobre Racket aquí (tienen excelente documentación y materiales de aprendizaje).

Tipos dependientes

La mayoría de los sistemas de tipos lo ayudan a descartar ciertas clases de errores. En un lenguaje de tipo dependiente, puede ir más allá y codificar invariantes de programa arbitrarios en los tipos. Una vez que el programa se compila, sabes que es correcto (hasta las especificaciones que imponen los tipos), no porque no hayas visto el caso de prueba que falla, sino porque el compilador ha demostrado estáticamente que tu programa no puede salir mal.

Un ejemplo clásico es un vector cuyo tipo informa sobre su longitud: (el ejemplo está en Agda)

data List (A : Set) : ℕ → Set where
[] : List A 0
_∷_ : ∀ {n} → A → List A n → List A (1 + n)

Ahora puede indicar más sobre funciones como map o append , como “el map conserva la longitud” o “la lista de resultados de append es tan larga como la longitud total de sus argumentos”.

map : ∀ {AB n} → (A → B) → List A n → List B n
map f [] = []
map f (x ∷ xs) = fx ∷ map f xs

_ ++ _: ∀ {A mn} → Lista A m → Lista A n → Lista A (m + n)
[] ++ ys = ys
(x ∷ xs) ++ ys = x ∷ (xs ++ ys)

Además de la corrección, más información significa que el compilador tiene más oportunidades para optimizar su programa. Por ejemplo, ML dependiente utiliza una forma limitada de tipos dependientes para optimizar las comprobaciones vinculadas para operaciones de matriz. Puede hacer cosas más complicadas, como definir un nuevo lenguaje de programación cuya semántica sea dada por un intérprete, luego escribir un compilador garantizado para emitir código de bajo nivel que esté de acuerdo con esta semántica de alto nivel.

Idris es un lenguaje de tipo dependiente destinado a la programación práctica.
(en lugar de probar el teorema). Su tutorial es un buen lugar para comenzar.

Bases de datos de aprovisionamiento de eventos: casi todas las bases de datos almacenan el estado actual de algún registro. Pero realmente ese registro es solo una vista agregada de todos los cambios que se le han producido, generalmente en su código. Mucha gente almacena el “historial” pero no almacena el evento real que cambió el elemento. Pero si lo hace … puede responder las preguntas de mañana con datos de ayer. En realidad, usas esto todos los días y probablemente no lo sepas … es el concepto en el que se basa Git. Esto tiene vínculos directos con los sistemas de transmisión de actores / mensajes, consistencia eventual, escalado y …

CQRS: segregación de responsabilidad de consulta de comandos. A la mayoría de los desarrolladores les gusta un solo lugar para los datos. les gusta estandarizar en un solo sistema de registro. Es fácil para ellos lidiar con esto en su mente. Pero si puede dividir sus sistemas en un conjunto que lee (consulta) y establece ese cambio (comando) y puede proyectar datos desde el almacenamiento de comandos al almacenamiento de consultas, le espera una increíble oportunidad. Se han ido las necesidades de escalar ambos lados del sistema de la misma manera … ya que nunca han sido lo mismo y, por lo tanto, fundamentalmente no pueden escalar de la misma manera. Es solo cuando divide los datos y las interacciones con ellos de esta manera que realmente puede comenzar a asimilar su dominio y convertirse en un experto (y lograr un excelente DDD … mencionado a continuación). Event Sourcing es un fantástico mecanismo de almacenamiento para el lado del comando. Una vez más, Git es un gran ejemplo de esto … todos los cambios se almacenan y su directorio de trabajo actual es solo todos los cambios aplicados.

Mientras estoy en DB – The Pick DB / Pick OS – Crear por Dick Pick (no es broma). Es difícil de describir, pero es como un ORM integrado en el DB, pero mucho más complejo que eso. Realmente es el concepto de una base de datos multivalor de archivo hash. La mayoría de los desarrolladores sr han construido algo como esto de vez en cuando, pero generalmente con código y múltiples consultas a la base de datos.

Ley inversa de Conways: la ley de Conways dice que el sistema tiende a estructurarse para reflejar los equipos y sus canales de comunicación que los establecen. IE: con 1 equipo generalmente obtienes un monolito. Con múltiples equipos, el código que construyen usará la comunicación de red como los humanos usan la comunicación. La ley inversa de Conway es una estrategia tal que debe construir sus equipos para que coincidan con la estructura del sistema que desee. Esto funciona tanto en niveles micro como macro. Uno de los artefactos de agile y que QA / sdet sea una función del equipo de desarrollo es realmente solo aplicar la ley inversa de conways. El movimiento DevOps en general es de nuevo simplemente aplicar la ley inversa.

Arquitectura RESTful: tan raro que veo que alguien realmente sepa de lo que realmente está hablando aquí. Generalmente es “pongo parámetros en la url, y devuelvo json, soy una API REST”. Pero esto está muy lejos de la verdad de lo que es la arquitectura tranquila. Tengo mis propias opiniones al respecto en Video: ¿Qué es RESTful? (2.0.0)

Diseño impulsado por dominio: al final, casi todo el software está modelando cosas del mundo real, sin embargo, pasamos tanto tiempo haciendo que el mundo real se ajuste a nuestros modelos de software existentes. Así es como podemos entregar cosas a bajo costo (menos código) y mantenerlas (menos código). DDD se trata principalmente de lo contrario, permitiendo que los codificadores codifiquen. A medida que el porcentaje de personas en el mundo que puede codificar aumenta, ‘será más fácil codificar exactamente los dominios que existen, y será mejor mantener esos sistemas haciendo cambios en el código, no evitándolos. Y realmente … cualquiera que haya estado haciendo esto sabe que, en su mayor parte, ningún sistema vive sin mantenimiento constante de cambio de código de todos modos. Si pasa la mayor parte de su tiempo codificando y refinando el dominio, la mayoría de las otras cosas se manejan mediante bibliotecas y conceptos estándar. El valor del negocio es casi siempre el dominio.

Prueba de mutación: realmente el santo grial de TDD. Esta es una técnica para evaluar sus pruebas. Cuando llegas al 100% de cobertura de código, comienzas a sentirte bien … pero a veces solo estás ejecutando líneas y no te importa lo que está haciendo esa línea. De hecho, muchas de sus pruebas son esto. La prueba de mutación modifica una de sus líneas reales a otra, y luego ejecuta su conjunto de pruebas. si no falla ninguna prueba / cada prueba pasa con el código modificado … ¡entonces nunca probó realmente la ejecución correcta de esa línea en primer lugar! Desea saber qué tan cubierto está su código, ejecutarlo a través de una prueba de mutación, se sorprenderá de todas las cosas que realmente no está asegurando. Además, cualquier persona que haya desarrollado / mantenido un proyecto (especialmente cuando se utiliza un enfoque DDD, ya que esto desencadena toneladas de refinamiento en su código) donde la intención original de cualquier prueba se pierde o ya no es aplicable … pero aún se aprueba, por lo que lo mantiene. . Las pruebas de mutación realmente mantienen sus pruebas (y su intención) alineadas con la base del código actual.

Programación funcional reactiva (FRP): no es solo una programación funcional, y no es la “programación reactiva” del manifiesto reactivo. Es la idea de los flujos de eventos que se reaccionan y se agregan. Funciona bien con bases de datos de abastecimiento de eventos cuando se habla de datos, de hecho, usarlo para agregar eventos en vistas agregadas y proyectar esas vistas en un modelo de lectura para la parte de consulta de su sistema CQRS es una gran estrategia. Pero también funciona en una secuencia de entrada del usuario. La unión bidireccional está bien y todo … pero la industria se está alejando de eso y hacia FRP (angular sigue el paso de reaccionar hacia esto) pero realmente oculta el verdadero procesamiento de flujo del desarrollador … lo cual es un terrible error. Echa un vistazo a Rx, RxJava, Bacon.js, etc. para ver realmente su poder. El ejemplo que me encanta es cuando necesita cancelar esa llamada ajax para escribir con anticipación porque el usuario ha agregado más entradas. ¿Alguna vez ha intentado programar eso y asegurarse de que la llamada ajax correspondiente a los últimos datos es la que se dibuja en la pantalla?

  1. Pureza , como en las funciones libres de efectos secundarios. Nunca he conocido a un programador en persona que conozca estos conceptos, sin embargo, este es uno de los aspectos más importantes de modularidad, escalabilidad y robustez.
  2. Programación funcional Casi ninguno de mis colegas había escuchado sobre eso antes de que lo mencionara por primera vez. Lo mismo para mis maestros cuando estaba en la universidad.
  3. Funciones de orden superior , es decir, funciones que toman una función como argumento y / o devuelven una función. Tales funciones son tan versátiles y poderosas. Permiten escribir menos código que hace más. Todos los programadores deben saber sobre las funciones de orden superior.
  4. Aplicación de función parcial . Cuando no proporciona todos los argumentos que necesita una función y crea una nueva función que toma los argumentos que faltan. Por ejemplo, supongamos que tenemos una función draw (Bitmap bmp, int x, int y), con lenguajes que admiten PFA, puede crear una función drawTree = draw (theTreeBitmap).
  5. Tuplas , es decir, secuencias de valores. La próxima versión de C # tendrá un mejor soporte para las tuplas, por lo que esperamos que más programadores descubran y usen estas pequeñas maravillas.
  6. Mónadas Son tan poderosos e inteligentes, pero la gran mayoría de los programadores nunca han oído hablar de ese término. No intentaré explicar cuáles son: parece que nadie puede explicar ese concepto de una manera que otro programador realmente entienda, y no voy a ser el próximo en aventurarse allí …

El 4to punto explicado con código:

En Haskell:

drawTrees :: [(Float, Float)] -> IO ()
posiciones de drawTrees = do
treeBitmap <- loadTreeBitmap
paraCada posición (dibujar treeBitmap)

draw :: Bitmap -> (Float, Float) -> IO ()
dibujar posición bmp = …

loadTreeBitmap :: IO (mapa de bits)
loadTreeBitmap = …

forEach :: [a] -> (a -> IO ()) -> IO ()
forEach list action = mapM_ action list

En C #, si admitía la aplicación de función parcial:

anular drawTrees (Listar posiciones)
{
Mapa de bits treeBitmap = loadTreeBitmap ();

posiciones.porCada (draw (treeBitmap));
}

sorteo vacío (bitmap bmp, posición Vector2)
{

}

Mapa de bits loadTreeBitmap ()
{

}

vacío estático para cada uno (esta lista lista, Acción acción)
{
for (int i = 0; i acción (lista [i]);
}

Editar: En respuesta a algunos de los comentarios, escribí que casi ninguno de mis profesores en la universidad había escuchado sobre FP. Resulta que solo los más jóvenes se habían enterado.

Una vez más, escribí que casi ninguno de mis colegas había escuchado sobre FP. El hecho es que solo los más jóvenes que estudiaron programación en una universidad habían oído hablar de eso.

Que no hay conceptos avanzados en programación.

Bueno, tal vez hay un concepto secreto: la mayoría de nosotros, programadores experimentados, realmente no creemos en las ilusiones publicadas por los informáticos.

Durante décadas, recibí el ACM Journal cubierto de amarillo sobre Programación Orientada a Objetos y lo encontré más divertido que MAD Magazine. Y me encanta el mecanismo del objeto y lo considero una verdadera obra de genio.

Después de haber leído las otras respuestas (serias), me veo obligado a responder:

Todas esas técnicas de codificación inteligentes que he leído generalmente no producen mucha ganancia para los productos de software finales.

Sería mucho mejor particionar sabiamente, codificar simplemente y dedicar el tiempo del proyecto al diseño del algoritmo y su multitarea. La codificación inteligente y sofisticada generalmente no compra mucho. De hecho, la codificación incorrecta a menudo es corregida por el compilador de optimización.

La programación de computadoras consiste en diseñar un algoritmo (para realizar el trabajo) y realizar tareas múltiples de manera eficiente, conociendo las características de las computadoras digitales. Luego se codifica, depura y prueba.

El tiempo dedicado al diseño del algoritmo, la partición y la multitarea generalmente vale la pena. El tiempo dedicado a la codificación inteligente generalmente no vale la pena, suponiendo que la codificación simple original sea decente para empezar.

La codificación simple también es más fácil de mantener y cambiar que la codificación inteligente.

Y si observa todos los avances logrados con las computadoras a lo largo de las décadas, descubrirá que los algoritmos (y las mejoras de hardware) fueron los avances, y rara vez se debió a una codificación inteligente.

Apunto al reconocimiento de voz, minería de datos, gráficos, comunicaciones de datos y otros logros importantes.

Entonces, ¿por qué los programadores dedican tanto tiempo y orgullo a la codificación inteligente en lugar de a los algoritmos, a la partición y a la multitarea?

Creo que es porque los algoritmos más sofisticados (y los avances de hardware) están diseñados por otros ingenieros y no por los programadores del proyecto.

Entonces, ¿qué queda para los programadores del proyecto?

¿Qué conceptos avanzados en medicina son desconocidos para el médico promedio? Casi todos son relevantes solo para especialistas.

Fue innovador en la década de 1970 hacer aplicaciones comerciales en sistemas de 0.5k a 4k al sobrecargar el código y los datos ya ejecutados con los bits que se ejecutarán a continuación desde medios magnéticos. Eso es ridículo ahora. Usted no sabe ni puede saber todo, especialmente lo que tiene un valor temporal.

Los conceptos de programación (multiprogramación, procesamiento paralelo, procesamiento heterogéneo, memoria virtual) resuelven problemas invisibles para los programadores de negocios, que son la mayoría y el promedio. Esos absorben cada concepto a medida que los fabricantes de herramientas lo integran en el sistema operativo o las herramientas de programación. La migración de aplicaciones desde el escritorio a la web oscurece aún más el O / S y las herramientas. Algunos conceptos, como la programación funcional, han existido durante años en lenguajes como Lisp, pero tuvieron poca utilidad para los programadores de negocios. Las técnicas de diseño son diferentes.

Diseño estructurado, diseño DBMS, diseño orientado a objetos, diseño basado en requisitos y lo que sigue. Cada uno conecta la tarea a realizar por y para personas del mundo real más directamente con la implementación que lo hace con cada vez menos preocupación por los conceptos de programación.

Me gustaría reformular la pregunta para aclarar mi respuesta, pero creo que se relaciona de manera importante con su pregunta: “¿Cuáles son algunos conceptos avanzados en programación que la mayoría de los programadores promedio nunca han pensado ?”

Mi respuesta a eso es la idea de que la programación es una forma de modelar con la computación. Lo que quiero decir con eso es que no se trata solo de lo que puedes hacer con la programación para crear algunos efectos deseados, sino de lo que puedes representar y pensar en lo que significa esa representación, no solo en sí mismo, sino para el tipo de discusión que la gente puede tener con él, qué se puede crear con él y con qué facilidad (lo que quiero decir con una pequeña cantidad de código, para mantener la carga cognitiva razonable en relación con la complejidad del problema).

Durante los últimos 20 años, la práctica estándar en la industria ha sido aislar la programación del usuario, ya que su trabajo al usar computadoras ha sido facilitar la operación de la computadora y su interacción con los datos. O bien, el propósito detrás de la programación que se ha percibido ha sido crear una experiencia en la que el usuario participe, ya sea siendo principalmente pasivo o interactuando con otras personas de maneras muy universales, pero no agudice la discusión por encima de algún umbral.

Hemos creado un mundo digital que es una versión optimizada del mundo analógico que hemos dejado atrás, pero no un mundo computacional. Lo que quiero decir es que la computación permite lo que hacemos, pero nuestro acceso a usar literalmente la computación para nuestros propios fines solo está disponible en nichos.

Un gran problema para permitir que este uso de la computación aumente nuestro nivel de discurso es que requiere un tipo diferente de educación que ayude a las personas a comprender de qué se tratan realmente las matemáticas y las ciencias. Nuestro sistema educativo incorpora un conjunto de ética que dice que es importante sensibilizar a las personas sobre los productos de las matemáticas y las ciencias, y de alguna manera permitir que las personas los utilicen de manera utilitaria, pero esto no nos lleva a un lugar donde podemos entender lo que he dicho aquí y por qué es importante.

He escrito más sobre esto en mi blog en la programación Reviving como alfabetización.

Probablemente, los tres conceptos más avanzados en programación de los que la mayoría de los programadores promedio nunca dan evidencia de saber nada son previsión, planificación y pruebas. La mayoría de las veces, las pruebas (si las hay) se presentan como una ocurrencia tardía en el mejor de los casos. Como no tienen ninguna prueba mientras escriben el programa, no saben si lo que han escrito funciona correctamente mientras están codificando, o cuándo detenerse cuando todo lo que se solicitó se realizó y funciona. Por supuesto, si tuviera algunas pruebas antes de escribir ese código, lo sabría. Lo que nos lleva a la planificación, no solo a sumergirnos en un código de escritura inicial. Por supuesto, a sus jefes les encanta (mira todas esas líneas de código, demos aumentos), pero el proyecto, de alguna manera, nunca sale como se suponía. Si alguien recuerda cómo se suponía que debía ir.

Ah, y previsión – irónicamente, lo guardé a propósito para el final (- ríete aquí si quieres -). ¿El producto terminado incluso hace lo que quieres que haga? Oh, su programa de consulta de base de datos realmente resuelve crucigramas. Ummmm, qué lindo.

Sobreviví con expresiones regulares durante 30 años. En sed, grep, awk, Python y vim. Racket tiene RE’s y RE’s en ‘match’, una función de coincidencia de patrones. La coincidencia de patrones es mucho más que expresiones regulares, es una forma diferente de programar. Los patrones deconstruyen una lista de numerosas maneras. Los patrones también pueden ser predicados para probar cualquier cosa, incluidas las coincidencias RE. Cuando un patrón coincide, algunas funciones se ejecutan con componentes seleccionados de los patrones coincidentes como argumentos. En una función recursiva, un patrón de detención es el elemento izquierdo y el elemento derecho el resultado
resultado.
[‘()’ ()]
Una función inversa para invertir una lista podría usar un patrón del último elemento de una lista (‘b’) y la lista completa sin el último elemento.
[(lista a … b) (contras b (reverso a))]
Agregue cada último elemento sucesivo al último elemento anterior hasta que la lista esté vacía.


¿Qué pasa con las especificaciones? Las condiciones previas y posteriores de las interfaces, incluida la suposición que hace sobre los parámetros. Usar afirmaciones para verificar esas condiciones. Aún mejor, ¿describirlos usa inglés simple y lógica formal para que no haya malentendidos o ambigüedades?

Los beneficios son:

1. Ayuda a comprender el programa, las personas no tienen que leer la implementación para comprender la intención y las restricciones.

2. Ayude a escribir código de alta calidad obligándose a pensar con claridad antes de codificar.

3. Ayuda a descubrir errores temprano.

¿Por qué la gente no los escribe?

La misma razón por la que el uso del condón no aumentó después de que las personas saben que previene ciertas enfermedades.

Esa es una pregunta realmente interesante, y al leer las respuestas encontré cosas que aparentemente necesito aprender.

Hace muchas lunas, busqué ayudar a mi universidad construyendo una supercomputadora con piezas de computadoras de desecho. En mi viaje de crear lo que eventualmente fue una máquina núcleo Linux 88 con varios terabytes de RAM, aprendí un montón. Mi respuesta para usted es la informática concurrente y cuándo es útil y cómo funciona.

Al buscar software preexistente, las cosas de código abierto no estaban haciendo el corte. Por lo tanto, debe descubrir cómo los procesadores programan los trabajos, separando las cosas para que sean intrínsecamente paralelas siempre que sea posible, la comunicación de la máquina y la agregación de resultados, etc. Así que aquí es largo. Incluye quórum de procesador y montaje.

Fue muy divertido e incluso tuve que programar un hardware integrado para la optimización [que es un área que, según creo, los programadores deberían saber].

Aparte de esto, aprendí:

  1. Las arquitecturas independientes son de mal humor cuando chatean.
  2. SSH es un tesoro, también lo es vim
  3. Dell Optiplex puede tomar una caminata en serio
  4. Asegúrese de que su batería vref esté cargada
  5. A los solucionadores no les gusta este juego [alguien ya gana mucho dinero aquí].
  6. Puede que odies el código que parece lograr magia, pero cuando ocurre la magia, lo dejas en paz. Hasta que deje de imitar, en ese momento te quitas el sombrero de la hélice del médico brujo y vuelves a los negocios.
  7. La programación es un viaje monádico.

Fantástica pregunta, ¡continúa!

Cuanto más sabes, más te das cuenta de cuánto no sabes.

Sólo para nombrar unos pocos….

Memoria transaccional de software, validación cruzada N-Fold, mónadas, fibras, unificación, programación inductiva, programación lógica inductiva, programación funcional inductiva, aumento de gradiente, todo lo relacionado con explosión combinatoria u optimización combinatoria, bosque aleatorio, máquinas de vectores de soporte, autómatas celulares, principal análisis de componentes, agrupación de medios k, vecinos más cercanos a k, cuaternarios, redes bayesianas, refuerzo ada, refuerzo suave, árboles de partición de espacio binario, cinemática inversa, cinemática directa, representación volumétrica, radiosidad, cualquier tipo de algoritmo de iluminación global, máquinas de vectores de soporte, principal análisis de componentes, descomposición propia, caras propias, transformadas de coseno, transformada de Fourier, transformadas rápidas de Fourier …

Hay muchas áreas de especialidad en programación y una innumerable cantidad de algoritmos y técnicas. La mayoría de los programadores apenas han arañado la superficie de lo que es posible. Incluso los “mejores” programadores terminan eligiendo un área de especialidad.

La verdadera pregunta es “¿Cuál es el problema que estás tratando de resolver?”

En C / C ++, hay formas de reducir la lógica ‘if / then / else’ que puede ayudar a acelerar las cosas. Si hay demasiadas declaraciones if / then, puede provocar que los aspectos predictivos de la rama de la CPU se inunden y aumente el consumo de energía.

Un ejemplo muy simple (quizás pobre) no se multiplicará por cero o uno para acelerar las operaciones matemáticas.

p.ej

int A = 10;
int B = entrada de usuario;
int C;
si (B == 0) C = 0;
más
si (B == 1) C = A;
más
C = A * B;

Esta lógica bien intencionada intenta acelerar la multiplicación eliminando multiplicaciones innecesarias, pero el uso del múltiple if / then / else puede ralentizar las cosas.
entonces,

int A = 10;
int B = entrada de usuario;
int C = A * B;

En realidad podría producir resultados más rápidos.

No estoy seguro de que esto califique porque es más un enfoque alternativo a la programación en general que un concepto / técnica avanzada, y porque muchos programadores al menos han oído hablar de él (aunque lo más probable es que nunca haya encontrado un lenguaje / entorno que realmente esté implementando ), pero siempre me ha intrigado el concepto de programación Literate .

La idea de escribir el código fuente de un programa como casi un producto secundario de documentar su funcionamiento me parece un enfoque muy elegante para crear código bien documentado, bien pensado y fácilmente mantenible.

Es probable que tenga sus inconvenientes y limitaciones, y creo que hay buenas razones por las que nunca se convirtió en un éxito, pero el concepto es bastante brillante, en mi humilde opinión.

“Todo el mundo sabe que la depuración es el doble de difícil que escribir un programa en primer lugar. Entonces, si eres tan inteligente como puedes ser cuando lo escribes, ¿cómo lo vas a depurar? ”- Los elementos del estilo de programación, Kernighan y Plauger.

Puede que no parezca un concepto avanzado, pero mi experiencia es que la mayoría de los programadores promedio no han oído hablar de él, y los mejores programadores que conozco piensan en ello cada vez que escriben código.

AVX 512: Extensiones vectoriales avanzadas 512

Las Extensiones de vectores avanzadas son las instrucciones de Intel para llevar a cabo el procesamiento de vectores. Son instrucciones SIMD (Single Instruction Multiple Data) que utilizan 32 registros de vector de 512 bits. Cada uno de estos registros de 512 bits puede incluir 16 números de coma flotante de precisión simple (32 bits) u 8 números de coma flotante de precisión doble (64 bits). Los registros también pueden operar en enteros (32 o 64 bits).

SIMD significa que una sola instrucción opera en múltiples datos. Un ejemplo muy simple de eso sería agregar todos los números en una matriz. Usted da las instrucciones y el procesador las agrega en grandes pedazos a la vez .

Entonces, la próxima vez que desee agregar todos los elementos en una matriz, verifique que su compilador admita extensiones vectoriales y úselos. Esto debería hacer que su código sea más rápido.

Instrucciones de captación previa: las memorias caché son bits de memoria integrados en el procesador y, por lo tanto, son mucho más rápidos de acceder que la RAM. Los procesadores Intel tienen 3 niveles de cachés

  • Caché L1: Este es el más cercano al procesador y el más rápido para acceder por el procesador. El caché L1 es de 32 kb; 16 kb para código y 16 kb para datos.
  • Caché L2: estos son el siguiente nivel de caché después de los cachés L1. Toma un poco más de tiempo acceder a estos que acceder a los cachés L1 . Los cachés L2 son 128 kb (código combinado + datos).
  • Caché L3: los cachés L3 son de 3 MB y el acceso a ellos es mucho más lento que los otros 2 pero más rápido que la recuperación de la RAM. Al igual que L2, este es un código combinado + caché de datos.

Las instrucciones de captación previa de Intel recuperan datos (y código) en los cachés L1 y L2 antes de que sean necesarios. Esto hace que el acceso posterior a estos bits de datos y código sea mucho más rápido. Sin embargo, Intel advierte que no es necesario utilizar estas instrucciones. Los procesadores Intel ya hacen un buen trabajo en el negocio del almacenamiento en caché.

El procesador obtiene datos de la RAM en los cachés en fragmentos llamados líneas de caché y el ancho de la línea de caché es de 64 bytes. En otras palabras, cuando el procesador tiene que obtener cosas de la RAM, obtiene 64 bytes a la vez. Esto se hace mediante la obtención de datos 8 veces obteniendo 8 bytes cada vez (esto se llama modo ráfaga).

Este mecanismo de almacenamiento en caché explota el concepto de localidad de referencia espacial (cuando accede a una pieza de datos o código, pronto necesitará acceder a piezas de código o datos a su alrededor). Aquí es donde las matrices tienen ventaja sobre las listas vinculadas. Recuerde que los elementos de una matriz viven uno al lado del otro en la RAM, por lo que cuando accede a un elemento de la matriz, existe una gran posibilidad de que el siguiente elemento ya esté en la caché.

Traducción Lookaside Buffer: este es un tipo especial de memoria para el almacenamiento en caché de direcciones. Es como esto; cuando el procesador accede a una dirección en la memoria, convierte la dirección dada de su forma virtual en una dirección física. Los resultados de esta conversión se almacenan en caché en el TLB para que el proceso sea más rápido la próxima vez.