Julia: Sintaxis alternativa para `map(func, x)`

Creado en 23 sept. 2014  ·  283Comentarios  ·  Fuente: JuliaLang/julia

Esto fue discutido en detalle aquí . Estaba teniendo problemas para encontrarlo, y pensé que merecía su propio número.

breaking speculative

Comentario más útil

Una vez que un miembro del triunvirato (Stefan, Jeff, Viral) fusione #15032 (que creo que está listo para fusionarse), cerraré esto y presentaré un problema de hoja de ruta para delinear los cambios propuestos restantes: corregir el cálculo de tipo de transmisión, obsoleto @vectorize , convierta .op en azúcar de transmisión, agregue "fusión de transmisión" a nivel de sintaxis y finalmente fusione con la asignación en el lugar. Los dos últimos probablemente no lleguen a 0.5.

Todos 283 comentarios

+1

O func.(args...) como azúcar sintáctico para

broadcast(func, args...)

¿Pero tal vez soy el único que preferiría eso?
De cualquier manera, +1.

:-1: En todo caso, creo que la otra sugerencia de Stefan de f[...] tiene una buena similitud con las comprensiones.

Al igual que @ihnorton , tampoco me gusta mucho esta idea. En particular, no me gusta la asimetría de tener a .+ b y sin.(a) .

Tal vez no necesitemos una sintaxis especial. Con #1470, podríamos hacer algo como

call(f::Callable,x::AbstractArray) = applicable(f,x) ? apply(f,x) : map(f,x)

¿Correcto? Sin embargo, tal vez esto sería demasiado mágico para obtener un mapa automático en cualquier función.

@quinnj Esa línea resume mis mayores temores acerca de permitir la sobrecarga de llamadas. No podré dormir durante días.

Todavía no estoy seguro de que sea sintácticamente posible, pero ¿qué pasa con .sin(x) ? ¿Es más parecido a a .+ b ?

Creo que [] se está sobrecargando demasiado y no funcionará para este propósito. Por ejemplo, probablemente podamos escribir Int(x) , pero Int[x] construye una matriz y, por lo tanto, no puede significar map .

Estaría a bordo con .sin(x) .

Tendríamos que recuperar algo de sintaxis para eso, pero si Int(x) es la versión escalar, entonces Int[x] es razonable por analogía para construir un vector de tipo de elemento Int . En mi opinión, la oportunidad de hacer que la sintaxis sea más coherente es en realidad uno de los aspectos más atractivos de la propuesta f[v] .

¿Cómo hace que la sintaxis f[v] para map haga que la sintaxis sea más coherente? No entiendo. map tiene una "forma" diferente a la sintaxis actual del constructor de matrices T[...] . ¿Qué tal Vector{Int}[...] ? ¿No funcionaría eso?

lol, perdón por el susto @JeffBezanson! Jaja, la sobrecarga de llamadas definitivamente da un poco de miedo, de vez en cuando, pienso en los tipos de ofuscación de código que puedes hacer en julia y con call , podrías hacer algunas cosas complicadas.

Creo que .sin(x) también suena como una buena idea. ¿Hubo consenso sobre qué hacer con argumentos múltiples?

:-1:. Guardar un par de caracteres en comparación con el uso de funciones de orden superior, no creo que valga la pena el costo en legibilidad. ¿Te imaginas un archivo con .func() / func.() y func() intercalados por todas partes?

Parece probable que eliminemos la sintaxis a.(b) todos modos, al menos.

¡Vaya, habla de revolver un nido de abejas! Cambié el nombre para reflejar mejor la discusión.

También podríamos cambiar el nombre de 2 argumentos map a zipWith :)

Si alguna sintaxis es realmente necesaria, ¿qué tal [f <- b] u otro juego de palabras con las comprensiones _dentro_ de los corchetes?

( @JeffBezanson simplemente tiene miedo de que alguien vaya a escribir CJOS o Moose.jl :) ... si obtenemos esa característica, simplemente póngala en la sección Don't do stupid stuff: I won't optimize that del manual)

Actualmente, escribir Int[...] indica que está construyendo una matriz de tipo de elemento Int . Pero si Int(x) significa convertir x en Int aplicando Int como una función, entonces también podría considerar que Int[...] signifique "aplicar Int a cada cosa en ...", oh, que por cierto produce valores de tipo Int . Entonces, escribir Int[v] sería equivalente a [ Int(x) for x in v ] y Int[ f(x) for x in v ] sería equivalente a [ Int(f(x)) for x in v ] . Por supuesto, entonces ha perdido parte de la utilidad de escribir Int[ f(x) for x in v ] en primer lugar, es decir, que podemos saber estáticamente que el tipo de elemento es Int , pero si se aplica Int(x) debe producir un valor de tipo Int (no es una restricción irrazonable), entonces podríamos recuperar esa propiedad.

Me parece más vectorización/infierno de gatos implícitos. ¿Qué haría Int[x, y] ? O peor, Vector{Int}[x] ?

No estoy diciendo que sea la mejor idea ni siquiera que la defienda; solo estoy señalando que no choca _completamente_ con el uso existente, que en sí mismo es un poco complicado. Si pudiéramos hacer que el uso existente sea parte de un patrón más coherente, sería una victoria. No estoy seguro de qué significaría f[v,w] : las opciones obvias son [ f(x,y) for x in v, y in w ] o map(f,v,w) pero aún hay más opciones.

Siento que a.(b) apenas se usa. Realizó una prueba rápida y se usa solo en 54 de los aproximadamente 4000 archivos fuente de julia disponibles: https://gist.github.com/jakebolewski/104458397f2e97a3d57d.

Creo que choca completamente. T[x] tiene la "forma" T --> Array{T} , mientras que map tiene la forma Array{T} --> Array{S} . Esos son bastante incompatibles.

Para hacer esto, creo que tendríamos que renunciar a T[x,y,z] como constructor por Vector{T} . La indexación de matriz simple y antigua, A[I] donde I es un vector, se puede ver como map(i->A[i], I) . "Aplicar" una matriz es como aplicar una función (por supuesto, matlab incluso usa la misma sintaxis para ellos). En ese sentido, la sintaxis realmente funciona, pero perderíamos la sintaxis de vector tipado en el proceso.

Siento que debatir la sintaxis aquí distrae del cambio más importante: hacer map rápido.

Obviamente, hacer que map sea rápido (que, por cierto, debe ser parte de un rediseño bastante completo de la noción de funciones en julia) es más importante. Sin embargo, pasar de sin(x) a map(sin, x) es muy importante desde la perspectiva de la usabilidad, por lo que para eliminar realmente la vectorización, la sintaxis es muy importante.

Sin embargo, pasar de sin(x) a map(sin, x) es muy importante desde la perspectiva de la usabilidad, por lo que para eliminar realmente la vectorización, la sintaxis es muy importante.

Totalmente de acuerdo.

Estoy de acuerdo con @JeffBezanson en que f[x] es bastante irreconciliable con las construcciones de matrices tipeadas actuales Int[x, y] etc.

Otra razón para preferir .sin a sin. es finalmente permitir usar, por ejemplo Base.(+) para acceder a la función + en Base (una vez que a.(b) se elimina).

Cuando un módulo define su propia función sin (o lo que sea) y queremos usar esa función en un vector, ¿hacemos Module..sin(v) ? Module.(.sin(v)) ? Module.(.sin)(v) ? .Module.sin(v) ?

Ninguna de estas opciones parece realmente buena ya.

Siento que esta discusión pierde la esencia del problema. Es decir: al asignar funciones de argumento único a contenedores, siento que la sintaxis map(func, container) es _ya_ clara y concisa. En cambio, es solo cuando se trata de múltiples argumentos que creo que podríamos beneficiarnos de una mejor sintaxis para curry.

Tome por ejemplo la verbosidad de map(x->func(x,other,args), container) , o encadene una operación de filtro para empeorarla filter(x->func2(x[1]) == val, map(x->func1(x,other,args), container)) .

En estos casos, creo que una sintaxis de mapa abreviada no ayudaría mucho. No es que crea que estos son particularmente malos, pero a) no creo que una abreviatura de map ayudaría mucho yb) me encanta anhelar algo de la sintaxis de Haskell. ;)

IIRC, en Haskell, lo anterior se puede escribir filter ((==val) . func2 . fst) $ map (func1 other args) container con un ligero cambio en el orden de los argumentos a func1 .

En elm .func está definido por x->x.func y esto es muy útil, consulte los registros de elm . Esto debe tenerse en cuenta antes de tomar esta sintaxis para map .

Me gusta eso.

Aunque el acceso al campo no es tan importante en Julia como en muchos idiomas.

Sí, se siente menos relevante aquí ya que los campos en Julia son más para uso "privado". Pero con la discusión en curso sobre la sobrecarga del acceso al campo, eso puede volverse más sensato.

f.(x) parece la solución menos problemática, si no fuera por la asimetría con .+ . Pero mantener la asociación simbólica de . con la "operación por elementos" es una buena idea en mi humilde opinión.

Si la construcción de matrices con tipo actual puede quedar obsoleta, entonces func[v...] se puede traducir a map(func, v...) , y las matrices literales se pueden escribir T[[a1, ..., an]] (en lugar de T[a1, ..., an] actual

También encuentro que sin∘v es bastante natural (cuando se ve una matriz v en una aplicación desde índices hasta valores contenidos), o más simplemente sin*v o v*[sin]' ( lo que requiere definir *(x, f::Callable) ), etc.

Volviendo a este problema con una mente fresca, me di cuenta de que f.(x) puede verse como una sintaxis bastante natural. En lugar de leerlo como f. y ( , puede leerlo como f y .( . .( es entonces, metafóricamente, una versión basada en elementos del operador de llamada de función ( , que es totalmente consistente con .+ y amigos.

La idea de que .( sea un operador de llamada de función me entristece mucho.

@johnmyleswhite ¿Te importa elaborar? Hablaba de la intuición de la sintaxis, o de su consistencia visual con el resto del lenguaje, no de la implementación técnica en absoluto.

Para mí, ( no es parte de la semántica del lenguaje en absoluto: es solo parte de la sintaxis. Así que no me gustaría tener que inventar una manera para que .( y ( comiencen a diferir. ¿El primero genera multicall Expr en lugar de call Expr ?

No. Como dije, no estaba insinuando en absoluto que debería haber dos operadores de llamadas diferentes. Solo trato de encontrar una sintaxis _visualmente_ consistente para las operaciones con elementos.

Para mí, lo que mata estas opciones es la cuestión de cómo vectorizar funciones de múltiples argumentos. No hay una sola manera de hacerlo y cualquier cosa que sea lo suficientemente general como para admitir todas las formas posibles comienza a parecerse mucho a las comprensiones de matrices multidimensionales que ya tenemos.

Es bastante estándar para múltiples argumentos map iterar sobre todos los argumentos.
Si hiciéramos esto, haría .( sintaxis para una llamada al mapa. Esa sintaxis podría
no ser tan bueno por varias razones, pero estaría bien con estos aspectos.

El hecho de que sean posibles varias generalizaciones para funciones de argumentos múltiples no puede ser un argumento en contra de admitir al menos algunos casos especiales, al igual que la transposición de matrices es útil incluso si se puede generalizar de varias maneras para los tensores.

Solo tenemos que elegir la solución más útil. Las opciones posibles ya se han discutido aquí: https://github.com/JuliaLang/julia/issues/8389#issuecomment -55953120 (y los siguientes comentarios). Como dijo @JeffBezanson , el comportamiento actual de map es razonable. Un criterio interesante es poder reemplazar @vectorize_2arg .

Mi punto es que tener sin.(x) y x .+ y coexistiendo es incómodo. Prefiero tener .sin(x) -> map(sin, x) y x .+ y -> map(+, x, y) .

.+ en realidad usa broadcast .

Algunas otras ideas, por pura desesperación:

  1. Dos puntos de sobrecarga, sin:x . No generaliza bien a múltiples argumentos.
  2. sin.[x] --- esta sintaxis está disponible, actualmente sin sentido.
  3. sin@x --- no tan disponible, pero tal vez posible

Realmente no estoy convencido de que necesitemos esto.

Yo tampoco. Creo que f.(x) es la mejor opción aquí, pero no me encanta.

Pero sin esto, ¿cómo podemos evitar crear todo tipo de funciones vectorizadas, en particular cosas como int() ? Esto es lo que me impulsó a iniciar esta discusión en https://github.com/JuliaLang/julia/issues/8389.

Deberíamos alentar a las personas a usar map(func, x) . No es tanto escribir y es inmediatamente claro para cualquiera que venga de otro idioma.

Y, por supuesto, asegúrese de que sea rápido.

Sí, pero para uso interactivo lo encuentro muy doloroso. Eso va a ser una gran molestia para las personas que vienen de R (al menos, no sé sobre otros idiomas), y no me gustaría que esto dé la sensación de que Julia no es adecuada para el análisis de datos.

Otro problema es la consistencia: a menos que esté dispuesto a eliminar todas las funciones vectorizadas actualmente, incluidas log , exp , etc., y pida a las personas que usen map en su lugar (que podría ser una prueba interesante de la practicidad de esta decisión), el lenguaje va a ser inconsistente, lo que dificulta saber de antemano si una función está vectorizada o no (y con qué argumentos).

Muchos otros idiomas han estado usando map durante años.

Como entendí el plan para dejar de vectorizar todo, eliminar la mayoría o todas las funciones vectorizadas siempre fue parte de la estrategia.

Sí, por supuesto que dejaríamos de vectorizarlo todo. La inconsistencia ya está ahí: ya es difícil saber qué funciones son o deberían ser vectorizadas, ya que no hay una razón realmente convincente por la que sin , exp etc., deban mapearse implícitamente sobre arreglos.

Y decirle a los escritores de bibliotecas que pongan @vectorize en todas las funciones apropiadas es una tontería; deberías poder simplemente escribir una función, y si alguien quiere calcularla para cada elemento, usa map .

Imaginemos lo que sucede si eliminamos las funciones matemáticas vectorizadas de uso común:

  1. Personalmente, no me importa escribir map(exp, x) en lugar de exp(x) , aunque este último es un poco más corto y limpio. Sin embargo, existe una _gran_ diferencia en el rendimiento. La función vectorizada es aproximadamente 5 veces más rápida que el mapa de mi máquina.
  2. Cuando trabajas con expresiones compuestas, el problema es más interesante. Considere una expresión compuesta: exp(0.5 * abs2(x - y)) , entonces tenemos
# baseline: the shortest way
exp(0.5 * abs2(x - y))    # ... takes 0.03762 sec (for 10^6 elements)

# using map (very cumbersome for compound expressions)
map(exp, 0.5 * map(abs2, x - y))   # ... takes 0.1304 sec (about 3.5x slower)

# using anonymous function (shorter for compound expressions)
map((u, v) -> 0.5 * exp(abs2(u - v)), x, y)   # ... takes 0.2228 sec (even slower, about 6x baseline)

# using array comprehension (we have to deal with two array arguments)

# method 1:  using zip to combine the arguments (readability not bad)
[0.5 * exp(abs2(u - v)) for (u, v) in zip(x, y)]  # ... takes 0.140 sec, comparable to using map

# method 2:  using index, resulting in a slightly longer statement
[0.5 * exp(abs2(x[i] - y[i])) for i = 1:length(x)]  # ... takes 0.016 sec, 2x faster than baseline 

Si vamos a eliminar las funciones matemáticas vectorizadas, la única forma aceptable tanto en legibilidad como en rendimiento parece ser la comprensión de arreglos. Aún así, no son tan convenientes como las matemáticas vectorizadas.

-1 para eliminar versiones vectorizadas. De hecho, las bibliotecas como VML y Yeppp ofrecen un rendimiento mucho mayor para las versiones vectorizadas y debemos descubrir cómo aprovecharlas.

Si estos están en la base o no, es una discusión diferente y una discusión más amplia, pero la necesidad es real y el rendimiento puede ser mayor que el que tenemos.

@lindahua y @ViralBShah : Algunas de sus preocupaciones parecen basarse en la suposición de que nos desharemos de las funciones vectorizadas antes de realizar mejoras en map , pero no creo que nadie haya propuesto hacer eso.

Creo que el ejemplo de @lindahua es bastante revelador: la sintaxis vectorizada es mucho mejor y mucho más cercana a la fórmula matemática que las otras soluciones. Sería bastante malo perder eso, y las personas que provienen de otros lenguajes científicos probablemente considerarán esto como un punto negativo en Julia.

Estoy de acuerdo con eliminar todas las funciones vectorizadas (cuando map es lo suficientemente rápido), y ver cómo funciona. Creo que el interés de proporcionar una sintaxis de conveniencia será aún más visible en ese momento, y aún será el momento de agregarla si resulta que es el caso.

Creo que Julia es diferente de muchos otros lenguajes porque enfatiza el uso interactivo (las expresiones más largas son molestas de escribir en ese caso) y los cálculos matemáticos (las fórmulas deben ser lo más parecidas posible a las expresiones matemáticas para que el código sea legible). Es en parte por eso que Matlab, R y Numpy ofrecen funciones vectorizadas (la otra razón es, por supuesto, el rendimiento, un problema que puede desaparecer en Julia).

Mi sensación de la discusión es que se subestima la importancia de las matemáticas vectorizadas. En realidad, una de las principales ventajas de las matemáticas vectorizadas radica en la concisión de la expresión: es mucho más que un recurso provisional para "lenguajes con bucle for lento".

Comparando y = exp(x) y

for i = 1:length(x)
    y[i] = exp(x[i])
end

El primero es obviamente mucho más conciso y legible que el segundo. El hecho de que Julia haga que los bucles sean eficientes no significa que siempre debamos desvectorizar los códigos, lo que, para mí, es bastante contraproducente.

Deberíamos animar a la gente a escribir códigos de forma natural. Por un lado, esto significa que no debemos tratar de escribir códigos intrincados y torcer funciones vectorizadas en un contexto en el que no encajan; por otro lado, deberíamos apoyar el uso de matemáticas vectorizadas siempre que tengan más sentido.

En la práctica, la asignación de fórmulas a matrices de números es una operación muy común, y debemos esforzarnos por hacer que esto sea conveniente en lugar de engorroso. Para ello, los códigos vectorizados siguen siendo la forma más natural y concisa. Aparte del rendimiento, siguen siendo mejores que llamar a la función map , especialmente para expresiones compuestas con múltiples argumentos.

Ciertamente no queremos versiones vectorizadas de todo, pero usar el mapa cada vez para vectorizar sería molesto por las razones que Dahua acaba de mencionar anteriormente.

Si el mapa fuera rápido, sin duda nos permitiría centrarnos en tener un conjunto más pequeño y significativo de funciones vectorizadas.

Debo decir que estoy totalmente del lado de apoyar una notación de mapa concisa. Siento que es el mejor compromiso entre las diferentes necesidades.

No me gustan las funciones vectorizadas. Oculta lo que está pasando. Este hábito de crear versiones vectorizadas de funciones conduce a un código misterioso. Supongamos que tiene un código en el que se llama a una función f de un paquete en un vector. Incluso si tiene alguna idea de lo que hace la función, no puede estar seguro al leer el código si lo hace por elementos o si funciona en el vector como un todo. Si los lenguajes de computación científica no tuvieran un historial de tener estas funciones vectorizadas, no creo que las aceptemos ahora.

También conduce a la situación en la que se le anima implícitamente a escribir versiones vectorizadas de funciones para habilitar el código conciso donde se usan las funciones.

El código que es más explícito sobre lo que está pasando es el bucle, pero como dice @lindahua , termina siendo muy detallado, lo que tiene sus propias desventajas, especialmente en un lenguaje que también está diseñado para uso interactivo.

Esto lleva al compromiso map , que creo que está más cerca del ideal, pero aún estoy de acuerdo con @lindahua en que no es lo suficientemente conciso.

Donde no estaré de acuerdo con @lindahua es que las funciones vectorizadas son la mejor opción, por las razones que mencioné anteriormente. A lo que conduce mi razonamiento es que Julia debería tener una notación muy concisa para map .

Me parece muy atractivo cómo Mathematica lo hace con su notación abreviada. La notación abreviada para aplicar una función a un argumento en Mathematica es @ , por lo que Apply la función f a un vector como: f @ vector . La notación abreviada relacionada para mapear una función es /@ , por lo que asigna f al vector como: f /@ vector . Esto tiene varias características atractivas. Ambas abreviaturas son concisas. El hecho de que ambos usen el símbolo @ enfatiza que existe una relación entre lo que hacen, pero el / en el mapa aún lo distingue visualmente para que quede claro cuándo está mapeando y cuándo no lo son Esto no quiere decir que Julia deba copiar a ciegas la notación de Mathematica, solo que una buena notación para el mapeo es increíblemente valiosa

No estoy sugiriendo deshacerse de todas las funciones vectorizadas. Ese tren hace mucho que salió de la estación. Más bien, sugiero mantener la lista de funciones vectorizadas lo más corta posible y proporcionar una buena notación de mapa concisa para desalentar la adición a la lista de funciones vectorizadas.

Todo esto está, por supuesto, condicionado a que map y las funciones anónimas sean rápidas. En este momento, Julia está en una posición extraña. Solía ​​ser el caso en los lenguajes de computación científica que las funciones se vectorizaban porque los bucles eran lentos. Esto no es un problema. En cambio, en Julia, vectorizó funciones porque el mapa y las funciones anónimas son lentas. Así que estamos de vuelta donde empezamos, pero por diferentes razones.

Las funciones de biblioteca vectorizadas tienen una desventaja: solo están disponibles las funciones proporcionadas explícitamente por la biblioteca. Es decir, por ejemplo, sin(x) es rápido cuando se aplica a un vector, mientras que sin(2*x) es repentinamente mucho más lento ya que requiere una matriz intermedia que debe recorrerse dos veces (primero escribiendo, luego leyendo).

Una solución sería una biblioteca de funciones matemáticas vectorizables. Estas serían implementaciones de sin , cos , etc. que están disponibles para LLVM para insertarlas. LLVM podría luego vectorizar este bucle y, con suerte, conducir a un código muy eficiente. Yeppp parece tener los núcleos de bucle correctos, pero no parece exponerlos para insertarlos.

Otro problema con la vectorización como paradigma es que simplemente no funciona en absoluto si usa tipos de contenedor distintos a los bendecidos por la biblioteca estándar. Puede ver esto en DataArrays: hay una cantidad ridícula de código de metaprogramación que se usa para volver a vectorizar funciones para los nuevos tipos de contenedores que estamos definiendo.

Combina eso con el punto de @eschnett y obtienes:

  • Las funciones vectorizadas solo funcionan si se restringe a las funciones de la biblioteca estándar
  • Las funciones vectorizadas solo funcionan si se restringe a los tipos de contenedores en la biblioteca estándar

Quiero aclarar que mi punto no es que siempre debamos mantener las funciones vectorizadas, sino que necesitamos una forma que sea tan concisa como escribir funciones vectorizadas. Usar map probablemente no satisfaga esto.

Me gusta la idea de @eschnett de etiquetar algunas funciones como _vectorizable_, y el compilador puede asignar automáticamente una función vectorizable a una matriz sin requerir que los usuarios definan explícitamente una versión vectorizada. El compilador también puede fusionar una cadena de funciones vectorizables en un bucle fusionado.

Esto es lo que tengo en mente, inspirado en los comentarios de @eschnett :

# The <strong i="11">@vec</strong> macro tags the function that follows as vectorizable
<strong i="12">@vec</strong> abs2(x::Real) = x * x
<strong i="13">@vec</strong> function exp(x::Real) 
   # ... internal implementation ...
end

exp(2.0)  # simply calls the function

x = rand(100);
exp(x)    # maps exp to x, as exp is tagged as vectorizable

exp(abs2(x))  # maps v -> exp(abs2(v)), as this is applying a chain of vectorizable functions

El compilador también puede volver a vectorizar el cálculo (a un nivel inferior) al identificar la oportunidad de usar SIMD.

Por supuesto, @vec debe estar disponible para el usuario final, de modo que las personas puedan declarar sus propias funciones como vectorizables.

Gracias, @lindahua : tu aclaración ayuda mucho.

¿Sería @vec diferente de declarar una función @pure ?

@vec indica que la función se puede mapear de forma elemental.

No todas las funciones puras entran en esta categoría, por ejemplo, sum es una función pura, y no creo que sea recomendable declararla como _vectorizable_.

¿No se podría volver a derivar la propiedad vec de sum de las etiquetas pure y associative en + junto con el conocimiento sobre cómo reduce / foldl / foldr funcionan cuando se les dan las funciones pure y associative ? Obviamente, todo esto es hipotético, pero si Julia hiciera todo lo posible por los rasgos de los tipos, me imagino que mejoraría sustancialmente el estado del arte de la vectorización al dedicarse también a los rasgos de las funciones.

Siento que agregar una nueva sintaxis es lo contrario de lo que queremos (después de limpiar la sintaxis especial para Any[] y Dict). El _punto_ completo de eliminar estas funciones vectorizadas es reducir los casos especiales (y no creo que la sintaxis deba ser diferente a la semántica de funciones). Pero estoy de acuerdo en que un mapa conciso sería útil.

Entonces, ¿por qué no agregar un infijo conciso operador map ? Aquí elegiré $ arbitrariamente. Esto haría que el ejemplo de @lindahua pasara de

exp(0.5 * abs2(x - y))

para

exp $ (0.5 * abs2 $ (x-y))

Ahora, si solo tuviéramos soporte similar al de Haskell para operadores infijos definidos por el usuario, esto solo sería un cambio de una línea ($) = map . :)

En mi opinión, las otras propuestas de sintaxis están demasiado cerca visualmente de la sintaxis existente y requerirían un mayor esfuerzo mental para analizarlas al mirar el código:

  • foo.(x) -- visualmente similar al acceso de miembro de tipo estándar
  • foo[x]: ¿estoy accediendo al miembro x-th de la matriz foo o llamando al mapa aquí?
  • .foo(x) -- tiene problemas como señaló @kmsquire

Y siento que la solución @vec está demasiado cerca de los @vectorize que estamos tratando de evitar en primer lugar. Ciertamente, sería bueno tener algunas anotaciones ala # 8297 y podrían ayudar a un compilador más inteligente en el futuro a reconocer estas oportunidades de fusión de secuencias y optimizar en consecuencia. Pero no me gusta la idea de forzarlo.

El mapa Infix más las funciones anónimas rápidas también podrían ayudar con la creación de temporales si le permitieran hacer algo como:

(x, y) -> exp(0.5 * abs2(x - y)) $ x, y

Me pregunto si la idea del nuevo y genial Trait.jl puede tomarse prestada en el contexto de designar una función como vectorizable. Por supuesto, en este caso estamos viendo _instancias_ individuales del tipo Function que pueden ser vectorizables o no, en lugar de que un tipo julia tenga un rasgo específico.

Ahora, si solo tuviéramos soporte similar al de Haskell para operadores infijos definidos por el usuario

6582 #6929 ¿No es suficiente?

Hay un punto en esta discusión sobre la vectorización de expresiones completas con la menor cantidad posible de arreglos temporales. Los usuarios que desean una sintaxis vectorizada no querrán solo un exp(x) vectorizado; querrían escribir expresiones como

y =  √π exp(-x^2) * sin(k*x) + im * log(x-1)

y haz que se vectorice mágicamente

No sería necesario marcar las funciones como "vectorizables". Esta es más bien una propiedad de cómo se implementan las funciones y están disponibles para Julia. Si se implementan, por ejemplo, en C, entonces deben compilarse en el código de bytes LLVM (no en archivos de objetos) para que el optimizador LLVM aún pueda acceder a ellos. Implementarlos en Julia también funcionaría.

Vectorizabilidad significa que uno implementa la función de una manera que está muy bien descrita por el proyecto Yeppp: sin ramas, sin tablas, división o raíz cuadrada si están disponibles como instrucciones vectoriales en hardware, y de lo contrario muchas operaciones fusionadas de multiplicación y suma y vector. fusionar operaciones.

Desafortunadamente, tales implementaciones dependerán del hardware, es decir, uno puede tener que elegir diferentes algoritmos o diferentes implementaciones dependiendo de qué instrucciones de hardware sean eficientes. He hecho esto en el pasado (https://bitbucket.org/eschnett/vecmathlib/wiki/Home) en C++, y con un público objetivo ligeramente diferente (operaciones basadas en plantillas que se vectorizan manualmente en lugar de una vectorización automática compilador).

Aquí en Julia, las cosas serían más fáciles ya que (a) sabemos que el compilador será LLVM y (b) podemos implementar esto en Julia en lugar de C++ (macros frente a plantillas).

Hay otra cosa a considerar: si uno renuncia a partes del estándar IEEE, entonces puede mejorar en gran medida la velocidad. Muchos usuarios saben que, por ejemplo, los números desnormalizados no son importantes, o que la entrada siempre será menor que sqrt(max(Double)) , etc. La pregunta es si ofrecer rutas rápidas para estos casos. Sé que estaré muy interesado en esto, pero otros pueden preferir resultados exactamente reproducibles en su lugar.

Déjame cocinar un prototipo vectorizable exp en Julia. Entonces podemos ver cómo LLVM hace vectorizar un bucle y qué velocidades obtenemos.

¿Es demasiado aterrador usar paréntesis de ancho completo alrededor del argumento de la función?

Lo siento, no me di cuenta de que estaba repitiendo exactamente lo mismo que @johnmyleswhite estaba hablando arriba sobre la función con el rasgo. Seguir adelante.

@eschnett No creo que sea razonable vincular la API (ya sea que las funciones sean vectorizables o no) con los detalles de implementación (cómo se compila la función). Suena bastante complejo de entender y mantener estable en el tiempo y entre arquitecturas, y no funcionaría al llamar a funciones en bibliotecas externas, por ejemplo, log no se detectaría como vectorizable ya que llama a una función desde openlibm.

OTOH La idea de @johnmyleswhite de usar rasgos para comunicar cuáles son las propiedades matemáticas de una función podría ser una gran solución. (La propuesta de @lindahua es una característica que sugerí en algún lugar hace un tiempo, pero la solución de usar rasgos puede ser aún mejor).

Ahora, si solo tuviéramos soporte similar al de Haskell para operadores infijos definidos por el usuario

6582 #6929 ¿No es suficiente?

Debería haber dicho: ... operadores infijos _no Unicode_ definidos por el usuario, ya que no creo que queramos exigir a los usuarios que escriban caracteres Unicode para acceder a una funcionalidad tan básica. Aunque veo que $ es en realidad uno de los agregados, ¡así que gracias por eso! Wow, entonces esto realmente funciona en Julia _hoy_ (incluso si no es "rápido"... todavía):

julia> ($) = map
julia> sin $ (0.5 * (abs2 $ (x-y)))

No sé si es la mejor opción para map pero usar $ para xor realmente parece un desperdicio. Bitwise xor no se usa tan a menudo. map es mucho más importante.

El punto anterior de @jiahao es muy bueno: las funciones vectorizadas individuales como exp son en realidad una especie de truco para obtener _expresiones_ vectorizadas como exp(-x^2) . La sintaxis que hace algo como @devec sería realmente valiosa: obtendría un rendimiento desvectorizado más la generalidad de no necesitar identificar funciones individualmente como vectorizadas.

La capacidad de usar rasgos de función para esto sería genial, pero aún lo encuentro menos satisfactorio. Lo que realmente sucede en general es que una persona escribe una función y otra la itera.

Acepto que esto no es una propiedad de la función, es una propiedad del uso de la función. La discusión sobre la aplicación de rasgos parece un caso de ladrar al árbol equivocado.

Lluvia de ideas: ¿qué tal si marca los argumentos que desea mapear para que admita el mapeo de argumentos múltiples?

a = split("the quick brown")
b = split("fox deer bear")
c = split("jumped over the lazy")
d = split("dog cat")
e = string(a, " ", b., " ", c, " ", d.) # -> 3x2 Vector{String} of the combinations   
# e[1,1]: """["the","quick", "brown"] fox ["jumped","over","the","lazy"] dog"""

No estoy seguro de si .b o b. es mejor para mostrar que desea que se mapee. Me gusta devolver un resultado multidimensional de 3x2 en este caso, ya que representa la forma del ping map .

Cañada

Aquí https://github.com/eschnett/Vecmathlib.jl hay un repositorio con una muestra
implementación de exp , escrito de una manera que LLVM puede optimizar.
Esta implementación es aproximadamente el doble de rápida que la estándar exp
implementación en mi sistema. (Probablemente) aún no alcanza la velocidad de Yeppp,
probablemente porque LLVM no desenrolla el bucle SIMD respectivo como
agresivamente como Yeppp. (Comparé las instrucciones desmontadas.)

Escribir una función vectorizable exp no es fácil. Usarlo se ve así:

function kernel_vexp2{T}(ni::Int, nj::Int, x::Array{T,1}, y::Array{T,1})
    for j in 1:nj
        <strong i="16">@simd</strong> for i in 1:ni
            <strong i="17">@inbounds</strong> y[i] += vexp2(x[i])
        end
    end
end

donde el bucle j y los argumentos de la función están ahí solo para
propósitos de evaluación comparativa.

¿Hay una macro de @unroll para Julia?

-erik

El domingo 2 de noviembre de 2014 a las 8:26 p. m., Tim Holy [email protected] escribió:

Estoy de acuerdo en que esto no es una propiedad de la función, es una propiedad de
el uso de la función. La discusión sobre la aplicación de rasgos parece una
caso de ladrar al árbol equivocado.

Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61433026.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

funciones vectorizadas individuales como exp son en realidad una especie de truco para obtener _expresiones_ vectorizadas como exp(-x^2)

La sintaxis central para extraer expresiones completas del dominio escalar sería muy interesante. La vectorización es solo un ejemplo (donde el dominio objetivo son los vectores); otro caso de uso interesante sería elevarse al dominio de la matriz (#5840) donde la semántica es bastante diferente. En el dominio de la matriz, también sería útil explorar cómo podría funcionar el envío en diferentes expresiones, ya que en el caso general querrías Schur-Parlett y otros algoritmos más especializados si quisieras algo más simple como sqrtm . (Y con una sintaxis inteligente, podría deshacerse de las funciones *m por completo - expm , logm , sqrtm , ...)

¿Hay una macro @unroll para Julia?

usando Base.Cartesiano
@nexpr 4 d->(y[i+d] = exp(x[i+d])

(Consulte http://docs.julialang.org/en/latest/devdocs/cartesian/ si tiene preguntas).

@jiahao Generalizar esto a funciones matriciales suena como un desafío interesante, pero mi conocimiento al respecto es casi nulo. ¿Tienes alguna idea sobre cómo funcionaría? ¿Cómo se articularía eso con la vectorización? ¿Cómo permitiría la sintaxis marcar la diferencia entre aplicar exp elemento-sabio en un vector/matriz y calcular su matriz exponencial?

@timholy : ¡Gracias! No pensé en usar cartesiano para desenrollar.

Desafortunadamente, el código producido por @nexprs (o por desenrollado manual) ya no está vectorizado. (Esto es LLVM 3.3, tal vez LLVM 3.5 sería mejor).

Re: desenrollar, vea también la publicación de @toivoh sobre julia-users . También puede valer la pena darle una oportunidad al #6271.

@nalimilan Todavía no he pensado en esto, pero escalar-> el levantamiento de matriz sería bastante simple de implementar con una sola función matrixfunc (digamos). Una sintaxis hipotética (completamente inventando algo) podría ser

X = randn(10,10)
c = 0.7
lift(x->exp(c*x^2)*sin(x), X)

que sería entonces

  1. identificar los dominios de origen y de destino del aumento de X que son del tipo Matrix{Float64} y tienen elementos (parámetro de tipo) Float64 (definiendo así implícitamente un aumento de Float64 => Matrix{Float64} ) , luego
  2. llame a matrixfunc(x->exp(c*x^2)*sin(x), X) para calcular el equivalente de expm(c*X^2)*sinm(X) , pero evitando la multiplicación de matrices.

En algún otro código X podría ser un Vector{Int} y el levantamiento implícito sería de Int a Vector{Int} , y luego lift(x->exp(c*x^2)*sin(x), X) podría entonces llama a map(x->exp(c*x^2)*sin(x), X) .

Uno podría imaginar también otros métodos que especificaran explícitamente los dominios de origen y de destino, por ejemplo, lift(Number=>Matrix, x->exp(c*x^2)*sin(x), X) .

@nalimilan La vectorización no es realmente una propiedad de la API. Con la tecnología de compilación actual, una función solo se puede vectorizar si está en línea. Las cosas dependen principalmente de la implementación de la función: si está escrita de la "manera correcta", entonces el compilador puede vectorizarla (después de insertarla en un bucle circundante).

@eschnett : ¿Está usando el mismo significado de vectorización que otros? Parece que se trata de SIMD, etc., que no es lo que entiendo que significa @nalimilan .

Derecha. Hay dos nociones diferentes de vectorización aquí. uno trata
con obtener SIMD para bucles internos estrechos (vectorización del procesador). El principal
el problema que se está discutiendo aquí es la sintaxis/semántica de alguna manera "automáticamente"
poder llamar a una función de argumento único (o múltiple) en una colección.

El martes 4 de noviembre de 2014 a las 7:04 p. m., John Myles White [email protected]
escribió:

@eschnett https://github.com/eschnett : ¿Estás usando el mismo significado?
de vectorización como otros? Parece que se trata de SIMD, etc., que
no es lo que entiendo @nalimilan https://github.com/nalimilan to
significar.


Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -61738237.

En simetría con otros operadores . , ¿no debería f.(x) aplicar una colección de funciones a una colección de valores? (Por ejemplo, para transformar de algún sistema de coordenadas de unidad nd a coordenadas físicas).

Mientras discutíamos la sintaxis, surgió la idea de que usar bucles explícitos para expresar el equivalente de map(log, x) era demasiado lento. Por lo tanto, si uno puede hacer esto lo suficientemente rápido, llamar a map (o usar una sintaxis especial) o escribir bucles es equivalente en el nivel semántico, y no es necesario introducir una desambiguación sintáctica. Actualmente, llamar a una función de registro vectorial es mucho más rápido que escribir un bucle sobre una matriz, lo que hace que las personas soliciten una forma de expresar esta distinción en su código.

Aquí hay dos niveles de problemas: (1) sintaxis y semántica, (2) implementación.

El problema de la sintaxis y la semántica se trata de cómo el usuario puede expresar la intención de mapear ciertos cálculos en forma de elemento/transmisión a matrices dadas. Actualmente, Julia admite dos formas: usar funciones vectorizadas y permitir que los usuarios escriban el bucle explícitamente (a veces con la ayuda de macros). Ninguna de las dos formas es ideal. Si bien las funciones vectorizadas permiten escribir expresiones muy concisas como exp(0.5 * (x - y).^2) , tienen dos problemas: (1) es difícil trazar una línea en cuanto a qué funciones deben proporcionar una versión vectorizada y cuáles no, por lo que a menudo resulta en interminables debates del lado del desarrollador y confusión del lado del usuario (a menudo hay que consultar el documento para averiguar si ciertas funciones están vectorizadas). (2) Hace que sea difícil fusionar los bucles a través de los límites de la función. En este punto y probablemente dentro de varios meses/años, el compilador probablemente no podrá realizar tareas tan complejas como mirar varias funciones juntas, identificar el flujo de datos conjunto y producir una ruta de código optimizada a través de los límites de la función.

El uso de la función map soluciona el problema (1) aquí. Sin embargo, esto todavía no brinda ninguna ayuda para resolver el problema (2): el uso de funciones, ya sea una función vectorizada específica o un map genérico, siempre crea un límite de función que impide la fusión de bucles, que es fundamental en la computación de alto rendimiento. El uso de la función map también conduce a la verbosidad, por ejemplo, la expresión anterior ahora se convierte en una declaración más larga como map(exp, 0.5 * map(abs2, x - y)) . Puede imaginar razonablemente que este problema se agravaría con expresiones más complejas.

Entre todas las propuestas descritas en este hilo, personalmente creo que usar notaciones especiales para indicar el mapeo es la forma más prometedora de avanzar. En primer lugar, mantiene la concisión de la expresión. Tome la notación $ por ejemplo, las expresiones anteriores ahora se pueden escribir como exp $(0.5 * abs2$(x - y)) . Esto es un poco más largo que la expresión vectorizada original, pero no es tan malo, sin embargo, todo lo que requiere es insertar un $ en cada llamada de un mapeo. Por otro lado, esta notación también sirve como un indicador inequívoco de que se está realizando una asignación, que el compilador puede utilizar para romper el límite de la función y producir un bucle fusionado. En este curso, el compilador no tiene que mirar la implementación interna de la función; todo lo que necesita saber es que la función se asignará a cada elemento de las matrices dadas.

Dadas todas las facilidades de las CPU modernas, especialmente la capacidad de SIMD, fusionar varios bucles en uno solo es un paso hacia la computación de alto rendimiento. Este paso en sí mismo no activa la utilización de las instrucciones SIMD. La buena noticia es que ahora tenemos la macro @simd . El compilador puede insertar esta macro al comienzo del bucle producido cuando cree que hacerlo es seguro y beneficioso.

En resumen, creo que la notación $ (o propuestas similares) puede abordar en gran medida el problema de sintaxis y semántica, al tiempo que proporciona la información necesaria para que el compilador fusione bucles y explote SIMD, y así emita códigos de alto rendimiento.

El resumen de @lindahua es bueno en mi humilde opinión.

Pero creo que sería interesante extender esto aún más. Julia merece un sistema ambicioso que haga que muchos patrones comunes sean tan eficientes como los bucles desenrollados.

  • El patrón de fusionar llamadas a funciones anidadas en un solo bucle también debe aplicarse a los operadores, de modo que A .* B .+ C no conduzca a la creación de dos temporales, sino solo uno para el resultado.
  • La combinación de funciones y reducciones de elementos también debe manejarse, de modo que la reducción se aplique sobre la marcha después de calcular el valor de cada elemento. Por lo general, esto permitirá deshacerse de sumabs2(A) , reemplazándolo con una notación estándar como sum(abs$(A)$^2) (o sum(abs.(A).^2) ).
  • Finalmente, los patrones de iteración no estándar deben admitirse para matrices no estándar, de modo que para matrices dispersas, A .* B solo necesita manejar entradas distintas de cero y devuelve una matriz dispersa. Esto también sería útil si desea aplicar una función de elementos a un Set , un Dict o incluso un Range .

Los dos últimos puntos podrían funcionar haciendo que las funciones basadas en elementos devuelvan un tipo especial AbstractArray , digamos LazyArray , que calcularía sus elementos sobre la marcha (similar a Transpose escriba desde https://github.com/JuliaLang/julia/issues/4774#issuecomment-59422003). Pero en lugar de acceder ingenuamente a sus elementos repasándolos usando índices lineales de 1 a length(A) , se podría usar el protocolo iterador. El iterador para un tipo determinado elegiría automáticamente si la iteración por filas o por columnas es la más eficiente, según el diseño de almacenamiento del tipo. Y para matrices dispersas, permitiría omitir entradas cero (se requeriría que el original y el resultado tuvieran una estructura común, cf. https://github.com/JuliaLang/julia/issues/7010, https://github. com/JuliaLang/julia/issues/7157).

Cuando no se aplica reducción, un objeto del mismo tipo y forma que el original simplemente se llenaría iterando sobre LazyArray (equivalente a collect , pero respetando el tipo de la matriz original ). Lo único que se necesita para eso es que el iterador devuelva un objeto que se puede usar para llamar a getindex en LazyArray y setindex! en el resultado (por ejemplo, lineal o cartesiano coordenadas).

Cuando se aplica una reducción, usaría el método de iteración relevante en su argumento para iterar sobre las dimensiones requeridas de LazyArray y llenar una matriz con el resultado (equivalente a reduce pero usando un iterador personalizado para adaptarse al tipo de matriz). Una función (la utilizada en el último párrafo) devolvería un iterador repasando todos los elementos de la manera más eficiente; otros permitirían hacerlo sobre dimensiones específicas.

Todo este sistema también admitiría operaciones en el lugar de manera bastante sencilla.

Estaba pensando un poco en abordar la sintaxis y pensé en .= para aplicar operaciones elementales a la matriz.
Desafortunadamente, el ejemplo de @nalimilan sum(abs.(A).^2)) tendría que escribirse en dos pasos:

A = [1,2,3,4]
a .= abs(A)^2
result = sum(a)

Esto tendría la ventaja de ser fácil de leer y significaría que las funciones elementales solo necesitan escribirse para una entrada única (o múltiple) y optimizarse para ese caso en lugar de escribir métodos específicos de matriz.

Por supuesto, nada más que el rendimiento y la familiaridad impiden que alguien simplemente escriba map((x) -> abs(x)^2, A) este momento como se ha dicho.

Alternativamente, podría funcionar rodear una expresión que se va a mapear con .() .
No sé cuán difícil sería hacer esto, pero .sin(x) y .(x + sin(x)) mapearían la expresión dentro del paréntesis o la función que sigue a . .
Esto permitiría entonces reducciones como el ejemplo de @nalimilan donde sum(.(abs(A)^2)) podría escribirse en una sola línea.

Ambas propuestas usan un prefijo . que, mientras usaba la transmisión interna, me hizo pensar en operaciones elementales en arreglos. Esto podría intercambiarse fácilmente con $ u otro símbolo.
Esta es solo una alternativa a colocar un operador de mapa alrededor de cada función que se va a asignar y, en su lugar, agrupar la expresión completa y especificar la que se va a asignar en su lugar.

Experimenté con la idea LazyArray que expuse en mi último comentario: https://gist.github.com/nalimilan/e737bc8b3b10288abdad

Esta prueba de concepto no tiene azúcar sintáctico, pero (a ./ 2).^2 se traduciría a lo que está escrito en esencia como LazyArray(LazyArray(a, /, (2,)), ^, (2,)) . El sistema funciona bastante bien, pero necesita más optimización para ser remotamente competitivo con los bucles en cuanto a rendimiento. El problema (esperado) parece ser que la llamada de función en la línea 12 no está optimizada (casi todas las asignaciones ocurren allí), incluso en una versión donde no se permiten argumentos adicionales. Creo que necesito parametrizar el LazyArray en la función que llama, pero no he descubierto cómo podría hacerlo, y mucho menos manejar argumentos también. ¿Algunas ideas?

¿Alguna sugerencia sobre cómo mejorar el rendimiento de LazyArray ?

@nalimilan Experimenté con un enfoque similar hace un año, usando tipos de funtores en NumericFuns para parametrizar los tipos de expresiones perezosas. Probé una variedad de trucos, pero no tuve la suerte de cerrar la brecha de rendimiento.

La optimización del compilador se ha mejorado gradualmente durante el último año. Pero sigo sintiendo que todavía no es capaz de optimizar los gastos generales innecesarios. Este tipo de cosas requiere que los compiladores realicen funciones en línea agresivas. Puede intentar usar @inline y ver si mejora las cosas.

@lindahua @inline no hace ninguna diferencia en los tiempos, lo cual es lógico para mí ya que getindex(::LazyArray, ...) está especializado para una firma determinada LazyArray , que no especifica qué función debe ser llamado. Necesitaría algo como LazyArray{T1, N, T2, F} , con F la función a la que se debe llamar, para que al compilar getindex se conozca la llamada. ¿Hay alguna manera de hacer eso?

La integración sería otra gran mejora, pero por el momento los tiempos son mucho peores que incluso una llamada no integrada.

Puede considerar usar NumericFuns y F puede ser un tipo de functor.

Dahua

He necesitado funciones donde conozco el tipo de retorno para distribuido
computación, donde creo referencias al resultado antes del resultado (y
así su tipo) es conocido. Yo mismo he implementado algo muy similar, y
probablemente debería cambiar a usar lo que usted llama "Funtores". (no me gusta el
nombre "funtor", ya que suelen ser otra cosa <
http://en.wikipedia.org/wiki/Functor>, pero supongo que C++ confundió las aguas
aquí.)

Creo que tendría sentido separar la parte de Functor de la
funciones matematicas

-erik

El jueves 20 de noviembre de 2014 a las 10:35, Dahua Lin [email protected]
escribió:

Puede considerar usar NumericFuns y F puede ser un tipo de funtor.

Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/JuliaLang/julia/issues/8450#issuecomment -63826019.

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@lindahua He intentado usar funtores y, de hecho, el rendimiento es mucho más razonable:
https://gist.github.com/nalimilan/d345e1c080984ed4c89a

With functions:
# elapsed time: 3.235718017 seconds (1192272000 bytes allocated, 32.20% gc time)

With functors:
# elapsed time: 0.220926698 seconds (80406656 bytes allocated, 26.89% gc time)

Loop:
# elapsed time: 0.07613788 seconds (80187556 bytes allocated, 45.31% gc time) 

No estoy seguro de qué más se puede hacer para mejorar las cosas, ya que el código generado aún no parece óptimo. Necesito ojos más expertos para decir lo que está mal.

En realidad, la prueba anterior usó Pow , lo que aparentemente da una gran diferencia de velocidad dependiendo de si escribe un bucle explícito o usa LazyArray . Supongo que esto tiene que ver con la fusión de instrucciones que solo se realizaría en el último caso. El mismo fenómeno es visible con, por ejemplo, la adición. Pero con otras funciones, la diferencia es mucho menor, ya sea con una matriz de 100x100 o de 1000x1000, probablemente porque son externas y, por lo tanto, la inserción no gana mucho:

# With sqrt()
julia> test_lazy!(newa, a);
julia> <strong i="8">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.151761874 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="9">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.121304952 seconds (0 bytes allocated)

# With exp()
julia> test_lazy!(newa, a);
julia> <strong i="10">@time</strong> for i in 1:1000 test_lazy!(newa, a) end
elapsed time: 0.289050295 seconds (232000 bytes allocated)

julia> test_loop_dense!(newa, a);
julia> <strong i="11">@time</strong> for i in 1:1000 test_loop_dense!(newa, a) end
elapsed time: 0.191016958 seconds (0 bytes allocated)

Así que me gustaría saber por qué las optimizaciones no ocurren con LazyArray . El ensamblaje generado es bastante largo para operaciones simples. Por ejemplo, para x/2 + 3 :

julia> a1 = LazyArray(a, Divide(), (2.0,));

julia> a2 = LazyArray(a1,  Add(), (3.0,));

julia> <strong i="17">@code_native</strong> a2[1]
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
Source line: 1
    mov RAX, QWORD PTR [RDI + 8]
    mov RCX, QWORD PTR [RAX + 8]
    lea RDX, QWORD PTR [RSI - 1]
    cmp RDX, QWORD PTR [RCX + 16]
    jae L64
    mov RCX, QWORD PTR [RCX + 8]
    movsd   XMM0, QWORD PTR [RCX + 8*RSI - 8]
    mov RAX, QWORD PTR [RAX + 24]
    mov RAX, QWORD PTR [RAX + 16]
    divsd   XMM0, QWORD PTR [RAX + 8]
    mov RAX, QWORD PTR [RDI + 24]
    mov RAX, QWORD PTR [RAX + 16]
    addsd   XMM0, QWORD PTR [RAX + 8]
    pop RBP
    ret
L64:    movabs  RAX, jl_bounds_exception
    mov RDI, QWORD PTR [RAX]
    movabs  RAX, jl_throw_with_superfluous_argument
    mov ESI, 1
    call    RAX

A diferencia del equivalente:

julia> fun(x) = x/2.0 + 3.0
fun (generic function with 1 method)

julia> <strong i="21">@code_native</strong> fun(a1[1])
    .text
Filename: none
Source line: 1
    push    RBP
    mov RBP, RSP
    movabs  RAX, 139856006157040
Source line: 1
    mulsd   XMM0, QWORD PTR [RAX]
    movabs  RAX, 139856006157048
    addsd   XMM0, QWORD PTR [RAX]
    pop RBP
    ret

La parte hasta jae L64 es una verificación de límites de matriz. Usar @inbounds puede ayudar (si corresponde).

La parte de abajo, donde dos líneas consecutivas comienzan con mov RAX, ... , es un doble direccionamiento indirecto, es decir, acceder a un puntero a un puntero (o una matriz de matrices, o un puntero a una matriz, etc.). Esto puede tener que ver con la representación interna de LazyArray; tal vez usar inmutables (o representar inmutables de manera diferente por parte de Julia) puede ayudar aquí.

De cualquier manera, el código sigue siendo bastante rápido. Para hacerlo más rápido, tendría que estar integrado en la persona que llama, exponiendo más oportunidades de optimización. ¿Qué sucede si llamas a esta expresión, por ejemplo, desde un bucle?

Además: ¿Qué sucede si desmonta esto no desde REPL sino desde dentro de una función?

Tampoco puedo dejar de notar que la primera versión lleva a cabo un verdadero
división mientras que la segunda ha transformado x/2 en una multiplicación.

Gracias por los comentarios.

@eschnett LazyArray ya es inmutable, y estoy usando @inbounds en bucles. Después de ejecutar la esencia en https://gist.github.com/nalimilan/d345e1c080984ed4c89a , puede verificar lo que esto da en un bucle con esto:

function test_lazy!(newa, a)
    a1 = LazyArray(a, Divide(), (2.0,))
    a2 = LazyArray(a1, Add(), (3.0,))
    collect!(newa, a2)
    newa
end
<strong i="11">@code_native</strong> test_lazy!(newa, a); 

Entonces, ¿quizás todo lo que necesito es poder forzar la inserción? En mis intentos, agregar @inline a getindex no cambia los tiempos.

@toivoh ¿Qué podría explicar que en este último caso no se simplifique la división?

Seguí experimentando con la versión de dos argumentos (llamada LazyArray2 ). Resulta que para una operación simple como x .+ y , en realidad es más rápido usar un LazyArray2 que el actual .+ , y también está bastante cerca de los bucles explícitos (estos son para 1000 llamadas , consulte https://gist.github.com/nalimilan/d345e1c080984ed4c89a):

# With LazyArray2, filling existing array
elapsed time: 0.028212517 seconds (56000 bytes allocated)

# With explicit loop, filling existing array
elapsed time: 0.013500379 seconds (0 bytes allocated)

# With LazyArray2, allocating a new array before filling it
elapsed time: 0.098324278 seconds (80104000 bytes allocated, 74.16% gc time)

# Using .+ (thus allocating a new array)
elapsed time: 0.078337337 seconds (80712000 bytes allocated, 52.46% gc time)

Por lo tanto, parece que esta estrategia es viable para reemplazar todas las operaciones basadas en elementos, incluidos los operadores .+ , .* , etc.

También parece realmente competitivo lograr operaciones comunes como calcular la suma de las diferencias al cuadrado a lo largo de una dimensión de una matriz, es decir, sum((x .- y).^2, 1) (ver nuevamente la esencia):

# With LazyArray2 and LazyArray (no array allocated except the result)
elapsed time: 0.022895754 seconds (1272000 bytes allocated)

# With explicit loop (no array allocated except the result)
elapsed time: 0.020376307 seconds (896000 bytes allocated)

# With element-wise operators (temporary copies allocated)
elapsed time: 0.331359085 seconds (160872000 bytes allocated, 50.20% gc time)

@nalimilan
Su enfoque con LazyArrays parece ser similar a la forma en que funciona la fusión de vapor Haskell [1, 2]. ¿Tal vez podamos aplicar ideas de esa Área?

[1] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.104.7401
[2] http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.421.8551

@vchuravy Gracias. De hecho, esto es similar, pero más complejo porque Julia usa un modelo imperativo. Por el contrario, en Haskell, el compilador necesita manejar una gran variedad de casos e incluso manejar problemas de SIMD (que son manejados por LLVM en un punto posterior en Julia). Pero, sinceramente, no soy capaz de analizar todo en estos documentos.

@nalimilan Conozco el sentimiento. Encontré el segundo documento particularmente interesante ya que analiza la fusión de flujo generalizado, que aparentemente permite un buen modelo de cálculo sobre vectores.

Creo que el punto principal que debemos sacar de esto es que las construcciones como map y reduce en combinación con la pereza pueden ser lo suficientemente rápidas (o incluso más rápidas que los bucles explícitos).

Por lo que puedo decir, los corchetes todavía están disponibles en la sintaxis de llamadas. ¿Qué pasaría si esto se convirtiera en func{x} ? ¿Quizás un poco demasiado derrochador?

Sobre el tema de la vectorización rápida (en el sentido de SIMD), ¿hay alguna manera de que podamos emular la forma en que lo hace Eigen?

Aquí hay una propuesta para reemplazar todas las operaciones actuales de elementos con una generalización de lo que llamé LazyArray y LazyArray2 arriba. Por supuesto, esto se basa en la suposición de que podemos hacer esto rápido para todas las funciones sin depender de los funtores de NumericFuns.jl.

1) Agregue una nueva sintaxis f.(x) o f$(x) o lo que sea que cree un LazyArray llamando a f() en cada elemento de x .

2) Generalice esta sintaxis siguiendo cómo funciona actualmente broadcast , de modo que, por ejemplo f.(x, y, ...) o f$(x, y, ...) crea un LazyArray , pero expande las dimensiones de singleton de x , y , ... para darles un tamaño común. Por supuesto, esto se haría sobre la marcha mediante cálculos en los índices para que las matrices expandidas no se asignen realmente.

3) Haga .+ , .- , .* , ./ , .^ , etc. use LazyArray en lugar de broadcast .

4) Introducir un nuevo operador de asignación .= o $= que transformaría (llamando a collect ) un LazyArray en un arreglo real (de un tipo dependiendo de su entradas a través de reglas de promoción, y de un tipo de elemento dependiendo del tipo de elemento de las entradas y de la función llamada).

5) Tal vez incluso reemplace broadcast con una llamada a LazyArray y un collect inmediato de los resultados en una matriz real.

El punto 4 es clave: las operaciones basadas en elementos nunca devolverían arreglos reales, siempre LazyArray s, de modo que cuando se combinan varias operaciones, no se hacen copias y los bucles se pueden fusionar para lograr eficiencia. Esto permite solicitar reducciones como sum en el resultado sin asignar los temporales. Entonces, las expresiones de este tipo serían idiomáticas y eficientes, tanto para arreglos densos como para matrices dispersas:

y .= sqrt.(x .+ 2)
y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
sum((x .- y).^2, 1)

Creo que devolver este tipo de objeto liviano encaja perfectamente en la nueva imagen de vistas de matriz y Transpose / CTranspose . Significa que en Julia puede realizar operaciones complejas de manera muy eficiente con una sintaxis densa y legible, aunque en algunos casos debe llamar explícitamente a copy cuando necesita que una "pseudo-matriz" sea independiente de la matriz real en la que se basa.

Esto realmente suena como una característica importante. El comportamiento actual de los operadores basados ​​en elementos es una trampa para los nuevos usuarios, ya que la sintaxis es agradable y corta, pero el rendimiento suele ser terriblemente malo, aparentemente peor que en Matlab. Durante la semana pasada, varios hilos sobre julia-users tuvieron problemas de rendimiento que desaparecerían con un diseño de este tipo:
https://groups.google.com/d/msg/julia-users/t0KvvESb9fA/6_ZAp2ujLpMJ
https://groups.google.com/d/msg/julia-users/DL8ZsK6vLjw/w19Zf1lVmHMJ
https://groups.google.com/d/msg/julia-users/YGmDUZGOGgo/LmsorgEfXHgJ

Para los propósitos de este problema, separaría la sintaxis de la pereza. Pero tu propuesta es interesante.

Parece que llega un punto en el que solo hay _tantos puntos_. El ejemplo del medio en particular estaría mejor escrito como

x .|> x->exp(-x ^ 2) * sin(k * x) + im * log(x - 1)

que requiere solo funciones básicas y un eficiente map ( .|> ).

Esta es una comparación interesante:

y .=  √π exp.(-x .^ 2) .* sin.(k .* x) .+ im * log.(x .- 1)
y =  [√π exp(-x[i]^ 2) .* sin(k * x[i]) .+ im * log(x[i] - 1) for i = 1:length(x)]

Si descarta la parte for ... , la comprensión es solo un carácter más larga. Casi preferiría tener una sintaxis de comprensión abreviada que todos esos puntos.

Una comprensión 1d no conserva la forma, pero ahora que tenemos for i in eachindex(x) eso también podría cambiar.

Un problema con las comprensiones es que no admiten DataArrays.

Creo que podría valer la pena ver un montón de cosas que sucedieron en .Net que se parecen mucho a la idea de LazyArray. Esencialmente, se parece bastante a un enfoque de estilo LINQ para mí, donde tiene una sintaxis que se parece a las cosas elementales que tenemos ahora, pero en realidad esa sintaxis crea un árbol de expresión, y ese árbol de expresión luego se evalúa de alguna manera eficiente. . ¿Está eso de alguna manera cerca?

En .Net, llegaron lejos con esta idea: podría ejecutar estos árboles de expresión en paralelo en múltiples CPU (agregando .AsParallel()), o podría ejecutarlos en un clúster grande con DryadLINQ, o incluso en un http:/ /research.microsoft.com/en-us/projects/accelerator/ (es posible que este último no se haya integrado completamente con LINQ, pero tiene un espíritu similar si no recuerdo mal), o, por supuesto, podría traducirse a SQL si es el los datos tenían esa forma y solo usaba operadores que podían traducirse a declaraciones SQL.

Mi sensación es que Blaze también va en esa dirección, es decir, una forma de construir fácilmente objetos que describen cálculos, y luego puedes tener diferentes motores de ejecución para ello.

No estoy seguro de que esto sea muy claro, pero me parece que todo este problema debe analizarse en el contexto de cómo se puede generar un código similar a SIMD eficiente de bajo nivel y cómo podría usarse para computación GPU, agrupamiento, paralelo computacion etc

Sí, tienes razón en que el ejemplo más largo tiene demasiados puntos. Pero los dos más cortos son más típicos, y en ese caso es importante tener una sintaxis corta. Me gustaría separar la sintaxis de la pereza, pero como muestran sus comentarios, parece ser muy difícil, ¡siempre mezclamos los dos!

Uno podría imaginarse adaptando la sintaxis de comprensión, algo así como y = [sqrt(x + 2) over x] . Pero como señaló @johnmyleswhite , deberían admitir DataArrays , pero también matrices dispersas y cualquier nuevo tipo de matriz. Así que este es nuevamente un caso de mezcla de sintaxis y características.

Más fundamentalmente, creo que dos características que ofrece mi propuesta sobre las alternativas son:
1) Compatibilidad con la asignación en el lugar sin asignación mediante y[:] = sqrt.(x .+ 2) .
2) Soporte para reducciones sin asignación como sum((x .- y).^2, 1) .

¿Podría proporcionarse eso con otras soluciones (sin tener en cuenta los problemas de sintaxis)?

@davidanthoff Gracias, mirándolo ahora (creo que LazyArray también podría ser compatible con la computación paralela).

Tal vez esto podría combinarse con generadores; también son una especie de matriz perezosa. Me gusta un poco la sintaxis de comprensión [f(x) over x] , aunque podría ser conceptualmente difícil para los recién llegados (ya que el mismo nombre se usa efectivamente tanto para los elementos como para la matriz en sí). Si las comprensiones sin paréntesis crearan un generador (como jugué hace mucho tiempo ), entonces sería natural usar estas nuevas comprensiones sin paréntesis de estilo sobrex para devolver un LazyArray en lugar de recopilarlo inmediatamente.

@mbauman Sí, los generadores y las matrices perezosas comparten muchas propiedades. La idea de usar corchetes para recopilar un generador/arreglo perezoso, y no agregarlos para mantener el objeto perezoso, suena bien. Entonces, con respecto a mis ejemplos anteriores, uno podría escribir 1) y[:] = sqrt(x + 2) over x y sum((x - y)^2 over (x, y), 1) (aunque lo encuentro natural incluso para los recién llegados, dejemos el tema de over para la sesión de desprendimiento de bicicletas y concentrarse primero en los fundamentos).

Me gusta la idea f(x) over x . Incluso podríamos usar f(x) for x para evitar una nueva palabra clave. De hecho [f(x) for x=x] ya funciona. Entonces necesitaríamos hacer comprensiones equivalentes a map , para que puedan funcionar para no matrices. Array simplemente sería el valor predeterminado.

Debo admitir que también me ha gustado la idea over . Una diferencia entre over como mapa y for en comprensión de lista es lo que sucede en el caso de múltiples iteradores: [f(x, y) for x=x, y=y] da como resultado una matriz. Para el caso del mapa, generalmente aún desea un vector, es decir, [f(x, y) over x, y] sería equivalente a [f(x, y) for (x,y) = zip(x, y)]] . Debido a esto, sigo pensando que vale la pena introducir una palabra clave adicional over , porque, como ha surgido este problema, map sobre varios vectores es muy común y debe ser conciso.

¡Oye, convencí a Jeff sobre la sintaxis! ;-)

Esto pertenece al lado del #4470, por lo que se agrega a los proyectos 0.4 por ahora.

Si entiendo la esencia de la discusión, el principal problema es que queremos obtener una sintaxis similar a la de un mapeo que:

  • funciona con varios tipos de datos, como DataArrays, no solo matrices nativas;
  • es tan rápido como un bucle escrito manualmente.

Podría ser posible hacerlo utilizando la inserción, pero teniendo mucho cuidado para asegurarse de que la inserción funcione.

¿Qué pasa con un enfoque diferente: usar macro según el tipo de datos inferido? Si podemos inferir que la estructura de datos es DataArray, usamos map-macro proporcionado por la biblioteca DataArrays. Si es SomeKindOfStream, usamos la biblioteca de flujo proporcionada. Si no podemos inferir el tipo, solo usamos la implementación general despachada dinámicamente.

Esto podría obligar a los creadores de estructuras de datos a escribir tales macros, pero solo sería necesario si su autor quisiera que tuviera una ejecución realmente eficiente.

Si lo que estoy escribiendo no está claro, quiero decir que algo como [EXPR for i in collection if COND] podría traducirse a eval(collection_mapfilter_macro(:(i), :(EXPR), :(COND))) , donde collection_mapfilter_macro se elige en función del tipo de colección inferido.

No, no queremos hacer cosas así. Si DataArray define map (o el equivalente), siempre se debe llamar a su definición para DataArrays, independientemente de lo que se pueda inferir.

Este problema en realidad no se trata de implementación, sino de sintaxis. En este momento, muchas personas están acostumbradas a sin(x) mapear implícitamente si x es una matriz, pero hay muchos problemas con ese enfoque. La pregunta es qué sintaxis alternativa sería aceptable.

1) Soporte para la asignación en el lugar sin asignación usando y[:] = sqrt.(x .+ 2)
2) Soporte para reducciones sin asignación como sum((x .- y).^2, 1)

y = √π exp(-x^2) * sin(k*x) + im * log(x-1)

Mirando estos tres ejemplos de otros, creo que con la sintaxis for esto terminaría algo como esto:
1) y[:] = [ sqrt(x + 2) for x ])
2) sum([ (x-y)^2 for x,y ], 1)
y
y = [ √π exp(-x^2) * sin(k*x) + im * log(x-1) for x,k ]

¡Me gusta mucho esto! El hecho de que crea una matriz temporal es bastante explícito y sigue siendo legible y conciso.

Sin embargo, una pregunta menor, ¿podría x[:] = [ ... for x ] tener algo de magia para mutar la matriz sin asignar una temporal?
No estoy seguro de si esto proporcionaría muchos beneficios, pero me imagino que sería útil para arreglos grandes.
Sin embargo, puedo creer que puede ser un caldero de pescado completamente diferente que debería discutirse en otro lugar.

@ Mike43110 Su x[:] = [ ... for x ] podría escribirse x[:] = (... for x) , el RHS crea un generador, que se recopilaría elemento por elemento para llenar x , sin asignar una copia. Esa fue la idea detrás de mi experimento LazyArray anterior.

La sintaxis [f <- y] estaría bien si se combinara con una sintaxis Int[f <- y] para un mapa que conoce su tipo de salida y no necesita interpolar de f(y[1]) cuáles serán los otros elementos.

Especialmente, dado que esto también brinda una interfaz intuitiva para mapslices , [f <- rows(A)] donde rows(A) (o columns(A) o slices(A, dims) ) devuelve un Slice objeto para que se pueda usar el envío:

map(f, slice::Slice) = mapslices(f, slice.A, slice.dims)

Cuando agrega la indexación, esto se vuelve un poco más difícil. Por ejemplo

f(x[:,j]) .* g(x[i,:])

Es difícil igualar la concisión de eso. La explosión del estilo de comprensión es bastante mala:

[f(x[m,j])*g(x[i,n]) for m=1:size(x,1), n=1:size(x,2)]

donde, para empeorar las cosas, se requería inteligencia para saber que este es un caso de iteración anidada y no se puede hacer con un solo over . Aunque si f y g son un poco caros, esto podría ser más rápido:

[f(x[m,j]) for m=1:size(x,1)] .* [g(x[i,n]) for _=1, n=1:size(x,2)]

pero aún más.

Este tipo de ejemplo parece argumentar a favor de "puntos", ya que eso podría dar f.(x[:,j]) .* g.(x[i,:]) .

@JeffBezanson No estoy seguro de cuál es la intención de tu comentario. ¿Alguien ha sugerido deshacerse de la sintaxis .* ?

No; Me estoy centrando en f y g aquí. Este es un ejemplo en el que no puede simplemente agregar over x al final de la línea.

OK, ya veo, me había perdido el final del comentario. De hecho, la versión de puntos es mejor en ese caso.

Aunque con vistas de matriz, habrá una alternativa razonablemente eficiente (AFAICT) y no tan fea:
[ f(y) * g(z) for y in x[:,j], z in x[i,:] ]

¿Se podría resolver el ejemplo anterior anidando sobre palabras clave?

f(x)*g(y) over x,y

se interpreta como

[f(x)*g(y) for (x,y) = zip(x,y)]

mientras que

f(x)*g(y) over x over y

se convierte

[f(x)*g(y) for x=x, y=y]

Entonces, el ejemplo específico de arriba sería algo así como

f(x[:,n])*g(x[m,:]) over x[:,n] over x[m,:]

EDITAR: En retrospectiva, eso no es tan conciso como pensé que sería.

@JeffBezanson ¿Qué tal

f(x[:i,n]) * g(x[m,:i]) over i

da el equivalente de f.(x[:,n] .* g.(x[m,:]) . La nueva sintaxis x[:i,n] significa que i se está introduciendo localmente como iterador sobre los índices del contenedor x[:,n] . No sé si eso es posible de implementar. Pero parece (prima facie) ni feo ni engorroso, y la sintaxis misma da límites para el iterador, a saber, 1:longitud(x[:,n]). En lo que respecta a las palabras clave, "over" puede indicar que se va a usar todo el rango, mientras que "for" se puede usar si el usuario desea especificar un subrango de 1:longitud(x[:,n]):

f(x[:i,n]) * g(x[m,:i]) for i in 1:length(x[:,n])-1 .

@davidagold , :i ya significa el símbolo i .

Ah sí, buen punto. Bueno, mientras los puntos sean un juego justo, ¿qué pasa con

f(x[.i,n]) * g(x[m,.i]) over i

donde el punto indica que i se está introduciendo localmente como un iterador sobre 1:longitud(x[:,n). Supongo que, en esencia, esto cambia la notación de puntos de modificar las funciones a modificar las matrices, o más bien sus índices. Esto salvaría a uno del "punto arrastrado", señaló Jeff:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) over i ]

Opuesto a

f.(g.(e.^(x[m,:]))) .* p.(e.^(f.(y[:,n])))

aunque supongo que este último es un poco más corto. [EDITAR: también, si es posible omitir over i cuando no hay ambigüedad, entonces uno tiene una sintaxis un poco más corta:

[ f(g(e^(x[m,.i]))) * p(e^(f(y[.i,n]))) ] ]

Una ventaja potencial de la sintaxis de comprensión es que podría permitir una gama más amplia de patrones de operación por elementos. Por ejemplo, si el analizador entendiera que la indexación con i en x[m, .i] es implícitamente la longitud del módulo (x[m,:]), entonces uno podría escribir

[ f(x[.i]) * g(y[.j]) over i, j=-i ]

Para multiplicar los elementos de x contra los elementos de y en orden inverso, es decir, el primer elemento de x contra el último elemento de y , etc. .Se podría escribir

[ f(x[.i]) * g(y[.j]) over i, j=i+1 ]

para multiplicar el i elemento de x por el elemento de i+1 de y (donde se multiplicaría el último elemento de x por el primer elemento de y debido a que la indexación se entiende en este contexto como módulo de longitud (x)). Y si p::Permutation permuta (1, ..., longitud(x)) uno podría escribir

[ f(x[.i]) * g(y[.j]) over i, j=p(i) ]

para multiplicar el i elemento de x por el elemento de p(i) de y .

De todos modos, esa es solo una humilde opinión de un extraño sobre un tema completamente especulativo. =p Agradezco el tiempo que alguien se toma para considerarlo.

Una versión mejorada de vectorize que usará el reciclaje de estilo r podría ser muy útil. Es decir, los argumentos que no coinciden con el tamaño del argumento más grande se amplían mediante el reciclaje. Luego, los usuarios pueden vectorizar fácilmente lo que quieran, independientemente de la cantidad de argumentos, etc.

unvectorized_sum(a, b, c, d) = a + b + c + d
vectorized_sum = @super_vectorize(unvectorized_sum)

a = [1, 2, 3, 4]
b = [1, 2, 3]
c = [1, 2]
d = 1

A = [1, 2, 3, 4]
B = [1, 2, 3, 1]
C = [1, 2, 1, 2]
D = [1, 1, 1, 1]

vectorized_sum(a, b, c, d) = vectorized_sum(A, B, C, D) = [4, 7, 8, 8]

Tiendo a pensar que reciclar sacrifica demasiada seguridad por conveniencia. Con el reciclaje, es muy fácil escribir código con errores que se ejecuta sin generar ningún error.

La primera vez que leí sobre el comportamiento de R, inmediatamente me pregunté por qué alguien pensaría que era una buena idea. Eso es algo realmente extraño y sorprendente de hacer implícitamente en arreglos de tamaños que no coinciden. Puede haber un puñado de casos en los que desee ampliar la matriz más pequeña, pero también puede querer relleno cero o repetir los elementos finales, o extrapolación, o un error, o cualquier otro número de aplicaciones dependientes. opciones

Si usar o no @super_vectorize quedaría en manos del usuario. También sería posible dar advertencias para varios casos. Por ejemplo, en R,

c(1, 2, 3) + c(1, 2)
[1] 2 4 4
Warning message:
In c(1, 2, 3) + c(1, 2) :
  longer object length is not a multiple of shorter object length

No tengo objeciones en hacer que sea algo opcional que los usuarios pueden elegir usar o no, pero no es necesario implementarlo en el idioma base cuando también se puede hacer en un paquete.

@vectorize_1arg y @vectorize_2arg ya están incluidos en Base, y las opciones que brindan al usuario parecen algo limitadas.

Pero este problema se centra en el diseño de un sistema para eliminar @vectorize_1arg y @vectorize_2arg de Base. Nuestro objetivo es eliminar las funciones vectorizadas del lenguaje y reemplazarlas con una mejor abstracción.

Por ejemplo, el reciclaje se puede escribir como

[ A[i] + B[mod1(i,length(B))] for i in eachindex(A) ]

que para mí está bastante cerca de la forma ideal de escribirlo. Nadie necesita construirlo por ti. Las preguntas principales son (1) ¿se puede hacer esto más conciso? (2) cómo extenderlo a otros tipos de contenedores.

Mirando la propuesta de @davidagold , me preguntaba si var: no podría usarse para ese tipo de cosas donde la variable sería el nombre antes de los dos puntos. Vi que esta sintaxis solía significar A[var:end] , por lo que parece estar disponible.

f(x[:,j]) .* g(x[i,:]) sería entonces f(x[a:,j]) * g(x[i,b:]) for a, b que no es mucho peor.

Sin embargo, múltiples dos puntos serían un poco extraños.

f(x[:,:,j]) .* g(x[i,:,:]) -> f(x[a:,a:,j]) * g(x[i,b:,b:]) for a, b fue mi pensamiento inicial sobre esto.

Ok, aquí hay una breve demostración de un programa de reciclaje. Debería ser capaz de manejar arreglos n-dimensionales. Probablemente sería posible incorporar tuplas de manera análoga a los vectores.

using DataFrames

a = [1, 2, 3]
b = 1
c = [1 2]
d = <strong i="6">@data</strong> [NA, 2, 3]

# coerce an array to a certain size using recycling
coerce_to_size = function(argument, dimension_extents...)

  # number of repmats needed, initialized to 1
  dimension_ratios = [dimension_extents...]

  for dimension in 1:ndims(argument)

    dimension_ratios[dimension] = 
      ceil(dimension_extents[dimension] / size(argument, dimension))
  end

  # repmat array to at least desired size
  if typeof(argument) <: AbstractArray
    rep_to_size = repmat(argument, dimension_ratios...)
  else
    rep_to_size = 
      fill(argument, dimension_ratios...)
  end

  # cut down array to exactly desired size
  dimension_ranges = [1:i for i in dimension_extents]
  dimension_ranges = tuple(dimension_ranges...)

  rep_to_size = getindex(rep_to_size, dimension_ranges...)  

end

recycle = function(argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  expand_arguments = 
    [coerce_to_size(argument, dimension_extents...) 
     for argument in argument_list]
end

recycle(a, b, c, d)

mapply = function(FUN, argument_list...)
  argument_list = recycle(argument_list...)
  FUN(argument_list...)
end

mapply(+, a, b, c, d)

Claramente, este no es el código más elegante o rápido (soy un inmigrante R reciente). No estoy seguro de cómo llegar desde aquí a una macro @vectorize .

EDITAR: bucle redundante combinado
EDIT 2: coerción separada al tamaño. actualmente solo funciona para 0-2 dimensiones.
EDIT 3: una forma un poco más elegante de hacer esto sería definir un tipo especial de matriz con indexación mod. Es decir,

special_array = [1 2; 3 5]
special_array.dims = (10, 10, 10, 10)
special_array[4, 1, 9, 7] = 3

EDIT 4: Las cosas que me pregunto existen porque esto fue difícil de escribir: ¿una generalización n-dimensional de hcat y vcat? ¿Una forma de llenar una matriz n-dimensional (que coincida con el tamaño de una matriz dada) con listas o tuplas de los índices de cada posición en particular? ¿Una generalización n-dimensional de repmat?

[pao: resaltado de sintaxis]

Realmente no desea definir funciones con la sintaxis foo = function(x,y,z) ... end en Julia, aunque funciona. Eso crea un enlace no constante del nombre a una función anónima. En Julia, la norma es usar funciones genéricas y los enlaces a las funciones son automáticamente constantes. De lo contrario, obtendrá un rendimiento terrible.

No veo por qué repmat es necesario aquí. Las matrices llenas con el índice de cada posición también son una señal de advertencia: no debería ser necesario usar una gran cantidad de memoria para representar tan poca información. Creo que tales técnicas realmente solo son útiles en idiomas donde todo necesita ser "vectorizado". Me parece que el enfoque correcto es simplemente ejecutar un bucle en el que se transforman algunos índices, como en https://github.com/JuliaLang/julia/issues/8450#issuecomment -111898906.

Sí, eso tiene sentido. Aquí hay un comienzo, pero tengo problemas para descubrir cómo hacer el bucle al final y luego hacer una macro @vectorize .

function non_zero_mod(big::Number, little::Number)
  result = big % little
  result == 0 ? little : result
end

function mod_select(array, index...)
  # just return singletons
  if !(typeof(array) <: AbstractArray) return array end
  # find a new index with moded values
  transformed_index = 
      [non_zero_mod( index[i], size(array, i) )
       for i in 1:ndims(array)]
  # return value at moded index
  array[transformed_index...]
end

function mod_value_list(argument_list, index...)
  [mod_select(argument, index...) for argument in argument_list]
end

mapply = function(FUN, argument_list...)

  # largest dimension in arguments
  max_dimension = maximum([ndims(i) for i in argument_list])
  # initialize dimension extents to 1
  dimension_extents = [1 for i in 1:max_dimension]

  # loop through argument and dimension
  for argument_index in 1:length(argument_list)
    for dimension in 1:ndims(argument_list[argument_index])
      # find the largest size for each dimension
      dimension_extents[dimension] = maximum([
        size(argument_list[argument_index], dimension),
        dimension_extents[dimension]
      ])
    end
  end

  # more needed here
  # apply function over arguments using mod_value_list on arguments at each position
end

En la charla, @JeffBezanson mencionó la sintaxis sin(x) over x , ¿por qué no algo más como:
sin(over x) ? (o use algún carácter en lugar de tener over como palabra clave)

Una vez que esto se resuelva, también podemos resolver # 11872

Espero no llegar tarde a la fiesta, pero me gustaría dar un +1 a la propuesta de sintaxis de @davidagold . Es conceptualmente claro, conciso y se siente muy natural de escribir. No estoy seguro de si . sería el mejor carácter de identificación, o qué tan factible sería una implementación real, pero se podría hacer una prueba de concepto usando una macro para probarlo (esencialmente como @devec , pero incluso podría ser más fácil de implementar).

También tiene la ventaja de "encajar" con la sintaxis de comprensión de matriz existente:

result = [g(f(.i), h(.j)) over i, j]

contra

result = [g(f(_i), h(_j)) for _i in eachindex(i), _j in eachindex(j)]

La diferencia clave entre los dos es que el primero tendría más restricciones en la preservación de la forma, ya que implica un mapa.

over , range y window tienen algo de arte previo en el espacio OLAP como modificadores de la iteración, esto parece consistente.

No estoy interesado en la sintaxis . ya que parece un avance lento hacia el ruido de línea.

$ es quizás consistente, ¿interna los valores de iterar i, j en la expresión?

result = [g(f($i), h($j)) over i, j]

Para la vectorización automática de una expresión, ¿no podemos taint uno de los vectores en la expresión y hacer que el sistema de tipos levante la expresión al espacio vectorial?

Hago algo parecido a las operaciones de series temporales donde la expresividad de julia ya me permite escribir

ts_a = GetTS( ... )
ts_b = GetTS( ... ) 
factors = [ 1,  2, 3 ]

ts_x = ts_a * 2 + sin( ts_a * factors ) + ts_b 

que cuando se observa genera una serie temporal de vectores.

La parte principal que falta es la capacidad de elevar automáticamente las funciones existentes al espacio. esto hay que hacerlo a mano

Esencialmente, me gustaría poder definir algo como lo siguiente...

abstract TS{K}
function {F}{K}( x::TS{K}, y::TS{K} ) = tsjoin( F, x, y ) 
# tsjoin is a time series iteration operator

y luego ser capaz de especializarse para operaciones específicas

function mean{K}(x::TS{K}) = ... # my hand rolled form

Hola @JeffBezanson ,

Si entiendo correctamente, me gustaría proponer una solución a su comentario de JuliaCon 2015 con respecto a un comentario realizado anteriormente:
"[...] Y decirles a los escritores de bibliotecas que pongan @vectorize en todas las funciones apropiadas es una tontería; deberías poder simplemente escribir una función, y si alguien quiere calcularla para cada elemento, usa el mapa".
(Pero no abordaré el otro problema fundamental "[..] no hay una razón realmente convincente por la que sin, exp, etc. deban mapearse implícitamente sobre matrices").

En Julia v0.40, he podido obtener una solución algo mejor (en mi opinión) que @vectrorize :

abstract Vectorizable{Fn}
#Could easily have added extra argument to Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg{Fn} <: Vectorizable{Fn}

call{F}(::Type{Vectorizable2Arg{F}}, x1, x2) = eval(:($F($x1,$x2)))
function call{F,T1,T2}(fn::Type{Vectorizable2Arg{F}}, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Function in need of vectorizing:
function _myadd(x::Number, y::Number)
    return x+y+1
end

#"Register" the function as a Vectorizable 2-argument (alternative to @vectorize):
typealias myadd Vectorizable2Arg{:_myadd}

<strong i="13">@show</strong> myadd(5,6)
<strong i="14">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable!

Esto es más o menos razonable, pero es algo similar a la solución @vectorize . Para que la vectorización sea elegante, sugiero que Julia admita lo siguiente:

abstract Vectorizable <: Function
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#Note: by default, functions would normally be <: Function:
function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

¡Eso es todo! Tener una función heredada de una función Vectorizable la haría vectorizable.

Espero que esto esté en la línea de lo que estabas buscando.

Saludos,

MAMÁ

En ausencia de herencia múltiple, ¿cómo hereda una función de Vectorizable y de otra cosa? ¿Y cómo se relaciona la información de herencia de métodos específicos con la información de herencia de una función genérica?

@ma-laforge Ya puede hacerlo --- defina un tipo myadd <: Vectorizable2Arg , luego implemente call para myadd en Number .

¡Gracias por eso @JeffBezanson!

De hecho, casi puedo ver mi solución casi tan bien como lo que quiero:

abstract Vectorizable
#Could easily have parameterized Vectorizable, but want to show inheritance case:
abstract Vectorizable2Arg <: Vectorizable

function call{T1,T2}(fn::Vectorizable2Arg, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

#SECTION F: Function in need of vectorizing:
immutable MyAddType <: Vectorizable2Arg; end
const myadd = MyAddType()
function call(::MyAddType, x::Number, y::Number)
    return x+y+1
end

<strong i="7">@show</strong> myadd(5,6)
<strong i="8">@show</strong> myadd(collect(1:10),collect(21:30.0)) #Type stable

Ahora, lo único que faltaría sería una forma de "subescribir" cualquier función, de modo que toda la sección F pudiera reemplazarse con la sintaxis más elegante:

function myadd(x::Number, y::Number) <: Vectorizable2Arg
    return x+y+1
end

NOTA: Hice el tipo "MyAddType" y el nombre de la función en un objeto singleton "myadd" porque encuentro que la sintaxis resultante es más agradable que si uno estuviera usando Type{Vectorizable2Arg} en la firma de la llamada:

function call{T1,T2}(fn::Type{Vectorizable2Arg}, v1::Vector{T1}, v2::Vector{T2})

Lamentablemente , por su respuesta, parece que esto _no_ sería una solución adecuada para la "tontería" de la macro @vectorize .

Saludos,

MAMÁ

@johnmyleswhite :

Me gustaría responder a tu comentario, pero creo que no lo entiendo. ¿Puedes aclarar?

Una cosa que _puedo_ decir:
No hay nada especial en "Vectorizable". La idea es que cualquiera pueda definir su propia función "clase" (Ej: MyFunctionGroupA<:Function ). Luego podrían captar llamadas a funciones de ese tipo definiendo su propia firma de "llamada" (como se demostró anteriormente).

Habiendo dicho eso: Mi sugerencia es que las funciones definidas dentro de Base deberían usar Base.Vectorizable <: Function (o algo similar) para generar automáticamente algoritmos vectorizados automáticamente.

Entonces sugeriría que los desarrolladores de módulos implementen sus propias funciones usando un patrón algo así como:

myfunction(x::MyType, y::MyType) <: Base.Vectorizable

Por supuesto, tendrían que proporcionar su propia versión de promote_type(::Type{MyType},::Type{MyType}) , si el valor predeterminado aún no es devolver MyType .

Si el algoritmo de vectorización predeterminado es insuficiente, nada impide que el usuario implemente su propia jerarquía:

MyVectorizable{nargs} <: Function
call(fn::MyVectorizable{2}, x, y) = ...

myfunction(x::MyType, y:MyType) <: MyVectorizable{2}

MAMÁ

@ma-laforge, Perdón por no estar claro. Mi preocupación es que cualquier jerarquía siempre carecerá de información importante porque Julia tiene una herencia única, lo que requiere que se comprometa con un solo tipo principal para cada función. Si usa algo como myfunction(x::MyType, y::MyType) <: Base.Vectorizable entonces su función no se beneficiará de que otra persona defina un concepto como Base.NullableLiftable que genera automáticamente funciones de anulables.

Parece que esto no sería un problema con los rasgos (cf. https://github.com/JuliaLang/julia/pull/13222). También está relacionada la nueva posibilidad de declarar métodos como puros (https://github.com/JuliaLang/julia/pull/13555), lo que podría implicar automáticamente que dicho método es vectorizable (al menos para métodos de un solo argumento).

@johnmyleswhite ,

Si entiendo bien: no creo que sea un problema para _este_ caso en particular. Eso es porque estoy proponiendo un patrón de diseño. Tus funciones no _tienen_ que heredar de Base.Vectorizable ... Puedes usar las tuyas propias.

Realmente no sé mucho sobre NullableLiftables (parece que no tengo eso en mi versión de Julia). Sin embargo, suponiendo que herede de Base.Function (que tampoco es posible en mi versión de Julia):

NullableLiftable <: Function

Su módulo podría entonces implementar (solo una vez) un subtipo _nuevo_ vectorizable:

abstract VectorizableNullableLiftable <: NullableLiftable

function call{T1,T2}(fn::VectorizableNullableLiftable, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

Entonces, de ahora en adelante, ¡cualquiera que defina una función <: VectorizableNullableLiftable obtendrá su código de vectorización aplicado automáticamente!

function mycooladdon(scalar1, scalar2) <: VectorizableNullableLiftable
...

Entiendo que tener más de un tipo Vectorizable todavía es algo molesto (y un poco poco elegante)... Pero al menos eliminaría una de las molestas repeticiones (1) en Julia (tener que registrar _cada_ recién agregado función con una llamada a @vectorize_Xarg).

(1) Eso suponiendo que Julia admita la herencia en funciones (por ejemplo, myfunction(...)<: Vectorizable ), lo cual no parece, en v0.4.0. La solución que obtuve trabajando en Julia 0.4.0 es solo un truco... Todavía tienes que registrar tu función... no mucho mejor que llamar a @vectorize_Xarg

MAMÁ

Sigo pensando que es una especie de abstracción equivocada. Una función que puede o debe ser "vectorizada" no es un tipo particular de función. _Cada_ función se puede pasar a map , dándole este comportamiento.

Por cierto, con el cambio en el que estoy trabajando en la rama jb/functions, podrá hacer function f(x) <: T (aunque, claramente, solo para la primera definición de f ).

Ok, creo que entiendo mejor lo que estás buscando... y no es lo que sugerí. Creo que eso también podría ser parte de los problemas que tuvo @johnmyleswhite con mis sugerencias...

... Pero si ahora entiendo cuál es el problema, la solución me parece aún más simple:

function call{T1,T2}(fn::Function, v1::Vector{T1}, v2::Vector{T2})
    RT = promote_type(T1,T2) #For type stability!
    return RT[fn(v1[i],v2[i]) for i in 1:length(v1)]
end

myadd(x::Number, y::Number) = x+y+1

Dado que myadd es del tipo Function , debería quedar atrapado por la función call ... lo cual hace:

call(myadd,collect(1:10),collect(21:30.0)) #No problem

Pero call no se envía automáticamente en funciones, por alguna razón (no estoy seguro de por qué):

myadd(collect(1:10),collect(21:30.0)) #Hmm... Julia v0.4.0 does not dispatch this to call...

Pero me imagino que este comportamiento no debería ser demasiado difícil de cambiar . Personalmente, no sé cómo me siento acerca de crear funciones que lo abarquen todo, pero parece que eso es lo que quieres.

Me di cuenta de algo extraño: Julia ya auto-vectoriza funciones si no se escriben:

myadd(x,y) = x+y+1 #This gets vectorized automatically, for some reason

RE: Por cierto...:
¡Frio! Me pregunto qué cosas geniales podré hacer subtipificando funciones :).

Julia ya auto-vectoriza funciones si no se escriben

Parece así porque el operador + que se usa dentro de la función está vectorizado.

Pero me imagino que este comportamiento no debería ser demasiado difícil de cambiar. Personalmente, no sé cómo me siento acerca de crear funciones que lo abarquen todo, pero parece que eso es lo que quieres.

Comparto sus reservas. De manera sensata, no puede tener una definición que diga "aquí se explica cómo llamar a cualquier función", porque cada función en sí misma dice qué hacer cuando se llama, ¡eso es una función!

Debería haber dicho: ... operadores infijos no Unicode definidos por el usuario, ya que no creo que queramos exigir a los usuarios que escriban caracteres Unicode para acceder a dicha funcionalidad central. Aunque veo que $ es en realidad uno de los agregados, ¡así que gracias por eso! Wow, esto realmente funciona en Julia hoy (incluso si no es "rápido"... todavía):

julia> ($) = mapa
julia > sen$(0.5*(abs2$(xy)))

@binarybana ¿qué tal / \mapsto ?

julia> x, y = rand(3), rand(3);

julia> ↦ = map    # \mapsto<TAB>
map (generic function with 39 methods)

julia> sin ↦ (0.5 * (abs2 ↦ (x-y)))
3-element Array{Float64,1}:
 0.271196
 0.0927406
 0.0632608

También hay:

FWIW, al menos asumiría inicialmente que \mapsto era una sintaxis alternativa para lambdas, ya que se usa comúnmente en matemáticas y, esencialmente (en su encarnación ASCII, -> ) en Julia, también . Creo que esto sería bastante confuso.

Hablando de matemáticas… En la teoría de modelos he visto map expresado aplicando una función a una tupla sin paréntesis. Es decir, si \bar{a}=(a_1, \dots, a_n) , entonces f(\bar{a}) es f(a_1, \dots, a_n) (es decir, esencialmente, apply ) y f\bar{a} es (f(a_1), \dots, f(a_n)) (es decir, map ). Sintaxis útil para definir homomorfismos, etc., pero no tan fácilmente transferible a un lenguaje de programación :-}

¿Qué tal cualquiera de las otras alternativas como \Mapsto , lo confundiría con => (par)? Creo que ambos símbolos se distinguen aquí uno al lado del otro:

  • ->

Hay muchos símbolos que se parecen, ¿cuál sería la razón de tener tantos si solo usamos los que se ven muy diferentes o son ASCII puro?

Creo que esto sería bastante confuso.

Creo que la documentación y la experiencia solucionan esto, ¿estás de acuerdo?

También hay muchos otros símbolos como flechas, sinceramente, no sé para qué se usan en matemáticas o, de lo contrario, ¡solo propuse estos porque tienen map en sus nombres! :sonreír:

Supongo que mi punto es que -> es el intento de Julia de representar en ASCII. Entonces, usar para significar otra cosa parece desaconsejable. No es que no pueda distinguirlos visualmente :-)

Mi intuición es que si vamos a usar símbolos matemáticos bien establecidos, al menos deberíamos pensar en cómo el uso de Julia difiere del uso establecido. Parece lógico seleccionar símbolos con map en sus nombres, pero en este caso se refiere a la definición de un mapa (o de mapas de diferentes tipos, o los tipos de dichos mapas). Eso también es cierto para el uso en Pair, más o menos, donde en lugar de definir una función completa definiendo a qué se asigna un parámetro, en realidad enumera a qué se asigna un argumento dado (valor de parámetro), es decir, es un elemento de un explícitamente función enumerada, conceptualmente (por ejemplo, un diccionario).

@ Ismael-VC El problema con su sugerencia es que necesita llamar a map dos veces, mientras que la solución ideal no implicaría matrices temporales y se reduciría a map((a, b) -> sin(0.5 * abs2(a-b)), x, y) . Además, repetir dos veces no es bueno tanto visualmente como para escribir (para lo cual sería bueno tener un equivalente ASCII).

Es posible que a los usuarios de R no les guste esta idea, pero si avanzamos hacia la desaprobación del análisis de casos especiales infix-macro actual de ~ (los paquetes como GLM y DataFrames tendrían que cambiar al análisis de macros de sus fórmulas DSL, ref https:/ /github.com/JuliaStats/GLM.jl/issues/116), eso liberaría la rara mercancía de un operador ascii infijo.

a ~ b podría definirse en base como map(a, b) , y tal vez a .~ b podría definirse como broadcast(a, b) ? Si se analiza como un operador infijo convencional, los macro DSL como una emulación de la interfaz de fórmula R serían libres de implementar su propia interpretación del operador dentro de las macros, como lo hace JuMP con <= y == .

Tal vez no sea la sugerencia más bonita, pero tampoco lo son las abreviaturas en Mathematica si las usa en exceso... mi favorita es .#&/@

:+1: para una carcasa menos especial y más generalidad y consistencia, los significados que propones para ~ y .~ me parecen geniales.

+1 Toni.

@tkelman Para que quede claro, ¿cómo escribiría, por ejemplo sin(0.5 * abs2(a-b)) de forma completamente vectorizada?

Con una comprensión, probablemente. No creo que el mapa infijo funcione para varargs o en el lugar, por lo que la posibilidad de una sintaxis libre no resuelve todos los problemas.

Entonces eso realmente no solucionaría este problema. :-/

Hasta ahora, la sintaxis sin(0.5 * abs2(a-b)) over (a, b) (o una variante, posiblemente usando un operador infijo) es la más atractiva.

El título de este número es "Sintaxis alternativa para map(func, x) ". El uso de un operador infijo no resuelve la fusión de mapa/bucle para eliminar los temporales, pero creo que puede ser un problema aún más amplio, relacionado pero técnicamente separado que la sintaxis.

Sí, estoy de acuerdo con @tkelman , el punto es tener una sintaxis alternativa para map , por lo que sugerí usar \mapsto , . Lo que menciona @nalimilan parece ser más amplio y más adecuado para otro tema en mi humilde opinión, How to fully vecotrize expressions ?

<rambling>
¡Este problema ha estado ocurriendo durante más de un año (y podría continuar indefinidamente como muchos otros problemas ahora)! Pero podríamos tener Alternative syntax for map(func, x) ahora . De los ±450 colaboradores julianos, solo 41 han podido encontrar este problema y/o están dispuestos a compartir una opinión (eso es mucho para un problema de github pero claramente no es suficiente en este caso), en general, no hay sugerencias muy diferentes. (que no son variaciones menores de un mismo concepto).

Sé que a algunos de ustedes no les gusta la idea o ven el valor de hacer encuestas/sondeos (:sorprendidos:), pero como no necesito pedir permiso a nadie para algo como esto, lo haré de todos modos. Es un poco triste la forma en que no estamos aprovechando al máximo nuestra comunidad y las redes sociales y otras comunidades, y también es aún más triste que no veamos el valor de esto, veamos si puedo recopilar más opiniones diferentes y frescas , o al menos verificar. averiguar qué piensa la mayoría sobre las opiniones actuales en este tema en particular, como un experimento y ver cómo va. Tal vez sea realmente inútil, tal vez no lo sea, solo hay una forma de saberlo realmente.
</rambling>

@Ismael-VC: Si realmente quieres hacer una encuesta, lo primero que debes hacer es considerar cuidadosamente la pregunta que quieres hacer. No puede esperar que todos lean el hilo completo y resuman las opciones que se han discutido individualmente.

map(func, x) también cubre cosas como map(v -> sin(0.5 * abs2(v)), x) , y esto es lo que se discutió en este hilo. No pasemos esto a otro hilo, ya que sería más difícil tener en cuenta todas las propuestas discutidas anteriormente.

No me opongo a agregar sintaxis para el caso simple de aplicar una función genérica usando map , pero si lo hacemos, creo que sería una buena idea considerar la imagen más amplia al mismo tiempo. Si no fuera por eso, el problema podría haberse solucionado hace mucho tiempo.

@ Ismael-VC Es poco probable que las encuestas ayuden aquí en mi humilde opinión. No estamos tratando de averiguar cuál de varias soluciones es la mejor, sino más bien de encontrar una solución que nadie haya encontrado ya. Esta discusión ya es larga e involucró a muchas personas, no creo que agregar más ayude.

@Ismael-VC Está bien, siéntete libre de hacer una encuesta. De hecho, he realizado un par de encuestas de doodle sobre problemas en el pasado (por ejemplo, http://doodle.com/poll/s8734pcue8yxv6t4). En mi experiencia, las mismas o menos personas votan en las encuestas que las que discuten en los hilos de los temas. Tiene sentido para problemas de sintaxis muy específicos, a menudo superficiales. Pero, ¿cómo va a generar una encuesta nuevas ideas cuando todo lo que puede hacer es elegir entre las opciones existentes?

Por supuesto, el verdadero objetivo de este problema es eliminar funciones implícitamente vectorizadas. En teoría, la sintaxis para map es suficiente para eso, porque todas estas funciones solo están haciendo map en todos los casos.

He intentado buscar la notación matemática existente para esto, pero tiendes a ver comentarios en el sentido de que la operación es demasiado poco importante para tener notación. En el caso de funciones arbitrarias en un contexto matemático, casi puedo creerlo. Sin embargo, lo más parecido parece ser la notación de productos de Hadamard, que tiene algunas generalizaciones: https://en.wikipedia.org/wiki/Hadamard_product_ (matrices)#Analogous_Operations

Eso nos deja con sin∘x como sugirió @rfourquet . No parece muy útil, ya que requiere unicode y no es muy conocido de todos modos.

@nalimilan , creo que simplemente harías sin(0.5 * abs2(a-b)) ~ (a,b) lo que se traduciría en map((a,b)->sin(0.5 * abs2(a-b)), (a,b)) . No estoy seguro de si eso es del todo correcto, pero creo que funcionaría.

También desconfío de ahondar demasiado en el problema de déjame darte una expresión enorme y complicada y tú me la vectorizas perfectamente. Creo que la solución definitiva en ese sentido es crear un DAG completo de expresiones/tareas + planificación de consultas, etc. Pero creo que es mucho más difícil que simplemente tener una sintaxis conveniente para map .

@quinnj Sí, esa es esencialmente la sintaxis over propuesta anteriormente, excepto con un operador infijo.

Comentario serio: creo que es probable que reinvente SQL si persigue esta idea lo suficientemente lejos, ya que SQL es esencialmente un lenguaje para componer funciones de elementos de muchas variables que se aplican posteriormente a través de "vectorización" de filas.

@johnmyleswhite está de acuerdo, comienza a parecerse a un DSL, también conocido como Linq

En el tema publicado en los hilos, puede especializar el operador |> 'tubería' y obtener la funcionalidad de estilo de mapa. Puede leerlo como canalizar la función a los datos. Como bono adicional, puede usar el mismo para realizar la composición de funciones.

julia> (|>)(x::Function, y...) = map(x, y... )
|> (generic function with 8 methods)

julia> (|>)(x::Function, y::Function) = (z...)->x(y(z...))
|> (generic function with 8 methods)

julia> sin |> cos |> [ 1,2,3 ]
3-element Array{Float64,1}:
  0.514395
 -0.404239
 -0.836022

julia> x,y = rand(3), rand(3)
([0.8883630054185454,0.32542923024720194,0.6022157767415313],    [0.35274912207468145,0.2331784754319688,0.9262490059844113])

julia> sin |> ( 0.5 *( abs( x - y ) ) )
3-element Array{Float64,1}:
 0.264617
 0.046109
 0.161309

@johnmyleswhite Eso es cierto, pero hay metas intermedias que valen la pena y que son bastante modestas. En mi sucursal, la versión map de las expresiones vectorizadas de operaciones múltiples ya es más rápida que la que tenemos ahora. Por lo tanto, descubrir cómo hacer una transición sin problemas es algo urgente.

@johnmyleswhite No estoy seguro. Una gran cantidad de SQL se trata de seleccionar, ordenar y fusionar filas. Aquí solo estamos hablando de aplicar una función por elementos. Además, SQL no proporciona ninguna sintaxis para distinguir las reducciones (por ejemplo SUM ) de las operaciones con elementos (por ejemplo > , LN ). Estos últimos simplemente se vectorizan automáticamente al igual que en Julia actualmente.

@JeffBezanson La belleza del uso de \circ es que si interpreta una familia indexada como una función del conjunto de índices (que es la "implementación" matemática estándar), entonces el mapeo _es_ simplemente composición. Entonces (sin ∘ x)(i)=sin(x(i)) , o más bien sin(x[i]) .

El uso de la tubería, como menciona @mdcfrancis , sería esencialmente solo una composición de "orden de diagrama", que a menudo se hace con un punto y coma (posiblemente gordo) en matemáticas (o especialmente aplicaciones CS de la teoría de categorías), pero ya tenemos la tubería operador, por supuesto.

Si ninguno de estos operadores de composición está bien, entonces uno podría usar algunos otros. Por ejemplo, al menos algunos autores usan el humilde \cdot para la composición abstracta de flechas/morfismos, ya que es esencialmente la "multiplicación" del grupoide (más o menos) de flechas.

Y si uno quisiera un análogo ASCII: también hay autores que en realidad usan un punto para indicar la multiplicación. (Es posible que también haya visto que algunos lo usan para la composición; no lo recuerdo).

Así que uno podría tener sin . x … pero supongo que sería confuso :-}

Aún así... esa última analogía podría ser un argumento a favor de una de las primeras propuestas, es decir, sin.(x) . (O tal vez eso es exagerado).

Intentémoslo desde un ángulo diferente, no me dispares.

Si definimos .. por collect(..(A,B)) == ((a[1],..., a[n]), (b[1], ...,b[n])) == zip(A,B) , entonces usando T[x,y,z] = [T(x), T(y), T(z)] formalmente se cumple que

map(f,A,B) = [f(a[1],b[1]), ..., f(a[n],b[n])] = f[zip(A,B)...] = f[..(A,B)]

Eso motiva al menos una sintaxis para el mapa que no interfiere con la sintaxis para la construcción de matrices. Con :: o table la extensión f[::(A,B)] = [f(a[i], b[j]) for i in 1:n, j in 1:n] conduce al menos a un segundo caso de uso interesante.

Considere cuidadosamente la pregunta que desea hacer.

@toivoh Gracias, lo haré. Actualmente estoy evaluando varios software de sondeos/encuestas. Además, solo encuestaré sobre la sintaxis preferida, aquellos que quieran leer el hilo completo simplemente lo harán, simplemente no supongamos que nadie más estará interesado en hacerlo.

encontrar una solución que nadie haya encontrado ya

@nalimilan nadie entre nosotros, eso es. :sonreír:

¿Cómo va a generar una encuesta nuevas ideas cuando todo lo que puede hacer es elegir entre las opciones existentes?
las mismas o menos personas votan en las encuestas que las que discuten en los hilos de los temas.

@JeffBezanson Me alegra saber que ya has hecho encuestas, ¡sigue así!

  • ¿Cómo has estado promocionando tus encuestas?
  • Desde el software de sondeos/encuestas que he evaluado hasta ahora, kwiksurveys.com permite a los usuarios agregar sus propias opiniones en lugar de la opción _ninguna de estas opciones es para mí_.

sin∘x No parece muy útil, ya que requiere Unicode y no es muy conocido de todos modos.

Tenemos tantos Unicode, usémoslo, incluso tenemos una buena manera de usarlos con la terminación de pestañas, ¿qué hay de malo en usarlos entonces? Si no lo sabe, documentemos y eduquemos, si no existe, ¿qué hay de malo en inventarlo? ¿Realmente necesitamos esperar a que alguien más lo invente y lo use para que podamos tomarlo como un precedente y solo después considerarlo?

tiene un precedente, ¿entonces el problema es que es Unicode? ¿Por qué? entonces, ¿cuándo vamos a empezar a usar el resto del Unicode no muy conocido? ¿Nunca?

Según esa lógica, Julia no es muy conocida de todos modos, sin embargo, aquellos que quieran aprender, lo harán. Simplemente no tiene sentido para mí, en mi muy humilde opinión.

Es justo, no estoy totalmente en contra . Requerir unicode para usar una función bastante básica es solo una marca en contra. No necesariamente lo suficiente como para hundirlo por completo.

¿Sería completamente loco usar/sobrecargar * como una alternativa ASCII? Diría que podría argumentarse que tiene algún sentido matemáticamente, pero supongo que a veces puede ser difícil discernir su significado... (Por otra parte, si está restringido a la funcionalidad map , entonces map ya es una alternativa ASCII, ¿no?)

La belleza del uso de \circ es que si interpreta una familia indexada como una función del conjunto de índices (que es la "implementación" matemática estándar), entonces el mapeo _es_ simplemente composición.

No estoy seguro de comprar esto.

@hayd ¿Qué parte de eso? ¿Que una familia indexada (p. ej., una secuencia) puede verse como una función del conjunto de índices, o que el mapeo se convierte en composición? ¿O que esta es una perspectiva útil en este caso?

Creo que los dos primeros puntos (matemáticos) son bastante poco controvertidos. Pero, sí, no voy a defender fuertemente el uso de esto aquí, fue más que nada un "¡Ah, eso encaja un poco!" reacción.

@mlhetland |> está bastante cerca de -> y funciona hoy; también tiene la 'ventaja' de ser asociativo correcto.

x = parse( "sin |> cos |> [1,2]" )
:((sin |> cos) |> [1,2])

@mdcfrancis Claro. Pero eso le da la vuelta a la interpretación de la composición que describí. Es decir, sin∘x equivaldría a x |> sin , ¿no?

PD: Tal vez se perdió en el "álgebra", pero solo permitir funciones en la construcción de matriz tipeada T[x,y,z] tal que f[x,y,z] es [f(x),f(y),f(z)] da directamente

map(f,A) == f[A...]

que es bastante legible y podría tratarse como sintaxis.

Eso es inteligente. Pero sospecho que si podemos hacer que funcione, sin[x...] realmente pierde verbosidad frente a sin(x) o sin~x etc.

¿Qué pasa con la sintaxis [sin xs] ?

Esto es similar en sintaxis a la comprensión de matriz [sin(x) for x in xs] .

@mlhetland pecado |> x === map( pecado, x )

Ese sería el orden inverso del significado de encadenamiento de funciones actual . No es que no me importe encontrar un mejor uso para ese operador, pero necesitaría un período de transición.

@mdcfrancis Sí, entiendo que eso es lo que buscas. Lo que invierte las cosas (como reitera @tkelman ) wrt. la interpretación de la composición que esbocé.

Creo que integrar la vectorización y el encadenamiento estaría muy bien. Me pregunto si las palabras serían los operadores más claros.
Algo como:

[1, 2] mapall
  +([2, 3]) map
  ^(2, _) chain
  { a = _ + 1
    b = _ - 1
    [a..., b...] } chain
  sum chain
  [ _, 2, 3] chain
  reduce(+, _)

Varios mapas seguidos podrían combinarse automáticamente en un solo mapa para mejorar el rendimiento. También tenga en cuenta que asumo que el mapa tendrá algún tipo de función de transmisión automática. Reemplazar [1, 2] con _ al principio podría generar una función anónima. Tenga en cuenta que estoy haciendo uso de las reglas magrittr de R para encadenar (vea mi publicación en el hilo de encadenamiento).

Tal vez esto esté empezando a parecerse más a un DSL.

He estado siguiendo este problema durante mucho tiempo y no he comentado hasta ahora, pero esto está empezando a salirse de control en mi humilde opinión.

Apoyo firmemente la idea de una sintaxis limpia para el mapa. Me gusta más la sugerencia de @tkelman de ~ , ya que se mantiene dentro de ASCII para una funcionalidad tan básica, y me gusta bastante sin~x . Esto permitiría un mapeo de estilo de una sola línea bastante sofisticado como se discutió anteriormente. El uso de sin∘x también estaría bien. Para algo más complicado, tiendo a pensar que un bucle adecuado es mucho más claro (y generalmente el mejor rendimiento). Realmente no me gusta mucho la transmisión 'mágica', hace que el código sea mucho más difícil de seguir. Un bucle explícito suele ser más claro.

Eso no quiere decir que dicha funcionalidad no deba agregarse, pero primero tengamos una buena y concisa sintaxis map , especialmente porque está a punto de volverse súper rápido (según mis pruebas de la rama jb/functions ) .

Tenga en cuenta que uno de los efectos de jb/functions es que broadcast(op, x, y) tiene un rendimiento tan bueno como la versión personalizada x .op y que especializó manualmente la transmisión en op .

Para algo más complicado, tiendo a pensar que un bucle adecuado es mucho más claro (y generalmente el mejor rendimiento). Realmente no me gusta mucho la transmisión 'mágica', hace que el código sea mucho más difícil de seguir. Un bucle explícito suele ser más claro.

no estoy de acuerdo exp(2 * x.^2) es perfectamente legible y menos detallado que [exp(2 * v^2) for v in x] . En mi humilde opinión, el desafío aquí es evitar atrapar a las personas permitiéndoles usar el primero (que asigna copias y no fusiona operaciones): para esto, necesitamos encontrar una sintaxis que sea lo suficientemente corta para que la forma lenta pueda quedar obsoleta.

Más pensamientos. Hay varias cosas posibles que puede querer hacer al llamar a una función:

bucle sin argumentos (cadena)
recorrer solo el argumento encadenado (mapa)
recorrer todos los argumentos (mapall)

Cada uno de los anteriores podría ser modificado, por:
Marcar un elemento para recorrer (~)
Marcar un elemento para que no se repita (un conjunto adicional de [ ] )

Los elementos unibles deben manejarse automáticamente, sin tener en cuenta la sintaxis.
La expansión de las dimensiones singleton debería ocurrir automáticamente si hay al menos dos argumentos que se están repitiendo

La transmisión solo hace una diferencia cuando habría habido una dimensión
desajuste de lo contrario. Así que cuando dices no transmitir, quieres dar una
error en su lugar si el tamaño del argumento no coincide?

sin[x...] realmente pierde verbosidad ante sin(x) o sin~x, etc.

Además, continuando con el pensamiento, el mapa sin[x...] es una versión menos ansiosa en [f(x...)] .
la sintaxis

[exp(2 * (...x)^2)]

o algo similar como [exp(2 * (x..)^2)] estaría disponible y se explicaría por sí mismo si alguna vez se introduce un encadenamiento de funciones tácito real.

@nalimilan sí, pero eso encaja dentro de mi categoría de 'una sola línea' que dije que estaba bien sin bucles.

Mientras enumeramos todos nuestros deseos: mucho más importante para mí sería que los resultados de map se puedan asignar sin asignación ni copia. Esta es otra razón por la que seguiré prefiriendo los bucles para el código crítico para el rendimiento, pero si esto se puede mitigar (#249 actualmente no parece un cajero automático esperanzador), entonces todo esto se vuelve mucho más atractivo.

resultados del mapa para ser asignables sin asignación o copia

¿Puedes ampliar esto un poco? Ciertamente puedes mutar el resultado de map .

Supongo que quiere decir almacenar la salida de map en una matriz preasignada.

Sí exactamente. Disculpas si eso ya es posible.

Por supuesto. Tenemos map! , pero como observa, #249 está pidiendo una forma más agradable de hacerlo.

@jtravs Propuse una solución anterior con LazyArray (https://github.com/JuliaLang/julia/issues/8450#issuecomment-65106563), pero hasta ahora el rendimiento no fue el ideal.

@toivoh Hice varias ediciones en esa publicación después de publicarla. La pregunta que me preocupaba es cómo averiguar qué argumentos recorrer y cuáles no (por lo que mapall podría ser más claro que la transmisión). Creo que si está recorriendo más de un argumento, creo que siempre se debe expandir las dimensiones de singleton para producir matrices comparables si es necesario.

map! es exactamente correcto. Sería bueno si algún buen azúcar de sintaxis funcionara aquí también cubriera ese caso. ¿No podríamos tener que x := ... asigna implícitamente el RHS a x .

Puse un paquete llamado ChainMap que integra mapeo y encadenamiento.

He aquí un breve ejemplo:

<strong i="7">@chain</strong> begin
  [1, 2]
  -(1)
  (_, _)
  map_all(+)
  <strong i="8">@chain_map</strong> begin
    -(1)
    ^(2. , _)
  end
  begin
    a = _ - 1
    b = _ + 1
    [a, b]
  end
  sum
end

Seguí pensando en ello y creo que finalmente descubrí una sintaxis consistente y juliana para mapear matrices derivadas de la comprensión de matrices. La siguiente propuesta es juliana porque se basa en el lenguaje de comprensión de matriz que ya está establecido.

  1. A partir de f[a...] que en realidad fue propuesto por @Jutho , la convención
    que para a vectores a, b
f[a...] == map(f, a[:])
f[a..., b...] == map(f, a[:], b[:])
etc

que no introduce nuevos símbolos.

2.) Además de esto, propondría la introducción de un operador adicional: un operador de salpicadura que preserva la forma .. (digamos). Esto se debe a que ... es un operador de dispersión _flat_, por lo que f[a...] debería devolver un vector y no una matriz incluso si a es n -dimensional. Si se elige .. , entonces en este contexto,

f[a.., ] == map(f, a)
f[a.., b..] == map(f, a, b)

y el resultado hereda la forma de los argumentos. Permitir la transmisión

f[a.., b..] == broadcast(f, a, b)

permitiría escribir piensa como

sum(*[v.., v'..]) == dot(v,v)

Heureka?

Esto no ayuda con el mapeo de expresiones, ¿verdad? Una de las ventajas de la sintaxis over es cómo funciona con expresiones:

sin(x * (y - 2)) over x, y  == map((x, y) -> sin(x * (y - 2)), x, y) 

Bueno, posiblemente a través [sin(x.. * y..)] o sin[x.. * y..] arriba si quieres permitir eso. Me gusta un poco más que la sintaxis over porque da una pista visual de que los operadores de función están en los elementos y no en los contenedores.

Pero, ¿no puede simplificar eso para que x.. simplemente se asigne sobre x ? Entonces, el ejemplo de @johansigfrids sería:

sin(x.. * (y.. - 2))  == map((x, y) -> sin(x * (y - 2)), x, y)

@jtravs Debido al alcance ( [println(g(x..))] frente a println([g(x..)]) ) y la consistencia [x..] = x .

Otra posibilidad es tomar x.. = x[:, 1], x[:, 2], etc. como símbolo parcial de los subarreglos principales (columnas) y ..y como símbolo parcial de los subarreglos finales ..y = y[1,:], y[2,:] . Si ambos se ejecutan en diferentes índices, esto cubre muchos casos interesantes.

[f(v..)] == [f(v[i]) for i in 1:m ]
[v.. * v..] == [v[i] * v[i] for 1:m]
[v.. * ..v] == [v[i] * v[j] for i in 1:m, j in 1:n]
[f(..A)] == [f(A[:, j]) for j in 1:n]
[f(A..)] == [f(A[i, :]) for i in 1:m]
[dot(A.., ..A)] == [dot(A[:,i], A[j,:]) for i in 1:m, j in 1:n] == A*A
[f(..A..)] == [f(A[i,j]) for i in 1:m, j in 1:n]
[v..] == [..v] = v
[..A..] == A

( v un vector, A una matriz)

Prefiero over , ya que le permite escribir una expresión en sintaxis normal en lugar de introducir muchos corchetes y puntos.

Tienes razón sobre el desorden, pienso y traté de adaptar y sistematizar mi propuesta. Para no sobrecargar aún más la paciencia de todos, escribí mis pensamientos sobre mapas e índices, etc. en esencia https://gist.github.com/mschauer/b04e000e9d0963e40058 .

Después de leer este hilo, mi preferencia hasta ahora sería tener _both_ f.(x) para cosas simples y personas acostumbradas a funciones vectorizadas (el modismo " . = vectorizado" es bastante común), y f(x^2)-x over x para expresiones más complicadas.

Hay demasiadas personas que vienen de Matlab, Numpy, etcétera, para abandonar por completo la sintaxis de funciones vectorizadas; decirles que agreguen puntos es fácil de recordar. Una buena sintaxis similar a over para vectorizar expresiones complejas en un solo ciclo también es muy útil.

La sintaxis over realmente me molesta. Se me acaba de ocurrir por qué: supone que todos los usos de cada variable en una expresión están vectorizados o no vectorizados, lo que puede no ser el caso. Por ejemplo, log(A) .- sum(A,1) : suponga que eliminamos la vectorización de log . Tampoco puede vectorizar funciones sobre expresiones, lo que parece una deficiencia bastante importante, ¿y si quisiera escribir exp(log(A) .- sum(A,1)) y tener el exp y el log vectorizados y el sum no?

@StefanKarpinski , entonces debe hacer exp.(log.(A) .- sum(A,1)) y aceptar los temporales adicionales (por ejemplo, en uso interactivo donde el rendimiento no es crítico), o s = sum(A, 1); exp(log(A) - s) over A (aunque eso no es del todo correcto si sum(A,1) es un vector y quería transmitir); es posible que solo tenga que usar una comprensión. Independientemente de la sintaxis que se nos ocurra, no cubriremos todos los casos posibles, y su ejemplo es particularmente problemático porque cualquier sintaxis "automatizada" tendría que saber que sum es puro y que puede ser izada fuera del bucle/mapa.

Para mí, la primera prioridad es una sintaxis f.(x...) para broadcast(f, x...) o map(f, x...) para que podamos deshacernos de @vectorize . Después de eso, podemos continuar trabajando en una sintaxis como over (o lo que sea) para abreviar usos más generales de map y comprensiones.

@stevengj No creo que el segundo ejemplo funcione, porque - no se transmitirá. Suponiendo que A es una matriz, la salida sería una matriz de matrices de una sola fila, cada una de las cuales es el logaritmo de un elemento de A menos el vector de sumas a lo largo de la primera dimensión. Necesitarías broadcast((x, y)->exp(log(x)-y), A, sum(A, 1)) . Pero creo que tener una sintaxis concisa para map es útil y no necesariamente tiene que ser una sintaxis concisa para broadcast también.

¿Las funciones que históricamente han sido auto-vectorizadas como sin seguirán siéndolo con la nueva sintaxis, o quedará obsoleta? Me preocupa que incluso la sintaxis f. se sienta como un 'te pillé' a una gran cantidad de programadores científicos que no están motivados por argumentos de elegancia conceptual.

Mi sensación es que las funciones históricamente vectorizadas como sin deberían quedar obsoletas en favor de sin. , pero deberían quedar obsoletas de forma casi permanente (en lugar de eliminarse por completo en la versión posterior) para el beneficio de los usuarios de otros lenguajes científicos.

Un problema menor (?) con f.(args...) : aunque la sintaxis object.(field) se usa en su mayor parte rara vez y probablemente se puede reemplazar con getfield(object, field) sin demasiado dolor, hay un _lot_ de definiciones/referencias de métodos de la forma Base.(:+)(....) = .... , y sería doloroso cambiarlos a getfield .

Una solución sería:

  • Haga que Base.(:+) se convierta en map(Base, :+) como todos los demás f.(args...) , pero defina un método obsoleto map(m::Module, s::Symbol) = getfield(m, s) para compatibilidad con versiones anteriores
  • admitir la sintaxis Base.:+ (que actualmente falla) y recomendar esto en la advertencia de desaprobación para Base.(:+)

Me gustaría volver a preguntar: ¿es algo que podamos hacer en 0.5.0? Creo que es importante debido a la desaprobación de muchos constructores vectorizados. Pensé que estaría bien con esto, pero encuentro map(Int32, a) , en lugar de int32(a) un poco tedioso.

¿Es esto básicamente solo una cuestión de elegir la sintaxis en este punto?

¿Es esto básicamente solo una cuestión de elegir la sintaxis en este punto?

Creo que @stevengj dio buenos argumentos a favor de escribir sin.(x) en lugar de .sin(x) en su PR https://github.com/JuliaLang/julia/pull/15032. Así que diría que el camino ha sido despejado.

Tenía una reserva sobre el hecho de que aún no tenemos una solución para generalizar esta sintaxis para componer expresiones de manera eficiente. Pero creo que en esta etapa será mejor fusionar esta característica que cubre la mayoría de los casos de uso en lugar de mantener esta discusión sin resolver indefinidamente.

@JeffBezanson Estoy revirtiendo el hito en esto a 0.5.0 para mencionarlo durante una discusión de clasificación, principalmente para asegurarme de que no lo olvide.

¿#15032 también funciona para call , por ejemplo, Int32.(x) ?

@ViralBShah , sí. Cualquier f.(x...) se transforma en map(f, broadcast, x...) en el nivel de sintaxis, independientemente del tipo de f .

Esa es la principal ventaja de . sobre algo como f[x...] , que por lo demás es atractivo (y no requeriría cambios en el analizador) pero solo funcionaría para f::Function . f[x...] también choca conceptualmente un poco con las comprensiones de matriz T[...] . Aunque creo que a @StefanKarpinski le gusta la sintaxis de paréntesis.

(Para elegir otro ejemplo, se puede llamar a los objetos o::PyObject en PyCall, invocando el método __call__ del objeto de Python o , pero los mismos objetos también pueden admitir o[...] indexación f[x...] , pero funcionaría bien con la transmisión de o.(x...) ).

call ya no existe.

(También me gusta el argumento de @nalimilan de que f.(x...) hace que .( sea el análogo de .+ etc.)

Sí, la analogía puntual es la que más me gusta. ¿Podemos seguir adelante y fusionarnos?

¿Debería el getfield con un módulo ser una desaprobación real?

@tkelman , ¿a diferencia de qué? Sin embargo, la advertencia de desaprobación para Base.(:+) (es decir, argumentos de símbolos literales) debería sugerir Base.:+ , no getfield . (_Actualización_: también requirió una desaprobación de sintaxis para manejar definiciones de métodos).

@ViralBShah , ¿hubo alguna decisión sobre esto en la discusión de clasificación del jueves? #15032 está en bastante buena forma para fusionarse, creo.

Creo que Viral se perdió esa parte de la llamada. Mi impresión es que varias personas todavía tienen reservas sobre la estética de f.(x) y podrían preferir cualquiera

  1. un operador infijo que sería más simple conceptualmente y en implementación, pero no tenemos ningún ascii disponible por lo que puedo ver. Mi idea anterior de desaprobar el análisis de macros de ~ requeriría trabajo para reemplazar en paquetes y probablemente sea demasiado tarde para intentar hacerlo en este ciclo.
  2. o una nueva sintaxis alternativa que facilita la fusión de bucles y la eliminación de temporales. No se han implementado otras alternativas para acercarse al nivel de #15032, por lo que parece que deberíamos fusionar eso y probarlo a pesar de las reservas restantes.

Sí, tengo algunas reservas, pero no veo una mejor opción que f.(x) este momento. Parece mejor que elegir un símbolo arbitrario como ~ , y apuesto a que muchos de los que están acostumbrados a .* (etc.) podrían incluso adivinar de inmediato lo que significa.

Una cosa que me gustaría tener una mejor idea es si las personas están de acuerdo con _reemplazar_ las definiciones vectorizadas existentes con .( . Si a la gente no le gusta lo suficiente como para hacer el reemplazo, dudaría más.

Como usuario al acecho en esta discusión, me gustaría MUCHO usar esto para reemplazar mi código vectorizado existente.

Utilizo en gran medida la vectorización en julia para mejorar la legibilidad, ya que los bucles son rápidos. Así que me gusta usarlo mucho para exp, sin, etc., como se mencionó anteriormente. Como ya usaré .^, .* en tales expresiones agregando el punto extra a sin. Exp. etc. me parece muy natural, e incluso más explícito... especialmente cuando puedo incorporar fácilmente mis propias funciones con la notación general en lugar de mezclar sin(x) y map(f, x).

Todo para decir, como usuario habitual, ¡realmente espero que esto se fusione!

Me gusta más la sintaxis fun[vec] propuesta que fun.(vec) .
¿Qué opinas de [fun vec] ? Es como una lista de comprensión pero con una variable implícita. Podría permitir hacer T[fun vec]

Esa sintaxis es gratis en Julia 0.4 para vectores con longitud > 1:

julia> [sin rand(1)]
1x2 Array{Any,2}:
 sin  0.0976151

julia> [sin rand(10)]
ERROR: DimensionMismatch("mismatch in dimension 1 (expected 1 got 10)")
 in cat_t at abstractarray.jl:850
 in hcat at abstractarray.jl:875

Algo como [fun over vec] podría transformarse a nivel de sintaxis y tal vez valga la pena simplificar [fun(x) for x in vec] pero no es más simple que map(fun,vec) .

Sintaxis similares a [fun vec] : La sintaxis (fun vec) es gratuita y {fun vec} quedó en desuso.

julia> (fun vec)
ERROR: syntax: missing separator in tuple

julia> {fun vec}

WARNING: deprecated syntax "{a b ...}".
Use "Any[a b ...]" instead.
1x2 Array{Any,2}:
 fun  [0.3231600663395422,0.10208482721149204,0.7964663210635679,0.5064134055014935,0.7606900072242995,0.29583012284224064,0.5501131920491444,0.35466150455688483,0.6117729165962635,0.7138111929010424]

@diegozea , se descartó fun[vec] porque entra en conflicto con T[vec] . (fun vec) es básicamente sintaxis de Scheme, con el caso de múltiples argumentos presumiblemente siendo (fun vec1 vec2 ...) ... esto es bastante diferente a cualquier otra sintaxis de Julia. ¿O tenía la intención (fun vec1, vec2, ...) , que entra en conflicto con la sintaxis de tupla? Tampoco está claro cuál sería la ventaja sobre fun.(vecs...) .

Además, recuerde que un objetivo principal es eventualmente tener una sintaxis para reemplazar las funciones @vectorized (para que no tengamos un subconjunto "bendecido" de funciones que "funcionan en vectores"), y esto significa que el la sintaxis debe ser apetecible/intuitiva/conveniente para las personas acostumbradas a funciones vectorizadas en Matlab, Numpy, etcétera. También debe ser fácilmente componible para expresiones como sin(A .+ cos(B[:,1])) . Estos requisitos descartan muchas de las propuestas más "creativas".

sin.(A .+ cos.(B[:,1])) no se ve tan mal después de todo. Esto va a necesitar una buena documentación. ¿Se documentará f.(x) como .( similar a .+ ?
¿Se puede dejar obsoleto .+ en favor de +. ?

# Since 
sin.(A .+ cos.(B[:,1]))
# could be written as
sin.(.+(A, cos.(B[:,1])))
# +.
sin.(+.(A, cos.(B[:,1]))) #  will be more coherent.

@diegozea , #15032 ya incluye documentación, pero cualquier sugerencia adicional es bienvenida.

.+ seguirá deletreándose .+ . En primer lugar, esta ubicación del punto está demasiado arraigada y no se gana lo suficiente cambiando la ortografía aquí. En segundo lugar, como señaló @nalimilan , puede pensar en .( como un "operador de llamada de función vectorizado" y, desde esta perspectiva, la sintaxis ya es coherente.

(Una vez que se resuelvan las dificultades con el cálculo de tipos en broadcast (#4883), mi esperanza es hacer otro PR para que a .⧆ b para cualquier operador sea solo azúcar para una llamada a broadcast(⧆, a, b) . De esa manera, ya no necesitaremos implementar .+ etcétera explícitamente: obtendrá el operador de transmisión automáticamente simplemente definiendo + etc. aún podrá implementar métodos especializados, por ejemplo, llamadas a BLAS, sobrecargando broadcast para operadores particulares).

También debe ser fácilmente componible para expresiones como sin(A .+ cos(B[:,1])) .

¿Es posible analizar f1.(x, f2.(y .+ z)) como broadcast((a, b, c)->(f1(a, f2(b + c))), x, y, z) ?

Editar: Veo que ya se mencionó anteriormente... en el comentario oculto por defecto por @github...

@yuyichao , parece que la fusión de bucles debería ser posible si las funciones están marcadas como @pure (al menos si los tipos son inmutables), como comenté en #15032, pero esta es una tarea para el compilador, no el analizador (Pero la sintaxis vectorizada como esta es más conveniente que para exprimir el último ciclo de los bucles internos críticos).

Recuerde que el objetivo clave aquí es eliminar la necesidad de funciones @vectorized ; esto requiere una sintaxis al menos tan general, casi tan conveniente y al menos tan rápida. No requiere una fusión de bucles automatizada, aunque es bueno exponer la intención de broadcast del usuario al compilador para abrir la posibilidad de una fusión de bucles en una fecha futura.

¿Hay algún inconveniente si también fusiona bucles?

@yuyichao , la fusión de bucles es un problema mucho más difícil, y no siempre es posible incluso dejando de lado las funciones no puras (por ejemplo, vea el ejemplo exp(log(A) .- sum(A,1)) de @StefanKarpinski arriba). Esperar a que eso se implemente probablemente resultará en que _nunca_ se implemente, en mi opinión, tenemos que hacer esto de manera incremental. Comience por exponer la intención del usuario. Si podemos optimizar aún más en el futuro, genial. Si no, todavía tenemos un reemplazo generalizado para el puñado de funciones "vectorizadas" disponibles ahora.

Otro obstáculo es que .+ etc. no está actualmente expuesto al analizador como una operación broadcast ; .+ es solo otra función. Mi plan es cambiar eso (hacer .+ azúcar por broadcast(+, ...) ), como se indicó anteriormente. Pero nuevamente, es mucho más fácil progresar si los cambios son incrementales.

Lo que quiero decir es que hacer la fusión de bucles demostrando que es válido es difícil, por lo que podemos dejar que el analizador realice la transformación como parte de los esquemas. En el ejemplo anterior, se puede escribir como. exp.(log.(A) .- sum(A,1)) y se analizará como broadcast((x, y)->exp(log(x) - y), A, sum(A, 1)) .

También está bien si .+ aún no pertenece a la misma categoría (al igual que cualquier llamada de función que no se transmita por placa se incluirá en el argumento) e incluso está bien si solo hacemos esto (fusión de bucle) en un última versión. Principalmente estoy preguntando si es posible tener un esquema de este tipo (es decir, no ambiguo) en el analizador y si hay algún inconveniente al permitir el bucle escrito vectorizado y fusionado de esta manera.

hacer loop fusion demostrando que hacerlo es válido es difícil

Quiero decir que hacer eso en el compilador es difícil (tal vez no imposible), especialmente porque el compilador necesita investigar la complicada implementación de broadcast , a menos que tengamos un caso especial broadcast en el compilador, que es probablemente sea una mala idea y deberíamos evitarlo si es posible...

¿Quizás? Es una idea interesante, y no parece imposible definir la sintaxis .( como "fusionada" de esta manera, y dejar que la persona que llama no la use para funciones impuras. Lo mejor sería intentarlo y ver si hay casos difíciles (no veo ningún problema obvio en este momento), pero me inclino a hacer esto después de las relaciones públicas "sin fusión".

Me inclino a hacer esto después de las relaciones públicas "sin fusión".

Totalmente de acuerdo, especialmente porque .+ no se maneja de todos modos.

No quiero descarrilar esto, pero la sugerencia de @yuyichao me dio algunas ideas. El énfasis aquí está en qué funciones se vectorizan, pero eso siempre me parece un poco fuera de lugar: la verdadera pregunta es sobre qué variables vectorizar, lo que determina completamente la forma del resultado. Esta es la razón por la que me he inclinado a marcar argumentos para la vectorización, en lugar de marcar funciones para la vectorización. Marcar argumentos también permite funciones que vectorizan sobre un argumento pero no sobre otro. Dicho esto, podemos tener ambos y este PR tiene el propósito inmediato de reemplazar las funciones vectorizadas integradas.

@StefanKarpinski , cuando llama a f.(args...) o broadcast(f, args...) , se vectoriza sobre _todos_ los argumentos. (Para este propósito, recuerde que los escalares se tratan como matrices de dimensión 0). En la sugerencia de @yuyichao de f.(args...) = _fusión de sintaxis de transmisión_ (que me gusta cada vez más), creo que la fusión sería " stop" en cualquier expresión que no sea func.(args...) (para incluir .+ etc. en el futuro).

Entonces, por ejemplo, sin.(x .+ cos.(x .^ sum(x.^2))) se convertiría (en julia-syntax.scm ) en broadcast((x, _s_) -> sin(x + cos(x^_s_)), x, sum(broacast(^, x, 2))) . Observe que la función sum sería un "límite de fusión". La persona que llama sería responsable de no usar f.(args...) en los casos en que la fusión provocaría efectos secundarios.

¿Tiene un ejemplo en mente donde esto no sería suficiente?

que cada vez me gusta mas

Me alegro de que te guste. =)

Solo otra extensión más que probablemente no pertenezca a la misma ronda, podría ser posible usar .= , .*= o similar para resolver el problema de asignación en el lugar (haciéndolo distinto del asignación normal)

Sí, la falta de fusión para otras operaciones fue mi principal objeción a .+= etcétera en #7052, pero creo que eso se resolvería fusionando .= con otras llamadas func.(args...) . O simplemente fusionar x[:] = ... .

:thumbsup: Hay dos conceptos agrupados en esta discusión que, de hecho, son bastante ortogonales:
las "operaciones de transmisión fusionadas" de matlab'y o x .* y .+ z y apl'y "mapas en productos y zips" como f[product(I,J)...] y f[zip(I,J)...] . Hablar entre ellos también podría tener que ver con eso.

@mschauer , f.(I, J) ya es (en #15032) equivalente a map(x -> f(x...), zip(I, J) si I y J tienen la misma forma. Y si I es un vector de fila y J es un vector de columna o viceversa, entonces broadcast se mapea sobre el conjunto de productos (o puede hacer f.(I, J') si ambos son arreglos 1d). Así que no entiendo por qué piensas que los conceptos son "bastante ortogonales".

Ortogonal no era la palabra correcta, simplemente son lo suficientemente diferentes como para coexistir.

Sin embargo, el punto es que no necesitamos sintaxis separadas para los dos casos. func.(args...) puede admitir ambos.

Una vez que un miembro del triunvirato (Stefan, Jeff, Viral) fusione #15032 (que creo que está listo para fusionarse), cerraré esto y presentaré un problema de hoja de ruta para delinear los cambios propuestos restantes: corregir el cálculo de tipo de transmisión, obsoleto @vectorize , convierta .op en azúcar de transmisión, agregue "fusión de transmisión" a nivel de sintaxis y finalmente fusione con la asignación en el lugar. Los dos últimos probablemente no lleguen a 0.5.

Oye, estoy muy feliz y agradecido por 15032. Sin embargo, no desdeñaría la discusión. Por ejemplo, los vectores de vectores y objetos similares todavía son muy difíciles de usar en julia, pero pueden brotar como mala hierba como resultado de las comprensiones. Una buena notación implícita que no se base en codificar la iteración en dimensiones singleton tiene el potencial de facilitar esto mucho, por ejemplo, con los iteradores flexibles y las nuevas expresiones generadoras.

Creo que esto se puede cerrar ahora a favor de #16285.

¿Fue útil esta página
0 / 5 - 0 calificaciones