La versión beta de Move 2024 ahora incluye
macro
¡funciones!
Las funciones macro en Move son similares a las funciones normales: se definen los parámetros, el tipo de retorno y la lógica que determina cómo funcionan las funciones macro y, a continuación, se pueden llamar a las funciones macro desde cualquier parte del código. Sin embargo, a diferencia de las funciones normales, el compilador expande esas funciones para ejecutar las operaciones definidas en línea.
Las principales diferencias que separan las funciones macro de las funciones normales son:
Extensiones de sintaxis de funciones macro
Mueva la expansión del compilador y la semántica de llamadas
Parámetros lambda
Extensiones de sintaxis: la sintaxis Move especifica cómo definir las funciones macro, sus argumentos y su invocación. Estas reglas no solo ayudan al compilador a diferenciar las macros de las funciones normales, sino que también proporcionan una diferenciación visual entre las funciones normales y las macro durante el uso.
Expansión del compilador y semántica de llamadas: Move evalúa con entusiasmo los argumentos de funciones normales. En otras palabras, el entorno de ejecución evalúa por completo los argumentos que pasa a una función normal, ya sea que sean necesarios para la lógica de la función o no. Por el contrario, las funciones macro sustituyen directamente las expresiones por sus argumentos durante la expansión, y el entorno de ejecución no verá la llamada a la macro en absoluto. Esto significa que los argumentos no se evalúan en absoluto si nunca se llega al código del cuerpo de la macro con el argumento. Por ejemplo, si el argumento se sustituye en una expresión if-else y nunca se toma la rama con ese argumento, el argumento no se evaluará.
Parámetros Lambda: además de las expresiones normales, las funciones Macro permiten proporcionar código parametrizado como argumentos de función de orden superior. Esto es posible gracias a la introducción del tipo Lambda, que permite a los autores de macros crear macros potentes y flexibles.
Para obtener más detalles sobre cómo definir funciones macro, consulte la referencia oficial en el libro Move.
Anatomía de las funciones macro
El compilador Move expande las funciones macro durante la compilación, lo que permite una expresividad que no está disponible en las funciones normales. Para indicar este comportamiento de sustitución especial, las funciones macro tienen sus parámetros de tipo y de expresión prefijados con un
$
carácter. Considere una función Move llamada `my_assert` que se comporta como el primitivo del compilador assert! (sin errores inteligentes).
macro fun my_assert($cond: bool, $code: u64) { si (!$cond) aborta $code. }
El compilador Move sustituye los argumentos por parámetros de tipo y de expresión durante la expansión. Este comportamiento crea el proceso de evaluación diferida para los argumentos mencionados en la sección anterior. Como muestra el código, la macro `my_assert` verifica la condición que se le pasa y cancela el programa si la condición `$cond` se resuelve como
FALSO
(o no "verdadero") con el "$código" pasado como segundo argumento.
Para invocar el
mi_afirmación
función macro, el programador utiliza un carácter ! después del nombre de la función para indicar visualmente que los argumentos no se evalúan con entusiasmo:
my_assert!(vector[] == vector[], 0); my_assert!(true, 1 / 0); // ¡no se abortará! '1 / 0' no se evalúa.
Como habrás notado, el
$código
El valor pasado a la macro en la segunda invocación incluye una operación de división por cero. Aunque es un poco artificial, su propósito es mostrar que el compilador no evaluará la expresión porque el flujo de control nunca llegará a su ejecución debido a la condición verdadera provista.
Macros con argumentos lambda
La incorporación de funciones macro incluye la introducción de una característica completamente nueva: lambda. Al utilizar parámetros con tipos lambda, puede pasar código del llamador al cuerpo de la macro. Si bien la sustitución se realiza en tiempo de compilación, se utilizan de manera similar a las funciones anónimas, lambdas o cierres en otros lenguajes.
Por ejemplo, considere un bucle que ejecuta una lambda en cada número de
0
a un número n que proporciona el código.
macro pública fun do($n: u64, $f: |u64| -> ()) { let mut i = 0; let stop = $n; while (i < stop) { $f(i); i = i + 1; } }
El parámetro lambda (
$f
) es el segundo argumento de la macro do. El tipo de lambda se define como |u64| -> (), que toma un u64 como entrada y devuelve (). El tipo de retorno de () es implícito, por lo que el argumento también podría escribirse simplemente como $f: |u64|.
La lógica de su programa podría entonces usar esta macro para sumar los números de
0
hasta 10 usando el parámetro lambda:
sea mut suma = 0; hacer!(10, |i| suma = suma + i);
Aquí, el fragmento de código `|i| sum = sum + i` define la expresión lambda para la invocación de la macro. Cuando se la llama en el cuerpo de la macro, sus argumentos se evalúan y se vinculan en el cuerpo de la expresión lambda. Tenga en cuenta que, a diferencia de las llamadas a macros, las expresiones lambda evaluarán completamente sus argumentos antes de ejecutar el cuerpo de la expresión lambda.
Las funciones macro son higiénicas, por lo que la i en la lambda no es la misma que la i en la do. Consulte la referencia de Move para obtener más detalles.)
Macros en la biblioteca estándar de Move
Con la adición de macros, la
mover-stdlib
La biblioteca introduce una serie de funciones macro para capturar patrones de programación comunes.
Macros de números enteros
Para
u64
y otros tipos de enteros, la biblioteca move-stdlib proporciona un conjunto de funciones macro que simplifican los bucles comunes. Esta publicación examina el tipo u64, pero las mismas funciones macro están disponibles para u8, u16, u32, u128 y u256.
Ya has visto un ejemplo, ya que el código de la función do es una función macro en std::u64.
La lista completa de funciones macro actualmente disponibles para tipos enteros es:
macro pública fun do($stop: u64, $f: |u64|)
Bucles que aplican $f a cada número desde 0 hasta $stop (exclusivo).
macro pública fun do_eq($stop: u64, $f: |u64|)
Bucles que aplican $f a cada número desde 0 hasta $stop (incluido).
macro pública fun range_do($inicio: u64, $fin: u64, $f: |u64|)
Bucles que aplican $f a cada número desde $start hasta $stop (exclusivo).
macro pública fun range_do_eq($inicio: u64, $fin: u64, $f: |u64|)
Bucles que aplican $f a cada número desde $start hasta $stop (inclusive).
Para un ejemplo simplificado de aplicación de las funciones macro definidas, considere el siguiente bucle:
sea mut i = 0; mientras (i < 10) { foo(i); i = i + 1; }
Podrías reescribir este bucle implementando el
hacer
función macro para std::u64 como:
10u64.do!(|i| foo(i));
De manera similar, puedes hacer que el siguiente bucle sea más conciso usando el
Función macro u64::range_do_eq
:
sea mut i = 1; // recorre el bucle de 1 a 10^8 de 10 en 10 segundos while (i <= 100_000_000) { foo(i); i = i * 10; }
El resultado de la reescritura:
1u64.range_do_eq!(8, |i| foo(10u64.pow(i)));
Si bien esto es potencialmente más fácil de leer, se necesitará más gas para ejecutarlo.
vector
macros
En un espíritu similar al de los números enteros.
hacer
macro, puedes iterar sobre los elementos de un vector usando la función do.
vector[1, 2, 3, 4, 5].do!(|x| foo(x));
Esto es equivalente a:
sea mut v = vector[1, 2, 3, 4, 5]; v.reverse(); mientras (!v.is_empty()) { foo(v.pop_back()); }
El código expandido muestra que este proceso consume el vector. Utilice el
hacer_ref
y macros do_mut para iterar por referencia o por referencia mutable, respectivamente.
diversión check_coins(monedas: &vector<Moneda<SUI>>) { monedas.do_ref!(|moneda| assert!(moneda.valor() > 0)); /* se expande a let mut i = 0; let n = monedas.len(); while (i < n) { let moneda = &monedas[i]; assert!(moneda.valor() > 0); i = i + 1; } */ } fun tomar_10(monedas: &mut vector<Moneda<SUI>>) { monedas.do_mut!(|moneda| transferencia::transferencia_pública(moneda.toma(10), @0x42)); /* se expande a dejar mut i = 0; dejar n = monedas.len(); mientras (i < n) { dejar moneda = &mut monedas[i]; transferencia::transferencia_pública(moneda.toma(10), @0x42); i = i + 1; } */ }
Además de la iteración, puedes modificar y crear vectores con macros.
vector::tabulador
crea un vector de longitud n aplicando una lambda a cada índice.
diversión potencias_de_2(n: u64): vector<u64> { vector::tabulate(n, |i| 2u64.pow(i)) /* se expande a let mut v = vector[]; let mut i = 0; while (i < n) { v.push_back(2u64.pow(i)); i = i + 1; }; v */ }
vector::mapa
Crea un nuevo vector a partir de un vector existente aplicando una lambda a cada elemento. Mientras que vector::map opera en un vector por valor, las macros map_ref y map_mut operan en un vector por referencia y referencia mutable, respectivamente.
diversión into_balances(monedas: vector<Moneda<SUI>>): vector<Saldo<SUI>> { monedas.map!(|moneda| moneda.into_balance()) /* se expande a let mut v = vector[]; monedas.reverse(); while (!monedas.is_empty()) { let moneda = monedas.pop_back(); v.push_back(moneda.into_balance()); }; v */ }
Similar al mapa,
vector::filtro
crea un nuevo vector a partir de un vector existente conservando solo los elementos que satisfacen un predicado.
fun números distintos de cero(números: vector<u64>): vector<u64> { números.filter!(|n| n > 0) /* se expande a let mut v = vector[]; números.reverse(); while (!números.is_empty()) { let n = números.pop_back(); if (n > 0) { v.push_back(n); } }; v */ }
Consulte la Tabla A al final de este artículo para obtener una lista de las funciones macro actualmente disponibles para vectores.
opción
macros
Mientras
Opción
no es una colección con más de un elemento, el tipo tiene macros do (y do_ref y do_mut) para acceder fácilmente a su valor si es alguno.
fun maybe_transfer(moneda: Opción<Moneda<SUI>>, destinatario: dirección) { moneda.do!(|c| transferencia::transferencia_pública(c, destinatario)) /* se expande a si (moneda.es_alguna()) transferencia::transferencia_pública(moneda.destruir_alguna(), destinatario) de lo contrario moneda.destruir_ninguna() */ }
destruir
realiza la misma acción que do, pero quizás una macro más útil sea destroy_or, que proporciona una expresión predeterminada, que se evalúa solo si la opción es ninguna. El siguiente ejemplo encadena map, que aplica la lambda al valor de la opción si es alguna, y destroy_or para convertir de manera concisa una opción<Coin<SUI>> en un saldo<SUI>.
divertido para_equilibrar_optar(moneda: Opción<Moneda<SUI>>): Saldo<SUI> { moneda.map!(|c| c.into_equilibrio()).destruir_o!(Saldo::cero()) /* se expande a let opt = if (moneda.es_alguna()) Opción::alguna(monedas.destruir_alguna().into_equilibrio()) else Opción::ninguna(); if (opt.es_alguna()) opt.destruir_alguna() else Saldo::cero() */ }
Consulte la Tabla B al final de este artículo para obtener una lista de las funciones macro actualmente disponibles para Option.
Envolver
Las funciones macro pueden simplificar su código al convertir patrones comunes en macros reutilizables. La esperanza es que las funciones macro puedan reemplazar muchos, si no todos, sus bucles comunes (bucle y while). Esté atento a más funciones macro en la biblioteca move-stdlib y, pronto, en el marco sui.
Para obtener más detalles sobre cómo funcionan las funciones macro, consulte el libro Move.
Tabla A: Funciones macro disponibles actualmente para vectores
Anteponer
Diversión macro pública
a cada línea a continuación:
tabular<$T>($n: u64, $f: |u64| -> $T): vector<$T>
Crea un vector de longitud
norte
llamando a la función f en cada índice.
destruir<$T>($v: vector<$T>, $f: |$T|)
Destruir el vector
v
Al llamar a f en cada elemento y luego destruir el vector, no se conserva el orden de los elementos en el vector (comienza desde el final del vector).
hacer<$T>($v: vector<$T>, $f: |$T|)
Destruir el vector
v
Al llamar a f en cada elemento y luego destruir el vector, se conserva el orden de los elementos en el vector.
hacer_ref<$T>($v: &vector<$T>, $f: |&$T|)
Realizar una acción
F
en cada elemento del vector v. El vector no se modifica.
hacer_mut<$T>($v: &mut vector<$T>, $f: |&mut $T|)
Realizar una acción
F
en cada elemento del vector v. La función f/ toma una referencia mutable al elemento.
mapa<$T, $U>($v: vector<$T>, $f: |$T| -> $U): vector<$U>
Mapear el vector
v
a un nuevo vector aplicando la función f a cada elemento. Conserva el orden de los elementos en el vector, primero se llama primero.
mapa_ref<$T, $U>($v: &vector<$T>, $f: |&$T| -> $U): vector<$U>
Mapear el vector
v
a un nuevo vector aplicando la función f a cada elemento. Conserva el orden de los elementos en el vector, primero se llama primero.
filtro<$T: soltar>($v: vector<$T>, $f: |&$T| -> bool): vector<$T>
Filtrar el vector
v
Aplicando la función f a cada elemento, se devuelve un nuevo vector que contiene solo los elementos para los que f devuelve verdadero.
partición<$T>($v: vector<$T>, $f: |&$T| -> bool): (vector<$T>, vector<$T>)
Dividir el vector
v
en dos vectores aplicando la función f a cada elemento. Devuelve una tupla que contiene dos vectores: el primero contiene los elementos para los que f devuelve verdadero y el segundo contiene los elementos para los que f devuelve falso.
find_index<$T>($v: &vector<$T>, $f: |&$T| -> bool): Opción<u64>
Encuentra el índice del primer elemento del vector.
v
que satisface el predicado f. Devuelve some(index) si se encuentra dicho elemento; de lo contrario, none().
contar<$T>($v: &vector<$T>, $f: |&$T| -> bool): u64
Cuenta cuántos elementos hay en el vector
v
satisface el predicado f.
doblar<$T, $Acc>($v: vector<$T>, $init: $Acc, $f: |$Acc, $T| -> $Acc): $Acc
Reducir el vector
v
a un único valor aplicando la función f a cada elemento. Similar a fold_left en Rust y reduce en Python y JavaScript.
cualquier<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool
Si algún elemento del vector
v
satisface el predicado f. Si el vector está vacío, devuelve falso.
todo<$T>($v: &vector<$T>, $f: |&$T| -> bool): bool
Si todos los elementos del vector
v
satisface el predicado f. Si el vector está vacío, devuelve verdadero.
zip_do<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)
Destruye dos vectores
v1
y
v2
llamando a f para cada par de elementos. Se cancela si los vectores no tienen la misma longitud. Se conserva el orden de los elementos en los vectores.
zip_do_reverse<$T1, $T2>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2|)
Destruye dos vectores
v1
y
v2
llamando a f para cada par de elementos. Se cancela si los vectores no tienen la misma longitud. Comienza desde el final de los vectores.
zip_do_ref<$T1, $T2>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2|)
Iterar a través de
v1
y
v2
y aplicar la función f a las referencias de cada par de elementos. Los vectores no se modifican. Se cancela si los vectores no tienen la misma longitud. Se conserva el orden de los elementos en los vectores.
zip_do_mut<$T1, $T2>($v1: &mut vector<$T1>, $v2: &mut vector<$T2>, $f: |&mut $T1, &mut $T2|)
Iterar a través de
v1
y v2 y aplicar la función f a las referencias mutables de cada par de elementos. Los vectores pueden modificarse. Se cancela si los vectores no tienen la misma longitud. Se conserva el orden de los elementos en los vectores.
mapa_zip<$T1, $T2, $U>($v1: vector<$T1>, $v2: vector<$T2>, $f: |$T1, $T2| -> $U): vector<$U>
Destruye dos vectores
v1
y
v2
aplicando la función f a cada par de elementos. Los valores devueltos se recopilan en un nuevo vector. Se cancela si los vectores no tienen la misma longitud. Se conserva el orden de los elementos en los vectores.
zip_map_ref<$T1, $T2, $U>($v1: &vector<$T1>, $v2: &vector<$T2>, $f: |&$T1, &$T2| -> $U): vector<$U>
Iterar a través de
v1
y v2 y aplicar la función f a las referencias de cada par de elementos. Los valores devueltos se recopilan en un nuevo vector. Se cancela si los vectores no tienen la misma longitud. Se conserva el orden de los elementos en los vectores.
Tabla B: Funciones macro actualmente disponibles para la opción
Anteponer
Diversión macro pública
a cada línea a continuación:
destruir<$T>($o: Opción<$T>, $f: |$T|)
Destruir
Opción<T>
y llamar al cierre f en el valor dentro si contiene uno.
hacer<$T>($o: Opción<$T>, $f: |$T|)
Destruir
Opción<T>
y llamar al cierre f en el valor dentro si contiene uno.
do_ref<$T>($o: &Opción<$T>, $f: |&$T|)
Ejecutar un cierre en el valor interior
a
si tiene uno
do_mut<$T>($o: &mut Opción<$T>, $f: |&mut $T|)
Ejecutar un cierre en la referencia mutable al valor dentro
a
si tiene uno
o<$T>($o: Opción<$T>, $predeterminado: Opción<$T>): Opción<$T>
Seleccione el primero
Alguno
Valor de las dos opciones, o Ninguno si ambas son Ninguno. Equivalente a a.or(b) de Rust.
y<$T, $U>($o: Opción<$T>, $f: |$T| -> Opción<$U>): Opción<$U>
Si el valor es
Alguno
, llama al cierre f sobre él. De lo contrario, devuelve None. Equivalente a t.and_then(f) de Rust.
y_ref<$T, $U>($o: &Opción<$T>, $f: |&$T| -> Opción<$U>): Opción<$U>
Si el valor es
Alguno
, llama al cierre f sobre él. De lo contrario, devuelve None. Equivalente a t.and_then(f) de Rust.
mapa<$T, $U>($o: Opción<$T>, $f: |$T| -> $U): Opción<$U>
Mapa de un
Opción<T>
a Option<U> aplicando una función a un valor contenido. Equivalente a t.map(f) de Rust.
map_ref<$T, $U>($o: &Opción<$T>, $f: |&$T| -> $U): Opción<$U>
Mapa de un
Opción<T>
Valor de Option<U> mediante la aplicación de una función a un valor contenido por referencia. Se conserva la Option<T> original. Equivalente a t.map(f) de Rust.
filtro<$T: drop>($o: Opción<$T>, $f: |&$T| -> bool): Opción<$T>
Devolver
Ninguno
si el valor es Ninguno, de lo contrario devuelve Option<T> si el predicado f devuelve verdadero.
es_alguna_y<$T>($o: &Opción<$T>, $f: |&$T| -> bool): bool
Devolver
FALSO
si el valor es Ninguno, de lo contrario devuelve el resultado del predicado f.
destruir_o<$T>($o: Opción<$T>, $predeterminado: $T): $T
Destruir
Opción<T>
y devuelve el valor dentro si contiene uno, o el valor predeterminado en caso contrario. Equivalente a t.unwrap_or(default) de Rust.
Nota: esta función es una versión más eficiente de
destruir_con_valor_predeterminado
, ya que no evalúa el valor predeterminado a menos que sea necesario. La función destroy_with_default debería dejar de utilizarse en favor de esta función.