Una función es un código reutilizable que proporciona instrucciones para realizar una acción, como Dance() o Sleep(), y produce diferentes salidas en función de la entrada que se proporcione.
Las funciones proporcionan abstracción para comportamientos, lo que significa que estas funciones reutilizables ocultan los detalles de implementación que no son relevantes para otras partes de tu código y que no necesitas ver.
Utilicemos el pedido de comida de una carta como ejemplo de funciones y abstracción. La función para pedir comida podría ser algo así:
OrderFood(MenuItem : string) : food = {...}Cuando pides comida en un restaurante, le dices al camarero qué plato de la carta quieres,OrderFood("Ramen"). Desconoces cómo preparará el restaurante tu plato, pero esperas que te sirvan algo parecido a comida (food) después de pedirlo. Otros clientes pueden pedir diferentes platos de la carta y también esperar recibir su comida.
Por eso son útiles las funciones: solo hay que definir estas instrucciones en un lugar, en este caso, definiendo lo que debe ocurrir cuando alguien pide comida. A continuación, puedes reutilizar la función en diferentes contextos, por ejemplo, para cada cliente del restaurante que haga un pedido de la carta de comida.
Las siguientes secciones describen cómo crear una función y cómo utilizar una función una vez definida.
Cómo definir funciones
La firma de la función declara el nombre de la función (identificador), y la entrada (parámetros) y la salida (resultado) de la función.
Las funciones de Verse también pueden tener especificadores, que especifican cómo utilizar o implementar una función.
El cuerpo de la función es un bloque de código que define lo que hace la función cuando es llamada.
Las siguientes secciones explican estos conceptos de forma más detallada.
La sintaxis de la función tiene el siguiente aspecto:
Identifier(parameter1 : type, parameter2 : type) <specifier> : type = {}Parámetros
Un parámetro es una variable de entrada declarada en una firma de función y utilizada en el cuerpo de la función. Cuando se llama a una función, hay que asignar valores a los parámetros, si los hay. Los valores asignados se llaman argumentos de la función.
Una función puede no tener parámetros, por ejemplo, Sleep(), o tantos parámetros como se necesiten. Un parámetro se declara en la firma de función especificando un identificador y tipo entre los paréntesis (). Si tienes varios parámetros, deben estar separados por comas ,.
Por ejemplo:
Example(Parameter1 : int, Parameter2 : string) : string = {}Todos los siguientes son válidos:
Foo():void = {}
Bar(X:int):int = X
Baz(X:int, ?Y:int, ?Z:int = 0) = X + Y + ZLa sintaxis ?Y:int define un argumento con nombre con el nombre Y de tipo int.
La sintaxis ?Z:int = 0 define un argumento con nombre con el nombre Z de tipo int que no es necesario proporcionar cuando se llama a la función, pero utiliza 0 como su valor si no se proporciona.
Resultado
El resultado es la salida de una función cuando esa función es llamada. El tipo return especifica qué tipo de valor se puede esperar de la función si esta se ejecuta correctamente.
Si no quieres que tu función tenga un resultado, puedes establecer el tipo de valor de retorno en void. Las funciones con void como tipo de valor de retorno siempre devuelven el valor false, incluso cuando se especifica una expresión de resultado en el cuerpo de la función.
Especificadores
Además de los especificadores de la función que describen el comportamiento de la función definida, puede haber especificadores en el identificador (el nombre) de la función. Por ejemplo:
Foo<public>(X:int)<decides>:int = X > 0Este ejemplo define una función con el nombre Foo, que es de acceso público y tiene el efecto decides. Los especificadores después de la lista de parámetros y antes del tipo de valor de retorno describen la semántica de la función, y contribuyen al tipo de la función resultante. Los especificadores en el nombre de la función solo indican el comportamiento relacionado con el nombre de la función definida, como su visibilidad.
Cuerpo de la función
El cuerpo de la función es el bloque de código que define lo que hace la función. La función utiliza cualquier parámetro que se defina en la firma de función del cuerpo de la función para crear un resultado.
Una función devuelve automáticamente el valor producido por la última expresión ejecutada.
Por ejemplo:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2La función Bar() devuelve bien 1 o 2, dependiendo de si Foo[] falla.
Para forzar el retorno de un valor concreto (y la salida inmediata de la función), utiliza la expresión return.
Por ejemplo:
Minimum(X:int, Y:int):int =
if (X < Y):
return X
return YLa expresión return X saldrá de la función Minimum, devolviendo el valor contenido en X al autor de la llamada de la función. Ten en cuenta que si no se utilizan expresiones explícitas return aquí, la función devolverá por defecto la última expresión ejecutada. Esto daría lugar a que el valor de Y siempre se devolviera, lo que podría dar un resultado incorrecto.
Efectos
Los efectos en una función describen comportamientos adicionales que puede tomar la función cuando es llamada. Específicamente, el efecto decides en una función indica que la función podría fallar de una manera que el autor de la llamada podría necesitar controlar (o propagar a su autor de la llamada al ser también marcado como decides).
Por ejemplo:
Fail()<decides>:void = false?Esto define una función que siempre falla. Cualquier autor de llamada tendría que controlar o propagar el error. Observa la sintaxis: el efecto se describe como un especificador de la función. El tipo de una función con este efecto puede hacerse muy parecido a la definición de la función mediante la macro tipo:
type{_()<decides>void}Las funciones con el efecto decides también pueden devolver un valor si la función se ejecuta correctamente.
Por ejemplo:
First(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:t =
var ReturnOption:?t = false
for (Element : Array, F[Element], not ReturnOption?):
set ReturnOption = option{Element}
ReturnOption?Esta función determina el primer valor de la matriz que da como resultado una ejecución correcta de la función decides proporcionada. Esta función utiliza un tipo opción para mantener nuestro valor de retorno y decidir si la función tiene éxito o falla, ya que no se permiten sentencias return explícitas desde un contexto de fallo.
También puedes combinar los fallos con expresiones for. Una función decides con una expresión de fallo dentro de la expresión for solo tiene éxito si todas las iteraciones de la expresión for tienen éxito.
Por ejemplo:
All(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts><decides>:void =
for (Element : Array):
F[Element]Esta función solo tiene éxito si todos los elementos de Array dan como resultado un éxito para la función F. Si alguna entrada de Array provoca un fallo en la función F, la función All lo hará.
También puede utilizar la combinación de fallo con una expresión for para filtrar una entrada en función de las entradas que dan como resultado un éxito o un fallo.
Por ejemplo:
Filter(Array:[]t, F(:t)<transacts><decides>:void where t:type)<transacts>:[]t =
for (Element : Array, F[Element]):
ElementEsta función devuelve una matriz que contiene solo los elementos de Array que tienen como resultado el éxito de la función F.
Llamar a funciones
Una llamada a función es una expresión que evalúa (lo que se conoce como llamar o invocar) una función.
Hay dos formas de llamar a una función en Verse:
FunctionName(Arguments): esta forma requiere que la llamada a función tenga éxito y puede utilizarse en cualquier contexto.FunctionName[Arguments]: esta forma significa que la llamada a función puede fallar. Para utilizar esta forma, la función debe definirse con el especificador<decides>y llamarse en un contexto de fallo.
La invocación se realiza mediante el uso de paréntesis si la función no tiene el efecto decides. Por ejemplo:
Foo()
Bar(1)
Baz(1, ?Y := 2)
Baz(3, ?Y := 4, ?Z := 5)
Baz(6, ?Z := 7, ?Y := 8)Observa cómo los argumentos con nombre, por ejemplo ?Y:int, se pasan haciendo referencia al nombre precedido de ? y proporcionando un valor a la derecha de :=. Ten en cuenta también que el argumento con nombre ?Z es opcional. Es importante destacar que el orden de los argumentos nombrados en el lugar de la llamada es irrelevante, excepto por cualquier efecto secundario que pueda ocurrir mientras se produce el valor del argumento nombrado.
Para invocar una función que tiene el efecto decides, se deben utilizar corchetes. Esto permite que la indexación de matrices, que incurre en el efecto decides, siga una sintaxis similar a la de las funciones marcadas con el efecto decides. Por ejemplo:
Foo()<decides>:void = {}
Bar():int =
if (Foo[]):
1
else:
2Desempaquetado de tuplas
Una función que acepta múltiples argumentos es idéntica cuando se invoca de una función que acepta un único argumento de tupla con elementos de los mismos tipos que los argumentos múltiples. La falta de distinción cuando se invoca también se aplica al tipo de cada función: tienen el mismo tipo.
Por ejemplo:
Second(:any, X:t where t:type):t = XEsto es equivalente a:
Second(X:tuple(any, t) where t:type):t = X(1)Ambos pueden invocarse como:
X := 1
Y := 2
Second(X, Y)o
X:tuple(int, int) = (1, 2)
Second(X)Ambos satisfacen el tipo type{_(:any, :t where t:type):t}.
El tipo de función
El tipo de una función se compone de su tipo de parámetro (definido potencialmente como tupla sin empaquetar), su efecto y su tipo de resultado. Por ejemplo:
type{_(:type1, :type2)<effect1>:type3}Este es el tipo de una función que toma dos argumentos de type1 y type2 (o, de forma equivalente, un argumento de tipo tuple(type1, type2)), produce el efecto effect1 y devuelve un valor de tipo type3.
Sobrecarga
Varias funciones pueden compartir el mismo nombre siempre que no haya argumentos que satisfagan más de una función de este tipo. Esto se conoce como sobrecarga.
Por ejemplo:
Next(X:int):int = X + 1
Next(X:float):float = X + 1
int_list := class:
Head:int
Tail:?int_list = false
Next(X:int_list)<decides>:int_list = X.Tail?No hay solapamiento en los argumentos que aceptan estas funciones. La función correcta a invocar puede ser resuelta inequívocamente por los tipos proporcionados. Por ejemplo:
Next(0)
Next(0.0)
Next(int_list{Head := 0, Tail := int_list{Head := 1}})Sin embargo, se rechaza lo siguiente:
First(X:int, :any):int = X
First(X:[]int)<decides>int = X[0]Esto se debe a que la tupla y la matriz tienen una relación de subtipificado: la matriz es un supertipo de tupla cuando el tipo base de matriz es un supertipo de todos los tipos de elementos de tupla. Por ejemplo:
X := (1, 2)
First(X)En este ejemplo, la llamada a First puede satisfacerse mediante cualquiera de las dos definiciones de First. En el caso de las clases y las interfaces, no se puede producir una sobrecarga, ya que una clase puede modificarse posteriormente para implementar una interfaz o dos clases pueden modificarse para tener una relación de herencia. En su lugar, se debe utilizar el método de anulación. Por ejemplo:
as_int := interface:
AsInt():int
ToInt(X:as_int):int = X.AsInt()
thing1 := class(as_int):
AsInt():int = 1
thing2 := class(as_int):
AsInt():int = 2