La tercera parte de una serie de artículos sobre programación funcional se ha detenido. Hoy hablaremos sobre todos los tipos de este paradigma y mostraremos ejemplos de su uso. ¡Más información sobre tipos primitivos, tipos generalizados y mucho más debajo del corte!

Ahora que conocemos las funciones, veremos cómo los tipos interactúan con funciones como dominio y rango. Este artículo es solo una revisión. Para una inmersión más profunda en los tipos hay una serie de "comprensión de los tipos F #" .
Para comenzar, necesitamos una mejor comprensión de la notación de tipos. Vimos la notación de flecha " ->
" separando dominio y rango. Entonces la firma de la función siempre se ve así:
val functionName : domain -> range
Algunos ejemplos más de funciones:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
Si ejecuta este código en una ventana interactiva , puede ver las siguientes firmas:
val intToString : int -> string val stringToInt : string -> int
Significan:
intToString
tiene un dominio de tipo int
, que se asigna al rango de tipo string
.stringToInt
tiene un dominio de tipo string
, que se asigna a un rango de tipo int
.
Tipos primitivos
Hay tipos primitivos esperados: string, int, float, bool, char, byte, etc., así como muchos otros derivados del sistema de tipos .NET.
Un par de ejemplos más de funciones con tipos primitivos:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 2 let stringToString x = x + " world"
y sus firmas:
val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string
Anotación de tipo
En ejemplos anteriores, el compilador de F # definió correctamente los tipos de parámetros y resultados. Pero esto no siempre sucede. Si intenta ejecutar el siguiente código, recibirá un error de compilación:
let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type
El compilador no conoce el tipo de argumento "x", y debido a esto, no sabe si la "Longitud" es un método válido. En la mayoría de los casos, esto se puede solucionar pasando la "anotación de tipo" al compilador de F #. Entonces él sabrá qué tipo usar. En la versión fija, indicamos que el tipo "x" es una cadena.
let stringLength (x:string) = x.Length
Las llaves alrededor del parámetro x:string
son importantes. Si se omiten, ¡el compilador decidirá que la cadena es el valor de retorno! Es decir, se utilizan dos puntos para indicar el tipo de valor de retorno, como se muestra en el siguiente ejemplo.
let stringLengthAsInt (x:string) :int = x.Length
Indicamos que el parámetro x
es una cadena, y el valor de retorno es un entero.
Tipos de funciones como parámetros
Una función que toma otras funciones como parámetros o devuelve una función se denomina función de orden superior (la función de orden superior a veces se acorta a HOF). Se utilizan como abstracción para establecer un comportamiento lo más general posible. Este tipo de función es muy común en F #, la mayoría de las bibliotecas estándar las usan.
Considere la función evalWith5ThenAdd2
, que toma una función como parámetro y luego calcula esta función a partir de 5 y agrega 2 al resultado:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
La firma de esta función se ve así:
val evalWith5ThenAdd2 : (int -> int) -> int
Puede ver que el dominio es (int->int)
y el rango es int
. ¿Qué significa esto? Esto significa que el parámetro de entrada no es un valor simple, sino una función de muchas funciones de int
a int
. El valor de salida no es una función, sino solo un int
.
Probemos
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
y obtener:
val add1 : int -> int val it : int = 8
" add1
" es una función que asigna int
a int
, como vemos en la firma. Es un parámetro válido para evalWith5ThenAdd2
, y su resultado es 8.
Por cierto, la palabra especial " it
" se usa para denotar el último valor calculado, en este caso es el resultado que estábamos esperando. Esta no es una palabra clave, es solo una convención de nomenclatura.
Otro caso:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
da:
val times3 : int -> int val it : int = 17
" times3
" también es una función que asigna int
a int
, como se puede ver en la firma. También es un parámetro válido para evalWith5ThenAdd2
. El resultado de los cálculos es 17.
Tenga en cuenta que los datos de entrada son sensibles al tipo. Si la función pasada utiliza un float
, no un int
, entonces nada funcionará. Por ejemplo, si tenemos:
let times3float x = x * 3.0 // - (float->float) evalWith5ThenAdd2 times3float
El compilador, cuando intenta compilar, devolverá un error:
error FS0001: Type mismatch. Expecting a int -> int but given a float -> float
informando que la función de entrada debe ser una función de tipo int->int
.
Funciones como salida
Las funciones de valor también pueden ser el resultado de funciones. Por ejemplo, la siguiente función generará una función de "sumador" que agregará un valor de entrada.
let adderGenerator numberToAdd = (+) numberToAdd
Su firma:
val adderGenerator : int -> (int -> int)
significa que el generador toma un int
y crea una función ("sumador") que asigna ints
a ints
. Veamos cómo funciona:
let add1 = adderGenerator 1 let add2 = adderGenerator 2
Se crean dos funciones sumadoras. El primero crea una función que agrega 1 a la entrada, el segundo agrega 2. Tenga en cuenta que las firmas son exactamente lo que esperábamos.
val add1 : (int -> int) val add2 : (int -> int)
Ahora puede usar las funciones generadas como de costumbre, no son diferentes de las funciones definidas explícitamente:
add1 5 // val it : int = 6 add2 5 // val it : int = 7
Usar anotaciones de tipo para restringir los tipos de función
En el primer ejemplo, observamos una función:
let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int
En este ejemplo, F # puede concluir que " fn
" convierte int
en int
, por lo que su firma será int->int
.
Pero, ¿cuál es la firma de "fn" en el siguiente caso?
let evalWith5 fn = fn 5
Está claro que " fn
" es un tipo de función que requiere un int
, pero ¿qué devuelve? El compilador no puede responder esta pregunta. En tales casos, si es necesario indicar el tipo de función, puede agregar un tipo de anotación para los parámetros de la función, así como para los tipos primitivos.
let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5
Además, puede determinar el tipo de retorno.
let evalWith5AsString fn :string = fn 5
Porque la función principal devuelve una string
, la función " fn
" también se ve obligada a devolver una string
. Por lo tanto, no es necesario especificar explícitamente el tipo " fn
".
Escriba "unidad"
En el proceso de programación, a veces queremos que una función haga algo sin devolver nada. Considere la función " printInt
". La función realmente no devuelve nada. Simplemente imprime la cadena en la consola como un efecto secundario de la ejecución.
let printInt x = printf "x is %i" x //
¿Cuál es su firma?
val printInt : int -> unit
¿Qué es una " unit
"?
Incluso si la función no devuelve valores, aún necesita rango. No hay funciones "nulas" en el mundo de las matemáticas. ¡Cada función debe devolver algo, porque la función es una asignación, y la asignación debe mostrar algo!

Entonces, en F #, funciones como esta devuelven un tipo especial de resultado llamado " unit
". Contiene solo un valor, denotado por " ()
". Puede pensar que unit
y ()
son algo así como "void" y "null" de C #, respectivamente. Pero a diferencia de ellos, la unit
es el tipo real y ()
valor real. Para verificar esto, solo haz:
let whatIsThis = ()
Se recibirá la siguiente firma:
val whatIsThis : unit = ()
Lo que indica que la etiqueta " whatIsThis
" es de tipo unit
y está asociada con un valor ()
.
Ahora, volviendo a la firma " printInt
", podemos entender el significado de esta entrada:
val printInt : int -> unit
Esta firma dice que printInt
tiene un dominio de int
, que se traduce en algo que no nos interesa.
Funciones sin parámetros.
Ahora que entendemos la unit
, ¿podemos predecir su apariencia en un contexto diferente? Por ejemplo, intente crear una función reutilizable "hello world". Como no hay entrada ni salida, podemos esperar la unit -> unit
firma unit -> unit
. A ver:
let printHello = printf "hello world" //
Resultado:
hello world val printHello : unit = ()
No es exactamente lo que esperábamos. "Hello world" salió inmediatamente, y el resultado no fue una función, sino un simple valor de tipo unit. Podemos decir que este es un valor simple, porque, como vimos anteriormente, tiene una firma del formulario:
val aName: type = constant
En este ejemplo, vemos que printHello
realmente un valor simple ()
. Esta no es una función que podamos llamar más adelante.
¿Cuál es la diferencia entre printInt
y printHello
? En el caso de printInt
valor no puede determinarse hasta que sepamos el valor del parámetro x
, por lo que la definición fue una función. En el caso de printHello
no hay parámetros, por lo que el lado derecho se puede definir en su lugar. Y fue igual a ()
con un efecto secundario en forma de salida a la consola.
Puede crear una verdadera función reutilizable sin parámetros, lo que obliga a la definición a tener un argumento unit
:
let printHelloFn () = printf "hello world" //
Ahora su firma es igual a:
val printHelloFn : unit -> unit
y para llamarlo, debemos pasar ()
como parámetro:
printHelloFn ()
Fortalecimiento de tipos de unidades con la función ignorar
En algunos casos, el compilador requiere un tipo de unit
y se queja. Por ejemplo, los dos casos siguientes causarán un error del compilador:
do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello"
Para ayudar en estas situaciones, hay una función especial de ignore
que toma cualquier cosa y devuelve la unit
. La versión correcta de este código podría ser esta:
do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello"
Tipos genéricos
En la mayoría de los casos, si el tipo de un parámetro de función puede ser de cualquier tipo, necesitamos decir algo al respecto. F # usa genéricos .NET para tales situaciones.
Por ejemplo, la siguiente función convierte un parámetro en una cadena al agregar texto:
let onAStick x = x.ToString() + " on a stick"
No importa qué tipo de parámetro, todos los objetos pueden hacer en ToString()
.
Firma:
val onAStick : 'a -> string
¿Qué tipo 'a
? En F #, es una forma de indicar un tipo genérico que es desconocido en tiempo de compilación. Un apóstrofe antes de "a" significa que el tipo es genérico. Equivalente a esta firma en C #:
string onAStick<a>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
Debe entenderse que esta función F # todavía tiene una tipificación fuerte incluso con tipos genéricos. No acepta un parámetro de tipo Object
. La escritura fuerte es buena porque le permite mantener la seguridad de su tipo al componer funciones.
La misma función se usa para int
, float
y string
.
onAStick 22 onAStick 3.14159 onAStick "hello"
Si hay dos parámetros generalizados, entonces el compilador les dará dos nombres diferentes: 'a
para el primero, 'b
para el segundo, etc. Por ejemplo:
let concatString xy = x.ToString() + y.ToString()
Habrá dos tipos genéricos en esta firma: 'a
y 'b
:
val concatString : 'a -> 'b -> string
Por otro lado, el compilador reconoce cuando solo se requiere un tipo genérico. En el siguiente ejemplo, x
e y
deben ser del mismo tipo:
let isEqual xy = (x=y)
Entonces, una firma de función tiene el mismo tipo genérico para ambos parámetros:
val isEqual : 'a -> 'a -> bool
Los parámetros generalizados también son muy importantes cuando se trata de listas y otras estructuras abstractas, y veremos muchas de ellas en los siguientes ejemplos.
Otros tipos
Hasta ahora, solo se han discutido los tipos básicos. Estos tipos se pueden combinar de varias maneras en tipos más complejos. Su análisis completo será más adelante en otra serie , pero mientras tanto, y aquí los analizaremos brevemente, para que pueda reconocerlos en las firmas de funciones.
- Tuplas Este es un par, un triple, etc., compuesto de otros tipos. Por ejemplo,
("hello", 1)
es una tupla basada en string
e int
. Una coma es un sello distintivo de las tuplas; si una coma se ve en algún lugar de F #, es casi seguro que sea parte de la tupla.
En las firmas de funciones, las tuplas se escriben como "productos" de los dos tipos involucrados. En este caso, la tupla será del tipo:
string * int // ("hello", 1)
- Colecciones Los más comunes son list (list), seq (secuencia) y array. Las listas y las matrices tienen un tamaño fijo, mientras que las secuencias son potencialmente infinitas (detrás de escena, las secuencias son las mismas
IEnumrable
). En las firmas de funciones, tienen sus propias palabras clave: " list
", " seq
" y " []
" para matrices.
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- Opción (tipo opcional) . Este es un contenedor simple sobre objetos que pueden faltar. Hay dos opciones:
Some
(cuando el valor existe) y None
(cuando el valor no existe). En las firmas de funciones, tienen su propia palabra clave " option
":
int option // Some 1
- La marcada asociación (unión discriminada) . Se construyen a partir de muchas variaciones de otros tipos. Vimos algunos ejemplos en "¿por qué usar F #?" . En las firmas de funciones, se hace referencia a ellas por nombre de tipo; no tienen una palabra clave especial.
- Tipo de registro (registros) . Tipos como estructuras de bases de datos o filas, un conjunto de valores con nombre. También vimos algunos ejemplos en "¿por qué usar F #?" . En las firmas de funciones, se llaman por nombre de tipo y tampoco tienen su propia palabra clave.
Pon a prueba tu comprensión de los tipos
Aquí hay algunas expresiones para evaluar su comprensión de las firmas de funciones. Para verificar, simplemente ejecútelos en una ventana interactiva.
let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // : x? let testO x:string = x 1 // : :string ?
Recursos Adicionales
Hay muchos tutoriales para F #, incluidos los materiales para aquellos que vienen con experiencia en C # o Java. Los siguientes enlaces pueden ser útiles a medida que profundiza en F #:
También se describen varias otras formas de comenzar a aprender F # .
Finalmente, la comunidad F # es muy amigable para principiantes. Hay un chat muy activo en Slack, respaldado por la F # Software Foundation, con salas para principiantes a las que puedes unirte libremente . ¡Recomendamos encarecidamente que haga esto!
¡No te olvides de visitar el sitio de la comunidad de habla rusa F # ! Si tiene alguna pregunta sobre el aprendizaje de un idioma, estaremos encantados de discutirlo en las salas de chat:
Sobre autores de traducción
Traducido por @kleidemos
La traducción y los cambios editoriales fueron realizados por los esfuerzos de la comunidad de desarrolladores de F # de habla rusa . También agradecemos a @schvepsss y @shwars por preparar este artículo para su publicación.