Una función es un código reutilizable que ofrece instrucciones para realizar una acción, como Dance() o Sleep(), y produce diferentes salidas en función de la entrada que se brinde.
Las funciones ofrecen abstracción para los 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 un menú 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 del menú quieres, OrderFood("Ramen"). No sabes cómo el restaurante preparará tu plato, pero esperas recibir algo que se considere comida después de pedirlo. Otros clientes pueden pedir diferentes platos del menú y también esperar recibir su comida.
Por eso son útiles las funciones: sólo 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 del menú de comida.
En las secciones siguientes, se describe cómo crear una función y cómo utilizar una función una vez definida.
Cómo definir funciones
La firma de función declara el nombre de la función (identificador), la entrada (parámetros) y la salida (resultado) de la función.
Las funciones de Verse también pueden tener especificadores, los cuales especifican cómo usar 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 secciones siguientes explican estos conceptos con más detalle.
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 denominan argumentos de la función.
Una función puede no tener parámetros —por ejemplo, Sleep()— o tantos parámetros como se necesiten. Declaras un parámetro en la firma de función especificando un identificador y un 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 denominado con el nombre Y de tipo int.
La sintaxis ?Z:int = 0 define un argumento denominado con el nombre Z de tipo int que no es necesario indicar cuando se llama a la función, pero utiliza 0 como su valor si no se indica.
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 se ejecuta con éxito.
Si no quieres que tu función tenga un resultado, puedes establecer el tipo return en void. Las funciones con void como tipo return 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 > 0En este ejemplo, se define una función llamada 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 return 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 sólo indican el comportamiento relacionado con el nombre de la función definida, como su visibilidad.
Cuerpo de 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 1 o 2, en función de si Foo[] falla.
Para forzar la devolución 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, que devolverá el valor contenido en X al llamador de la función. Ten en cuenta que, si no se utilizan aquí expresiones return explícitas, la función devolverá la última expresión ejecutada de forma predeterminada. Esto daría como resultado que el valor de Y siempre se devuelva, 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 llamador podría necesitar controlar (o propagar a su llamador al marcarse también como decides).
Por ejemplo:
Fail()<decides>:void = false?Esto define una función que siempre falla. Cualquier llamador 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 tiene éxito.
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 matriz que resulta en una ejecución exitosa de la función decides brindada. Esta función utiliza un tipo option para contener nuestro valor devuelto y decidir si la función tiene éxito o falla, ya que no se permiten instrucciones return explícitas dentro de un contexto de fallo.
También puedes combinar las expresiones de fallo con 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 la matriz dan como resultado un éxito para la función F. Si alguna entrada de la matriz da como resultado un fallo para la función F, la función Todo dará como resultado un fallo.
También puedes utilizar la combinación de una expresión de fallo con una expresión for para aplicar un filtro a una entrada en función de qué entradas tengan éxito o fracasen.
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 la matriz que dan como resultado el éxito de la función F.
Cómo llamar funciones
Una llamada a una 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 la función tenga éxito y pueda utilizarse en cualquier contexto.FunctionName[Arguments]: esta forma significa que la llamada a la 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 por ? y brindando un valor a la derecha de :=. Ten en cuenta también que el argumento de 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 indistinguible cuando se invoca de una función que acepta un único argumento tuple con elementos de los mismos tipos que los múltiples argumentos. 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 (potencialmente definido como una 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 equivalentemente, 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 subtipado: 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 ser satisfecha por cualquier definición de First. En el caso de las clases y las interfaces, no se puede producir una sobrecarga, ya que una clase puede ser modificada posteriormente para implementar una interfaz o dos clases pueden ser modificadas 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