Articulo de referencia

Programación reactiva

En informática , la programación reactiva es un paradigma de programación declarativa que se ocupa de los flujos de datos y la propagación de cambios. Con este paradigma, es pos...

En informática , la programación reactiva es un paradigma de programación declarativa que se ocupa de los flujos de datos y la propagación de cambios. Con este paradigma, es posible expresar con facilidad flujos de datos estáticos (p. ej., matrices ) o dinámicos (p. ej., emisores de eventos ), y también comunicar que existe una dependencia inferida dentro del modelo de ejecución asociado , lo que facilita la propagación automática del flujo de datos modificado . [ 1 ] [ 2 ]

Por ejemplo, en la programación imperativa , significaría que se le asigna el resultado de en el momento en que se evalúa la expresión, y posteriormente, los valores de y pueden cambiarse sin que ello afecte al valor de . En cambio, en la programación reactiva , el valor de se actualiza automáticamente cada vez que cambian los valores de o , sin que el programa tenga que volver a enunciar explícitamente la instrucción para reasignar el valor de .a := b + cab + cbcaabca := b + ca

var b = 1 var c = 2 var a = b + c b = 10 console.log ( a ) // 3 (no 12 porque "= " no es un operador de asignación reactiva )// Ahora imagina que existe un operador especial "$=" que cambia el valor de una variable (ejecuta el código del lado derecho del operador y asigna el resultado a la variable del lado izquierdo) cada vez que se inicializa explícitamente, y cuando se modifican las variables referenciadas (del lado derecho del operador). var b = 1 var c = 2 var a $ = b + c b = 10 console . log ( a ) // 12

Otro ejemplo es un lenguaje de descripción de hardware como Verilog , donde la programación reactiva permite modelar los cambios a medida que se propagan a través de los circuitos.

La programación reactiva se ha propuesto como una forma de simplificar la creación de interfaces de usuario interactivas y la animación computacional casi en tiempo real .

Por ejemplo, en una arquitectura modelo-vista-controlador (MVC), la programación reactiva puede facilitar que los cambios en un modelo subyacente se reflejen automáticamente en una vista asociada . [ 3 ]

Enfoques para crear lenguajes de programación reactivos

Se emplean varios enfoques populares para crear lenguajes de programación reactivos. Un enfoque consiste en la especificación de lenguajes dedicados que se adaptan a diversas restricciones de dominio . Dichas restricciones suelen caracterizarse por el tiempo real, la computación embebida o la descripción del hardware. Otro enfoque implica la especificación de lenguajes de propósito general que incluyen soporte para la reactividad. Otros enfoques se articulan en la definición y el uso de bibliotecas de programación , o lenguajes específicos de dominio embebidos , que permiten la reactividad junto con un lenguaje de programación o sobre él. La especificación y el uso de estos diferentes enfoques dan lugar a compensaciones en las capacidades del lenguaje . En general, cuanto más restringido es un lenguaje, más información pueden proporcionar a los desarrolladores sus compiladores y herramientas de análisis (por ejemplo, al analizar si los programas pueden ejecutarse en tiempo real). Las compensaciones funcionales en la especificidad pueden resultar en un deterioro de la aplicabilidad general de un lenguaje.

Modelos de programación y semántica

La programación reactiva se rige por una variedad de modelos y semánticas . Estos se pueden dividir, a grandes rasgos, en tres dimensiones:

Técnicas y desafíos de la implementación

Esencia de las implementaciones

Los entornos de ejecución de lenguajes de programación reactiva se representan mediante un grafo que identifica las dependencias entre los valores reactivos involucrados. En dicho grafo, los nodos representan la acción de computación y las aristas modelan las relaciones de dependencia. Este entorno de ejecución utiliza dicho grafo para realizar un seguimiento de los distintos cálculos que deben ejecutarse nuevamente cuando cambia el valor de una entrada involucrada.

Algoritmos de propagación de cambios

Los métodos de propagación de datos más comunes son:

  • Pull – El consumidor de valor es proactivo , ya que consulta regularmente la fuente observada en busca de valores y reacciona cuando hay un valor relevante disponible. Esta práctica de comprobar periódicamente si hay eventos o cambios de valor se conoce comúnmente como sondeo .
  • Push : El consumidor recibe un valor de la fuente cuando este está disponible. Estos valores son autocontenidos; es decir, contienen toda la información necesaria y el consumidor no necesita consultar información adicional.
  • Modelo Push-pull : El consumidor de valor recibe una notificación de cambio , que es una breve descripción del cambio, por ejemplo, "algún valor ha cambiado": esta es la parte push . Sin embargo, la notificación no contiene toda la información necesaria (es decir, no contiene los valores reales), por lo que el consumidor necesita consultar la fuente para obtener más información (el valor específico) después de recibir la notificación: esta es la parte pull . Este método se usa comúnmente cuando hay un gran volumen de datos que podrían interesar a los consumidores. Por lo tanto, para reducir el rendimiento y la latencia, solo se envían notificaciones ligeras; luego, aquellos consumidores que necesitan más información la solicitan. Este enfoque también tiene el inconveniente de que la fuente podría verse saturada por muchas solicitudes de más información después de que se envía una notificación.

¿Qué presionar?

A nivel de implementación, la reacción a eventos consiste en la propagación a través de la información de un grafo, que caracteriza la existencia de un cambio. En consecuencia, los cálculos afectados por dicho cambio quedan obsoletos y deben marcarse para su reejecución. Estos cálculos suelen caracterizarse por el cierre transitivo del cambio (es decir, el conjunto completo de dependencias transitivas que afecta una fuente) en su fuente asociada. La propagación del cambio puede entonces dar lugar a una actualización del valor de los sumideros del grafo .

La información propagada en un grafo puede consistir en el estado completo de un nodo, es decir, el resultado del cálculo del nodo en cuestión. En estos casos, se ignora la salida anterior del nodo. Otro método implica la propagación delta, es decir, la propagación de cambios incrementales . En este caso, la información se propaga a lo largo de las aristas del grafo, que consisten únicamente en deltas que describen cómo cambió el nodo anterior. Este enfoque es especialmente importante cuando los nodos contienen grandes cantidades de datos de estado , que de otro modo serían costosos de recalcular desde cero.

La propagación delta es esencialmente una optimización que se ha estudiado ampliamente dentro de la disciplina de la computación incremental , cuyo enfoque requiere la satisfacción en tiempo de ejecución que involucra el problema de actualización de vistas . Este problema se caracteriza, de manera notoria, por el uso de entidades de base de datos , que son responsables del mantenimiento de las vistas de datos cambiantes.

Otra optimización común consiste en emplear la acumulación de cambios unarios y la propagación por lotes . Esta solución puede ser más rápida, ya que reduce la comunicación entre los nodos involucrados. Posteriormente, se pueden emplear estrategias de optimización que analicen la naturaleza de los cambios contenidos y realicen las modificaciones correspondientes (por ejemplo, dos cambios en el lote pueden anularse mutuamente y, por lo tanto, simplemente ignorarse). Otro enfoque disponible se describe como propagación de notificaciones de invalidez . Este enfoque hace que los nodos con entradas no válidas soliciten actualizaciones, lo que resulta en la actualización de sus propias salidas.

Existen dos métodos principales para construir un gráfico de dependencias :

  1. El grafo de dependencias se mantiene implícitamente dentro de un bucle de eventos . El registro de funciones de devolución de llamada explícitas genera dependencias implícitas. Por lo tanto, la inversión de control , inducida mediante la función de devolución de llamada, se mantiene. Sin embargo, para que las funciones de devolución de llamada sean funcionales (es decir, que devuelvan el valor del estado en lugar del valor de la unidad), es necesario que dichas funciones sean compositivas.
  2. Un grafo de dependencias es específico del programa y lo genera un programador. Esto facilita abordar la inversión de control de la función de devolución de llamada de dos maneras: o bien se especifica un grafo explícitamente (normalmente usando un lenguaje específico de dominio (DSL), que puede estar integrado), o bien se define un grafo implícitamente con expresión y generación usando un lenguaje arquetípico eficaz .

Desafíos de implementación en la programación reactiva

Fallos

Al propagar cambios, es posible elegir órdenes de propagación tales que el valor de una expresión no sea una consecuencia natural del programa fuente. Podemos ilustrar esto fácilmente con un ejemplo. Supongamos que secondses un valor reactivo que cambia cada segundo para representar el tiempo actual (en segundos). Consideremos esta expresión:

t = segundos + 1 g = (t > segundos) 

Como tsiempre debe ser mayor que seconds, esta expresión siempre debería evaluarse como verdadera. Desafortunadamente, esto puede depender del orden de evaluación. Cuando secondscambia, dos expresiones deben actualizarse: seconds + 1y la condicional. Si la primera se evalúa antes que la segunda, entonces esta invariante se cumplirá. Sin embargo, si la condicional se actualiza primero, usando el valor antiguo de ty el nuevo valor de seconds, entonces la expresión se evaluará como falsa. Esto se llama un fallo .

Algunos lenguajes reactivos no presentan fallos y demuestran esta propiedad. Esto se suele lograr ordenando topológicamente las expresiones y actualizando los valores en orden topológico. Sin embargo, esto puede tener implicaciones en el rendimiento, como el retraso en la entrega de valores (debido al orden de propagación). Por lo tanto, en algunos casos, los lenguajes reactivos permiten fallos, y los desarrolladores deben ser conscientes de la posibilidad de que los valores no se correspondan temporalmente con el código fuente del programa, y ​​de que algunas expresiones se evalúen varias veces (por ejemplo, se puede evaluar dos veces: una cuando llega el nuevo valor y otra cuando se actualiza).t > secondssecondst

Dependencias cíclicas

La ordenación topológica de dependencias depende de que el grafo de dependencias sea un grafo acíclico dirigido (DAG). En la práctica, un programa puede definir un grafo de dependencias con ciclos. Generalmente, los lenguajes de programación reactiva esperan que estos ciclos se rompan colocando algún elemento en una arista de retroceso para permitir que la actualización reactiva finalice. Normalmente, los lenguajes proporcionan un operador como delayel que utiliza el mecanismo de actualización para este propósito, ya que delayimplica que lo que sigue debe evaluarse en el siguiente paso temporal (lo que permite que la evaluación actual finalice).

Interacción con estado mutable

Los lenguajes reactivos suelen asumir que sus expresiones son puramente funcionales . Esto permite que un mecanismo de actualización elija diferentes órdenes para realizar las actualizaciones, dejando el orden específico sin especificar (lo que posibilita optimizaciones). Sin embargo, cuando un lenguaje reactivo se integra en un lenguaje de programación con estado, los programadores pueden realizar operaciones mutables. Cómo lograr una interacción fluida sigue siendo un problema abierto.

En algunos casos, es posible contar con soluciones parciales basadas en principios. Dos de estas soluciones son:

  • Un lenguaje puede ofrecer la noción de "celda mutable". Una celda mutable es aquella de la que el sistema de actualización reactiva tiene conocimiento, de modo que los cambios realizados en ella se propagan al resto del programa reactivo. Esto permite que la parte no reactiva del programa realice una mutación tradicional, al tiempo que permite que el código reactivo conozca y responda a esta actualización, manteniendo así la coherencia de la relación entre los valores del programa. Un ejemplo de lenguaje reactivo que proporciona dicha celda es FrTime. [ 4 ]
  • Las bibliotecas orientadas a objetos debidamente encapsuladas ofrecen una noción de estado encapsulada. En principio, por lo tanto, es posible que dicha biblioteca interactúe sin problemas con la parte reactiva de un lenguaje. Por ejemplo, se pueden instalar funciones de devolución de llamada en los métodos get de la biblioteca orientada a objetos para notificar al motor de actualización reactiva sobre los cambios de estado, y los cambios en el componente reactivo se pueden enviar a la biblioteca orientada a objetos a través de los métodos get. FrTime emplea esta estrategia. [ 5 ]

Actualización dinámica del grafo de dependencias

En algunos lenguajes reactivos, el grafo de dependencias es estático , es decir, permanece fijo durante la ejecución del programa. En otros lenguajes, el grafo puede ser dinámico , es decir, puede cambiar a medida que se ejecuta el programa. Como ejemplo sencillo, consideremos este ejemplo ilustrativo (donde secondses un valor reactivo):

t = si ((segundos mod 2) == 0): segundos + 1 demás: segundos - 1 fin t + 1 

Cada segundo, el valor de esta expresión cambia a una expresión reactiva diferente, de la cual t + 1depende. Por lo tanto, el gráfico de dependencias se actualiza cada segundo.

Permitir la actualización dinámica de dependencias proporciona una gran capacidad expresiva (por ejemplo, las dependencias dinámicas son habituales en los programas con interfaz gráfica de usuario [GUI]). Sin embargo, el motor de actualización reactiva debe decidir si reconstruir las expresiones cada vez o mantener el nodo de una expresión construido pero inactivo; en este último caso, debe asegurarse de que no participen en el cálculo cuando no deban estar activos.

Conceptos

Grados de explicitud

Los lenguajes de programación reactiva pueden abarcar desde aquellos muy explícitos, donde los flujos de datos se configuran mediante flechas, hasta aquellos implícitos, donde los flujos de datos se derivan de construcciones del lenguaje similares a las de la programación imperativa o funcional. Por ejemplo, en la programación reactiva funcional (PRF) implícita, una llamada a una función podría provocar implícitamente la construcción de un nodo en un grafo de flujo de datos. Las bibliotecas de programación reactiva para lenguajes dinámicos (como las bibliotecas "Cells" de Lisp y "Trellis" de Python) pueden construir un grafo de dependencias a partir del análisis en tiempo de ejecución de los valores leídos durante la ejecución de una función, lo que permite que las especificaciones de flujo de datos sean tanto implícitas como dinámicas.

En ocasiones, el término programación reactiva se refiere al nivel arquitectónico de la ingeniería de software, donde los nodos individuales en el grafo de flujo de datos son programas ordinarios que se comunican entre sí.

Estático o dinámico

La programación reactiva puede ser puramente estática, donde los flujos de datos se configuran de forma estática, o dinámica, donde los flujos de datos pueden cambiar durante la ejecución de un programa.

El uso de conmutadores de datos en el grafo de flujo de datos podría, hasta cierto punto, hacer que un grafo de flujo de datos estático parezca dinámico, difuminando ligeramente la distinción. Sin embargo, la programación reactiva dinámica real podría utilizar la programación imperativa para reconstruir el grafo de flujo de datos.

Programación reactiva de orden superior

Se podría decir que la programación reactiva es de orden superior si admite la idea de que los flujos de datos se pueden usar para construir otros flujos de datos. Es decir, el valor resultante de un flujo de datos es otro grafo de flujo de datos que se ejecuta utilizando el mismo modelo de evaluación que el primero.

Diferenciación del flujo de datos

Idealmente, todos los cambios de datos se propagan instantáneamente, pero esto no se puede garantizar en la práctica. En cambio, puede ser necesario asignar diferentes prioridades de evaluación a distintas partes del grafo de flujo de datos. Esto se conoce como programación reactiva diferenciada .

Por ejemplo, en un procesador de textos , la corrección ortográfica no tiene por qué estar totalmente sincronizada con la adición o inserción de caracteres. En este caso, se puede utilizar la programación reactiva diferenciada para dar menor prioridad al corrector ortográfico , permitiendo que se retrase mientras se mantienen otros flujos de datos más inmediatos.

Sin embargo, dicha diferenciación introduce una mayor complejidad en el diseño. Por ejemplo, decidir cómo definir las diferentes áreas de flujo de datos y cómo gestionar el paso de eventos entre ellas.

Modelos de evaluación de la programación reactiva

La evaluación de programas reactivos no se basa necesariamente en cómo se evalúan los lenguajes de programación basados ​​en pila. En cambio, cuando se modifica algún dato, el cambio se propaga a todos los datos que se derivan parcial o totalmente de los datos modificados. Esta propagación del cambio puede lograrse de diversas maneras, siendo quizás la más natural un esquema de invalidación/revalidación diferida.

Podría ser problemático propagar un cambio de forma ingenua usando una pila, debido a la posible complejidad de actualización exponencial si la estructura de datos tiene una forma determinada. Una de esas formas se puede describir como "forma de diamante repetida" y tiene la siguiente estructura: A n →B n →A n+1 , A n →C n →A n+1 , donde n=1,2... Este problema podría superarse propagando la invalidación solo cuando algunos datos no estén ya invalidados, y luego revalidando los datos cuando sea necesario usando evaluación perezosa .

Un problema inherente a la programación reactiva es que la mayoría de los cálculos que se evaluarían y olvidarían en un lenguaje de programación convencional, deben representarse en la memoria como estructuras de datos. Esto podría hacer que la programación reactiva consuma mucha memoria. Sin embargo, la investigación sobre lo que se denomina reducción de memoria podría solucionar este problema. [ 6 ]

Por otro lado, la programación reactiva es una forma de lo que podría describirse como "paralelismo explícito" y, por lo tanto, podría ser beneficiosa para aprovechar la potencia del hardware paralelo.

Similitudes con el patrón del observador

La programación reactiva presenta similitudes principales con el patrón observador, comúnmente utilizado en la programación orientada a objetos . Sin embargo, integrar los conceptos de flujo de datos en el lenguaje de programación facilitaría su expresión y, por lo tanto, podría aumentar la granularidad del grafo de flujo de datos. Por ejemplo, el patrón observador suele describir flujos de datos entre objetos o clases completas, mientras que la programación reactiva orientada a objetos podría centrarse en los miembros de dichos objetos o clases.

Aproches

Imperativo

Es posible fusionar la programación reactiva con la programación imperativa ordinaria . En este paradigma, los programas imperativos operan sobre estructuras de datos reactivas. [ 7 ] Esta configuración es análoga a la programación imperativa con restricciones ; sin embargo, mientras que la programación imperativa con restricciones gestiona restricciones de flujo de datos bidireccionales, la programación imperativa reactiva gestiona restricciones de flujo de datos unidireccionales. Una implementación de referencia es la extensión de tiempo de ejecución Quantum propuesta para JavaScript .

Orientado a objetos

La programación reactiva orientada a objetos (OORP) es una combinación de programación orientada a objetos y programación reactiva. Quizás la forma más natural de lograr esta combinación sea la siguiente: en lugar de métodos y campos, los objetos tienen reacciones que se reevalúan automáticamente cuando se modifican las reacciones de las que dependen.

Si un lenguaje OORP mantiene sus métodos imperativos, también entraría en la categoría de programación reactiva imperativa.

Funcional

La programación reactiva funcional (FRP) es un paradigma de programación para la programación reactiva sobre programación funcional .

Actor basado

El modelo de actores (actores) se propone para diseñar sistemas reactivos, a menudo combinado con programación reactiva funcional (FRP) y flujos reactivos para desarrollar sistemas reactivos distribuidos. [ 8 ] [ 9 ] [ 10 ] [ 11 ]

Basado en reglas

Una categoría más reciente de lenguajes de programación utiliza restricciones (reglas) como concepto principal. Consiste en reacciones a eventos que garantizan el cumplimiento de todas las restricciones. Esto no solo facilita las reacciones basadas en eventos, sino que también convierte a los programas reactivos en un elemento fundamental para la corrección del software. Un ejemplo de lenguaje de programación reactivo basado en reglas es Ampersand, que se fundamenta en el álgebra relacional . [ 12 ]

Implementaciones

  • Elm , una composición reactiva de interfaces de usuario web.
  • ObservableComputations , una implementación .NET multiplataforma .
  • Quantum JS es una extensión de tiempo de ejecución para JavaScript que incorpora la programación reactiva imperativa al lenguaje, creando una categoría completamente nueva en el espectro de la reactividad.
  • Reactive Streams , un estándar de JVM para el procesamiento asíncrono de flujos con contrapresión no bloqueante.
  • ReactiveX es una API para implementar programación reactiva con flujos, observables y operadores, con implementaciones en múltiples lenguajes, incluyendo RxJs, RxJava, Rx.NET, RxPy y RxSwift.
  • Rimmel.js , una biblioteca de interfaz de usuario JavaScript orientada a flujos, diseñada para usarse con RxJS y Observables.
  • Shiny es un framework web de código abierto desarrollado por Posit PBC que permite el desarrollo de aplicaciones web reactivas e interactivas utilizando R o Python, con diversas opciones de implementación que van desde servicios alojados en la nube hasta la contenerización local.
  • Solid.js aporta reactividad a JavaScript sin modificar la semántica de la sintaxis de JavaScript , junto con plantillas JSX reactivas.
  • Svelte aporta reactividad en forma de una sintaxis de JavaScript variante que se parece a JavaScript , pero que es reactiva de forma natural, a diferencia de JavaScript, que normalmente no lo es.

Véase también

Referencias

  1. Bainomugisha, Ingeniero; Carreton, Andoni Lombide; Van Cutsem, Tom; Mostinckx, Stijn; De Meuter, Wolfgang (2013). Un estudio sobre programación reactiva (Informe). Association for Computing Machinery (ACM) . Recuperado el 17 de mayo de 2026 .También incluye una taxonomía de los enfoques de programación reactiva existentes.
  2. Boussinot, Frederic (26 de marzo de 2007). "Programación reactiva: proyecto MIMOSA de INRIA – ENSMP" . Instituto Francés de Investigación en Ciencias de la Computación y Automatización (INRIA) . Recuperado el 17 de mayo de 2026 .Sitio web general sobre programación reactiva; consulte especialmente la sección " Descripción general ".
  3. Comunidad Trellis Tele. Modelo-vista-controlador y el patrón observador (Informe). PEAK: Python Enterprise Application Kit. Archivado del original el 3 de marzo de 2016. Consultado el 27 de julio de 2009 .
  4. "Integración de flujo de datos dinámico en un lenguaje de llamada por valor" . cs.brown.edu . Archivado del original el 18 de octubre de 2016. Consultado el 9 de octubre de 2016 .
  5. "Cruzando fronteras estatales: Adaptando marcos orientados a objetos a lenguajes reactivos funcionales" . cs.brown.edu . Archivado del original el 9 de octubre de 2016. Consultado el 9 de octubre de 2016 .
  6. Burchett, Kimberley; Cooper, Gregory H.; Krishnamurthi, Shriram (2007). "Lowering: una técnica de optimización estática para la reactividad funcional transparente". Actas del simposio ACM SIGPLAN de 2007 sobre evaluación parcial y manipulación de programas basada en semántica (PDF) . págs. 71–80 . Archivado (PDF) del original el 16 de abril de 2016. Recuperado el 8 de septiembre de 2014 . 
  7. Demetrescu, Camil; Finocchi, Irene; Ribichini, Andrea (22 de octubre de 2011). «Programación imperativa reactiva con restricciones de flujo de datos». Actas de la conferencia internacional ACM de 2011 sobre sistemas, lenguajes y aplicaciones de programación orientada a objetos . Oopsla '11. págs. 407–26 . arXiv : 1104.2293 . doi : 10.1145/2048066.2048100 . ISBN  9781450309400. S2CID 7285961 . 
  8. Van den Vonder, Sam; Renaux, Thierry; Oeyen, Bjarno; De Koster, Joeri; De Meuter, Wolfgang (2020). «Abordando el escuadrón incómodo para la programación reactiva: el modelo actor-reactor». Actas internacionales Leibniz en informática (LIPIcs) . Vol. 166. pp. 19:1–19:29. doi : 10.4230/LIPIcs.ECOOP.2020.19 . ISBN   9783959771542. S2CID 260445152 . Archivado del original el 20/10/2021 . Recuperado el 24/06/2021 . 
  9. Van den Vonder, Sam; Renaux, Thierry; De Meuter, Wolfgang (14 de junio de 2022). "Reactividad a nivel de topología en programas reactivos distribuidos: gestión reactiva de conocidos mediante Flocks". The Art, Science, and Engineering of Programming (Informe). Vol. 6. pp. 14:1–14:36. arXiv : 2202.09228 . doi : 10.22152/programming-journal.org/2022/6/14 . Archivado del original el 15 de marzo de 2023. Recuperado el 16 de marzo de 2023 .  
  10. Shibanai, Kazuhiro; Watanabe, Takuo (2018). «Programación reactiva funcional distribuida en un entorno de ejecución basado en actores». Actas del 8.º Taller Internacional ACM SIGPLAN sobre Programación Basada en Actores, Agentes y Control Descentralizado . Agere 2018. págs. 13–22 . doi : 10.1145/3281366.3281370 . ISBN  9781450360661. S2CID 53113447 . Archivado del original el 24-06-2021 . Recuperado el 24-06-2021 . 
  11. ^ Roestenburg, Raymond; Bakker, Rob; Williams, Rob (2016). "13: Transmisión". Akká en acción . Greenwich, Connecticut, EE.UU.: Manning Publications Co. p. 281.ISBN  978-1-61729-101-2.
  12. Joosten, Stef (2018). "Álgebra relacional como lenguaje de programación usando el compilador Ampersand" . Journal of Logical and Algebraic Methods in Programming . 100 : 113–29 . doi : 10.1016/j.jlamp.2018.04.002 . S2CID 52932824 .