Les funcions son blocs de codi que es poden reutilitzar en altres parts del codi.

Introducció

Funció

Declaració

A continuació tens un exemple de declaració d'una funció:

function sum(a: number, b: number): number {
    return a + b
}

La paraula reservada function s'utilitza per definir una funció.

A continuació va el nom de la funció:

function sum(a: number, b: number): number {
         ///  
    return a + b
}

A continuació es defineixen els paràmetres de la funció amb el seu tipus:

function sum(a: number, b: number): number {
            //////////////////////  
    return a + b
}

Es pot declarar el tipus del valor que retorna la funció (opcional):

function sum(a: number, b: number): number {
                                  ////////  
    return a + b
}

I a continuació és declara el cos de la funció (entre {})

La paraula return finalitza l'execució de la funció i retorna el valor corresponent

Invocar una funció és molt senzill:

const result = sum(3, 4)
console.log(result)

Cos d'una funció

El cos d'una funció pot utilitzar totes les diferents característiques del llenguatge:

  • variables
  • expressions if/ else
  • bucles while i for
  • invocar altres mètodes
  • definir altres funcions

A continuació tens un exemple:

function happySum(a: number, b: number) {
    
    function double(a: number) {
        return a * 2
    }

    if (a > 5) {
        a += 1000
    }

    return double(a + b)
}

const result = happySum(10, 50)
console.log(result) // 2120

Potser el que més et sorpren és que pots declarar una funció dins d'una funció 😯

Variables

Una variable declarada dins d'una funció només és visible dins de la funció.

Per això tenen el nom de variables locals (a la funció)

Per exemple,

function showMessage() {
    let message = "Hello, I'm TypeScript!" // local variable
    console.log(message)
}

showMessage() // Hello, I'm JavaScript!
console.log(message) // <-- Error! The variable is local to the function

En canvi, una variable declarara fora d'una funció és accessible per qualsevol funció.

let userName = 'John'

function showMessage() {
    let message = `Hello, ${userName}`
    alert(message)
}

showMessage() // Hello, John

Aquestes variables s'anomenen variables globals, i no s'han d'utilitzar.

La función tiene acceso completo a la variable externa.

Puede modificarlo también:

let userName = 'John'

function showMessage() {
    userName = 'Bob'
    let message = `Hello, ${userName}`
    alert(message)
}

showMessage() // Hello, Bob

La variable externa solo se usa si no hay una local.

Si una variable con el mismo nombre se declara dentro de la función, le hace sombra a la externa.

Por ejemplo, en el siguiente código la función usa la variable local userName i la variable exterior se ignora:

let userName = 'John'

function showMessage() {
    let userName = 'Alice'
    let message = `Hello, ${userName}`
    alert(message)
}

showMessage() // Hello, Alice

Parámetros

Podemos pasar datos arbitrarios a funciones usando parámetros.

En el siguiente ejemplo, la función tiene dos parámetros: from y text.

function showMessage(from: string, text: string) { // parameters: from, text
    console.log(`${from}: ${text} `)
}

showMessage('Ann', 'Hello!') // Ann: Hello!
showMessage('Ann', "What's up?") // Ann: What's up?

Cuando la función se llama (*) y (**), los valores dados se copian en las variables locales from y text. Y la función las utiliza.

Aquí hay un ejemplo más: tenemos una variable from y la pasamos a la función. Ten en cuenta: la función cambia from, pero el cambio no se ve afuera, porque una función siempre obtiene una copia del valor:

function showMessage(from: string, text: string) {
    from = from.toUpperCase() // hace que "from" se vea mejor
    console.log(`${from}: ${text}`)
}

let from = "Ann"

showMessage(from, "Hello") // ANN: Hello

// el valor de "from" es el mismo, la función modificó una copia local
console.log(from) // Ann

Cuando un valor es pasado como un parámetro de función, también se denomina argumento.

Para poner los términos claros:

  • Un parámetro es una variable listada dentro de los paréntesis en la declaración de función (es un término para el momento de la declaración)

  • Un argumento es el valor que es pasado a la función cuando esta es llamada (es el término para el momento en que se llama).

Declaramos funciones listando sus parámetros, luego las llamamos pasándoles argumentos.

En el ejemplo de arriba, se puede decir: “la función showMessage es declarada con dos parámetros, y luego llamada con dos argumentos: from y "Hola"”.

Devolviendo un valor

Una función puede devolver un valor al código de llamada como resultado.

El ejemplo más simple sería una función que suma dos valores:

function sum(a: number, b: number) {
    return a + b
}

let result = sum(1, 2)
console.log(result) // 3

La directiva return puede estar en cualquier lugar de la función. Cuando la ejecución lo alcanza, la función se detiene y el valor se devuelve al código de llamada (asignado al result anterior).

Puede haber muchos return en una sola función. Por ejemplo:

function checkAge(age: number) {
    if (age > 18) {
        return true
    } else {
        return confirm('¿Tienes permiso de tus padres?')
    }
}

let age = Number(prompt('¿Qué edad tienes?', "18"))

if (checkAge(age)) {
    alert('Acceso otorgado')
} else {
    alert('Acceso denegado')
}

Es posible utilizar return sin ningún valor. Eso hace que la función salga o termine inmediatamente.

Por ejemplo:

function showMovie(age: number) {
    if ( !checkAge(age) ) {
      return
    }

    alert( "Mostrándote la película" )
    // ...
  }

Paràmetres opcionals

Una funció pot tenir paràmetres opcionals.

Has d'utilitzar el simbol ? per marcar un paràmetre com opcional.

function f(x?: number) {
    if (x != undefined)
        console.log(x + 10)
}

f() // OK
f(10) // OK

Encara que el paràmetre s'ha especificat com de tipus number, el paràmetre en realitat té el tipus number | undefined.

Els paràmetres opcionals s'han de posar al final.

Paràmetres per defecte

Una altra opció és proporcionar un valor per defecte:

function f(x: number = 10) {
    console.log(x + 10)
}

f() // 20
f(10) // 20

Ara el paràmetre x sempre serà de tipus number perqué qualsevol argument undefined es reemplaza pel valor 10.

A més, quan un paràmetre és opcional, si vols pots passar undefined com argument i el resultat serà el mateix:

function f(x: number = 10) {
    console.log(x + 10)
}

f() // 20
f(10) // 20
f(undefined) // 20

Paràmetres rest

Pots definir funcions que prenen un nombre il·limitat d'arguments utilitzant parametres rest.

Un paràmetre rest apareix després de tots els altres paràmetres i utilitza la sintaxi ...:

function max(x: number, ...xs: number[]) {
    let result = x
    for (const x of xs) {
        if (x > result)
            result = x
    }
    return result
}

console.log(max(10)) // 10
console.log(max(10, 30, 15)) // 30

Variables de funció

A continuació pots veure com pots crear una funció anònima:

(function () { console.log("Hello") })

El motiu pel qual s'anomena anònim és perquè no està assignat a una variable i, per tant, no té nom.

Si executes el codi no passa res:

> bun test.ts

Si vols, pots crear i executar la funció amb ():

(function () { console.log("Hello") }) ()
                                       //

Ara, si executes el codi, es crea la funció anònima i s'executa:

> bun test.ts
Hello

Tanmateix, una funció anònima, també coneguda com a literal de funció, es pot assignar a una variable per crear una variable de funció:

const sayHi = function() { console.log("Hello") }

Això crea una variable de funció anomenada sayHi.

En aquesta expressió, la funció literal original es troba a la part dreta del símbol =:

const sayHi = function() { console.log("Hello") }
              ///////////////////////////////////

i el nou nom de la variable es troba al costat esquerre:

const sayHi = function() { console.log("Hello") }
      /////

Paràmetres

Una funció anònima és com qualsevol funció: pot prendre paràmetres i retorna valors

(function (a: number) { return a * 2 })

I es crida com qualsevol funció:

const result = (function (a: number) { return a * 2 }) (2)
console.log(result) // 4

I la podem asignar a una variable:

const double = function (a: number) { return a * 2}

Igual que la llista de paràmetres d'una funció, això significa que la variable de funció double pren un paràmetre, un number anomenat a.

Ara pots invocar la funció double així:

const double = function (a: number) { return a * 2}

const result = double(6)
console.log(result)  // 12

A més, quan tens altres funcions del tipus number => number les pots emmagatzemar en una llista:

const fs = [
    (function (a: number) { return a * 2}),
    (function (a: number) { return a * 3})
]

for (const f of fs) {
    console.log(f(4))
}

Si executes el codi, pots veure que s'executen totes les funcions de la llista:

> bun test.ts
8
12

Un exemple una mica absurd, però així pots començar a entendre que una funció anònima és un valor que es pot tractar com qualsevol altre valor.

Una funció és un valor

Totes les funcions són un valor, no només les anònimes, i poden estar referenciada per altres variables.

A continuaciño tens un exemple:

function sayHi() { 
    console.log("Hello")
}

const sayHello = sayHi 

sayHello() // Hello 
sayHi()    // Hello       

El mateix es pot fer amb una variable de funció:

const sayHi = function() {
    console.log("Hello")
}

const sayHello = sayHi
// ...

TODO explicar inicialització

Funció d'ordre superior

Una funció d'ordre superior ("higher-order function") és una funció que pren altres funcions com a paràmetres d'entrada o retorna una funció com a resultat.

Escriure una funció que pren paràmetres de funció

Per crear una funció que pren un paràmetre de funció, tot el que has de fer és:

  1. A la llista de paràmetres de la teva funció, defineix la signatura de la funció que vols acceptar
  2. Utilitza aquesta funció dins de la teva funció

Per demostrar-ho, aquí tens una funció que pren un paràmetre d'entrada anomenat f, on f és una funció:

function sayHello(f: ()=> void) {
    f()
}

El tipus del paràmetre f indica que f és una funció, i defineix els tipus de funcions que la funció sayHello accepta:

  • f és el nom del paràmetre d'entrada de la funció. És el mateix que anomenar un paràmtre string com so un parametre number com n.
  • La signatura de tipus de f especifica el tipus de funcions que acceptarà aquest funció.
  • La part () de la signatura de f (a la part esquerra del símbol =>) indica que f no pren paràmetres d'entrada.
  • La part void de la signatura (a la part dreta del símbol =>) indica que f no ha de retornar cap resultat.

Dins de la funció sayHello, s'invoca la funció f que s'ha passat com a paràmetre.

Ara que has definit sayHello, crea una funció que coincideixi amb la signatura de f perquè puguem provar-la.

La funció següent no pren paràmetres d'entrada i no retorna res, de manera que coincideix amb la signatura de tipus de f:

function helloDavid() {
    console.log("Hello, David")
}

Com que les signatures de tipus coincideixen, pots passar helloDavid a sayHello:

function sayHello(f: () => void) {
    f()
}

function helloDavid() {
    console.log("Hello, David")
}

sayHello(helloDavid)   // prints "Hello, David"

Felicitats 😊! Acabes de definir una funció anomenada sayHello que pren una funció com a paràmetre d'entrada i després invoca aquesta funció al cos de la funció.

Però el més important no és que sayHello pugui prendre una funció com a paràmetre d'entrada; és que pot prendre qualsevol funció que coincideixi amb la signatura de f.

Per exemple, com que la següent funció no pren paràmetres d'entrada i no retorna res, també funciona amb sayHello:

function bonjourJuliet() {
    console.log("Bonjour, Juliet")
}
> bun test.ts
Bonjour, Juliet

Sintaxis

En aquesta funció:

function sayHello(f: () => void) {
    f()
}

Has observat que la signatura de tipus de f és:

() => void

Saps que això significa, "una funció que no pren paràmetres d'entrada i no retorna res (void)".

A continuació tens una funció que pren dos paràmetres de tipus number i retorna un number:

f: (a: number, b: number) => number

T'imagines quin tipus de funcions coincideixen amb aquesta signatura?

La resposta és que qualsevol funció que pren dos paràmetres number d'entrada i retorna un number coincideix amb aquesta signatura, de manera que totes aquestes "funcions" coincideixen:

function sum(a: number, b: number): number {
    return a + b
}

function subtract(a: number, b: number): number {
    return a - b
}

function multiply(a: number, b: number): number {
    return a * b
}

La funció execute accepta com a paràmetre qualsevol de les tres funcions:

function execute(f: (a: number, b: number) => number) {
    console.log(f(5, 8))
}

// ...

execute(sum)
execute(subtract)
execute(multiply)

Pots veure que funciona:

> bun test.ts
13
-3
40

Prenent un paràmetre de funció juntament amb altres paràmetres

Perquè les funcions d'ordre superior siguin realment útils, també necessiten algunes dades per treballar-hi: per tant, també ha d'acceptar dades com a altres paràmetres d'entrada.

Per exemple, aquí tens una funció anomenada executeNTimes que té dos paràmetres d'entrada: una funció i un number:

function executeNTimes(f: () => void, n: number) {
    let i = 0
    while (i < n) {
        f()
        i += 1
    }
}

Com mostra el codi, executeNTimes executa la funció f n vegades. Com que un bucle simple com aquest no té valor de retorn, executeNTimes retorna void.

Per provar executeNTimes, defineix una funció que coincideixi amb la signatura de f:

function helloWord() {
    console.log("Hello, world")
}

A continuació, passa aquesta funció a executeNTimes juntament amb un number:

executeNTimes(helloWord, 3)

Si executes el codi:

> bun test.ts
Hello, world
Hello, world
Hello, world

Excel·lent🐱. La funció executeNTimes executa la funció helloWorld tres vegades.

Les teves funcions es poden continuar complicant-se com sigui necessari.

Per exemple, aquesta funció pren una funció de tipus (number, number) => number, juntament amb dos paràmetres d'entrada:

function executeAndPrint(f: (a: number, b: number) => number, x: number, y: number) {
    const result = f(x, y)
    console.log(result)
}

Com que les fucions sum i multiply coincideixen amb aquesta signatura de tipus, es poden passar a executeAndPrint juntament amb dos valors number:

function executeAndPrint(f: (a: number, b: number) => number, x: number, y: number) {
    const result = f(x, y)
    console.log(result)
}

function sum(a: number, b: number): number {
    return a + b
}

function multiply(a: number, b: number): number {
    return a * b
}

executeAndPrint(sum, 3, 11)       // prints 14
executeAndPrint(multiply, 3, 9)   // prints 27

Coherència de signatura del tipus de funció

Una bona part d'aprendre sobre les signatures de tipus de funció és que la sintaxi que utilitzes per definir els paràmetres d'entrada de la funció és la mateixa sintaxi que fas servir per escriure literals de funció.

Per exemple, si has d'escriure una funció que calcula la suma de dos nombres enters, la pots escriure així amb un tipus explícit:

const sum: (a: number, b: number) => number = function (a: number, b: number) {
    return a + b
}

Aquest codi consta de la signatura de tipus:

const sum: (a: number, b: number) => number = function (a: number, b: number) {
           //////////////////////////////// 
    return a + b
}

Els paràmetres d'entrada:

const sum: (a: number, b: number) => number = function (a: number, b: number): number {
                                                       //////////////////////////////
    return a + b
}

i el cos de la funció:

const sum: (a: number, b: number) => number = function (a: number, b: number) {
    return a + b
           ///// 
}

La consistència de TypeScript es mostra aquí, on aquest tipus de funció:

const sum: (a: number, b: number) => number = function (a: number, b: number) {
           //////////////////////////////// 
    return a + b
}

és el mateix que la signatura de tipus que utilitzes per definir un paràmetre d'entrada de funció:

function executeAndPrint(f: (a: number, b: number) => number, x: number, y: number) {
                            //////////////////////////////// 
    const result = f(x, y)
    console.log(result)
}

Funcions arrow

Hay otra sintaxis muy simple y concisa para crear funciones, que a menudo es mejor que las Expresiones de funciones.

Se llama “funciones arrow”, porque se ve así:

const f = (arg1, arg2, ..., argN) => expression

Esto crea una función f que acepta los parámetros arg1..argN, luego evalúa la expression del lado derecho mediante su uso y devuelve su resultado.

En otras palabras, es la versión más corta de:

const f = function(arg1, arg2, ..., argN) {
  return expression
}

Veamos un ejemplo concreto:


let sum = (a:number, b:number) => a + b

/* Esta función de flecha es una forma más corta de:
let sum = function(a: number, b: number) {
  return a + b
}
*/

alert( sum(1, 2) ) // 3

Como puedes ver, (a:number, b:number) => a + b significa una función que acepta dos argumentos llamados a y b.

Tras la ejecución, evalúa la expresión a + b y devuelve el resultado.

Si no hay parámetros, los paréntesis estarán vacíos; pero deben estar presentes:

let sayHi = () => alert("¡Hola!")

sayHi()

Las funciones "arrow" se pueden usar de la misma manera que las expresiones de función.

Por ejemplo, para crear dinámicamente una función:

let age = Number(prompt("What is your age?", "18"))

let welcome = (age < 18) ?
  () => alert('¡Hola!') :
  () => alert("¡Saludos!")

welcome()

Las funciones "arrow" pueden parecer desconocidas y poco legibles al principio, pero eso cambia rápidamente a medida que los ojos se acostumbran a la estructura.

Son muy convenientes para acciones simples de una línea.

Funciones de flecha multilínea

Las funciones de flecha que estuvimos viendo eran muy simples. Toman los parámetros a la izquierda de =>, los evalúan y devuelven la expresión del lado derecho.

A veces necesitamos una función más compleja, con múltiples expresiones o sentencias. En ese caso debemos encerrarlos entre llaves. La diferencia principal es que las llaves necesitan usar un return para devolver un valor (tal como lo hacen las funciones comunes).

Como esto:

let sum = (a: number, b: number) => {  // la llave abre una función multilínea
    let result = a + b
    return result // si usamos llaves, entonces necesitamos un "return" explícito
};

console.log(sum(1, 2)) // 3

Activitat

1.- Implementa la funció map, que retorna una llista en que ha aplicat f a tots els elements de xs:

function map(xs: number[], f: (x: number) => number): number[] {
    // TODO
}

const result = map([2, 3, 4], a => a + 2)

console.log(result) // [ 4, 5, 6 ]

function map(xs: number[], f: (x: number) => number): number[] {
    const result = []
    for (const x of xs) {
        result.push(f(x))
    }
    return result
}

2.- Implementa la funció filter, que retorna una llista amb tots els elements de xs que tornen true quan s'els hi aplica la funció f:

function filter(xs: number[], f: (x: number) => boolean): number[] {
    // ...
}

const result = filter([1, 2, 3, 4], a => a > 2)

console.log(result) // [ 3, 4]

function filter(xs: number[], f: (x: number) => boolean): number[] {
    const result = []
    for (const x of xs) {
        if (f(x))
            result.push(x)
    }
    return result
}

3.- Implementa la funció reduce, que retorna un únic resultat a partir d'anar afegint a result el que computi la funció f a partir de result i cada valor x:

function reduce(xs: number[], initialValue: number, f: (result: number, x: number) => number): number {
    let result = initialValue
    for (const x of xs) {
        result = f(result, x)
    }
    return result
}

const result = reduce([1, 5, 10], 0, (a, b) => a + b)

console.log(result) // 16

function reduce(xs: number[], initialValue: number, f: (result: number, x: number) => number): number {
    let result = initialValue
    for (const x of xs) {
        result = f(result, x)
    }
    return result
}

4.- Donada aquesta llista:

const xs = [1, -5, 10, 15, 20]

const result = // ...

console.log(result) // 12000

Amb les funcions anteriors:

  1. Filtra tots els element que són positius
  2. A continuació multiplica per 2 tots els elements que són majors que 10
  3. A continuació multiplica tots els elements de la llista entre ells.

let ys = filter(xs, x => x > 0)
ys = map(ys, x => x > 10 ? x * 2 : x)
const result = reduce(ys, 1, (a, b) => a * b)

O en una sola linea

const result = reduce(map(filter(xs, x => x > 0), x => x > 10 ? x * 2 : x), 1, (a, b) => a * b)

Crear una funció que retorna una funció

Imagina que vols escriure una funció greet que retorna una funció.

Aquesta funció agafarà un paràmetre de tipus stringp i l'imprimirà amb console.log()`.

Per simplificar aquest primer exemple, greet no prendrà cap paràmetre d'entrada; només crearà una funció i la retornarà.

Tenint en compte aquesta afirmació, pots començar a construir greet. Ja saps que serà una funció:

function greet() {}

També saps que aquesta funció retornarà una funció que (a) pren un paràmetre string i (b) imprimeix aquesta string amb console.log. Per tant, aquesta funció té el tipus string => void:

function greet(): (s: string) => void {
    // ...
}

Ara només necessites el cos de la funció. Ja saps que la funció ha de retornar una funció, i aquesta funció pren un string i l'imprimeix. Aquesta funció anònima coincideix amb aquesta descripció:

(name: string) => console.log(`Hello, ${name}`)

Ara només has de tornar aquesta funció des de la funció:

function greet(): (s: string) => void {
    return (name: string) => console.log(`Hello, ${name} `)
}

Com que aquesta funció retorna una funció, obtens la funció cridant a `greet()p . Aquest és un bon pas per fer al REPL perquè verifica el tipus de la nova funció:

const greetFunction = greet()

Ara pots invocar a greetFunction:

greetFunction("David") // prints "Hello, David"

Enhorabona, acabes de crear una funció dque retorna una funció i després has executat aquesta funció.

La nostra funció seria més útil si poguéssiu passar una salutació, així que fem-ho. Tot el que has de fer és passar la salutació com a paràmetre a la funció greet i utilitzar-la al string que hi ha dins console.log:

function greet(theGreeting: string): (s: string) => void {
    return (name: string) => console.log(`${theGreeting}, ${name} `)
}

Ara, quan invoques la teva funció, el procés és més flexible perquè pots canviar la salutació, tal com pots veure a continuació:

const sayHola = greet("Hola")

sayHola és una funció que pren un paràmetre d'entrada de tipus string i retorna void(res).

const sayHola: (s: string) => void = greet("Hola")

Ara, quan invoques la funció sayHola la sortida és diferent:

sayHola("Laura") // prints "Hello, Laura"

Pots crear tantes funcions diferent com vulguis:

const sayCiao = greet("Ciao")
const sayBonjour = greet("Bonjour")

sayCiao("Laura") // prints "Ciao, Laura"

Activitat

1.- A continuació tens un codi que genera una llista de llistes de números:

{
    const xss: number[][] = Array.from({ length: 100 }, () =>
        Array.from({ length: 100 },
            () => Math.floor(Math.random() * 100000)))

    console.log(xss)
}

Escriu un codi que torni la llista en què la suma dels seus números és menor:

{
    const xss: number[][] = Array.from({length: 100}, () =>
        Array.from({length: 100},
            () => Math.floor(Math.random() * 100000)))

    const result = xss.
        map(xs => [xs.reduce((a, b) => a + b), xs]).
        reduce((acc, sum_xs) => sum_xs[0] > acc[0] ? sum_xs : acc)

    console.log(result)
}