Notación Hindley-Milner
Una forma de crear una notación para expresar qué tipos de parámetro toma una función y qué devuelve.
Lo básico
Una función que toma un valor primario (“tipo antiguo” como cadena, número, booleano, matriz, función …) y devuelve otro valor primario:
instruction :: String -> String
const instruction = function (verb) { return verb + " me";};la función instruction toma una cadena y devuelve una cadena
También podría hacer algo así:
length :: String → Number
const length = function (s) { return s.length;};En el caso de una matriz de números:
length :: [Number] → Number
const length = function(arr){ retrun arr.length }Trabajando con funciones
En el caso de una función, envolvemos nuestra función entre paréntesis y dentro de los paréntesis tenemos nuestro tipo de entrada y nuestro tipo de salida:
addOneToAll :: ((Number → Number),[Number]) → [Number]
const addOne = function (x) { return x + 1;};const addOneToAll = (addOne, arr) => arr.map(addOne);En este caso tenemos una función llamada addOneToAll que espera como primer parámetro una función (en nuestro caso addOne) y esta función aceptará un número y devolverá un número. Y como segundo parámetro una matriz de números y devolverá otra matriz de números.
Funciones de currying
Ahora, ¿qué pasa con una función que devuelve una función que devuelve otra función …
Siguiendo lo anterior, tendríamos algo como esto:
replace :: String -> (String -> (String -> String))
var replace = curry(function (find, replacement, str) { var regex = new RegExp(find, "g"); return str.replace(regex, replacement);});En este caso también hacemos que la función sea curry para tomar parámetros uno por uno
Y en la programación funcional podemos asumir que todo es curry, así que tendemos a dejar caer los corchetes y algo como esto:
replace :: String -> String -> String -> String
Trabajando con funciones que toman múltiples parámetros como entrada (Variables arbitrarias de Hindley-Milner)
Mostramos el ejemplo con la función de longitud donde podríamos tener:
length :: [Number] → Number
o
length :: string → Number
En este caso podríamos escribir ambos con una variable arbitraria como:
length :: [a] → Number
Otro ejemplo común es la identidad:
identity :: a -> a
Y un ejemplo más complejo:
map :: (a -> b) -> [a] -> [b]
const map = curry(function (callback, array) { return array.map(callback);});La función de mapa toma una función que toma una variable de tipo a y devuelve una variable de tipo b.
Luego toma una matriz de valores, todos de tipo a, y devuelve una matriz de valores, todos de tipo b.
Trabajando con Ramda
Tipos parametrizados
Podemos imaginar fácilmente un tipo que representa una colección de elementos similares, llamémoslo una Caja. Pero ninguna instancia es una Caja arbitraria; cada uno solo puede contener un tipo de elemento.
makeBox :: Number -> Number -> Number -> [a] -> Box a
const makeBox = curry((height, width, depth, items) => /* ... */);Alias de tipo
Si tuviéramos un tipo parametrizado Usuario String, donde la String se supone que representa un nombre, y quisieramos ser más específicos acerca del tipo de String que se representa al generar una URL, podríamos crear un alias de tipo así:
toUrl :: User Name u => Url -> u -> Url
Name = String
Url = String
const toUrl = curry( (base, user) => base + user.name.toLowerCase().replace(/\W/g, "-"));toUrl("http://example.com/users/", { name: "Fred Flintstone", age: 24 });//=> 'http://example.com/users/fred-flintstone'Restricciones de tipo [Ord]
A veces queremos restringir los tipos genéricos que podemos usar en alguna firma de alguna manera u otra.
Podríamos querer una función máxima que pueda operar sobre Números, en Cadenas, en Fechas, pero no en Objetos arbitrarios.
Queremos describir tipos ordenados, aquellos para los cuales a < b siempre devolverá un resultado significativo
maximum :: Ord a => [a] -> a
const maximum = (vals) => reduce((curr, next) => (next > curr ? next : curr), head(vals), tail(vals));maximum([3, 1, 4, 1]); //=> 4maximum(["foo", "bar", "baz", "qux", "quux"]); //=> 'qux'maximum([ new Date("1867-07-01"), new Date("1810-09-16"), new Date("1776-07-04"),]); //=> new Date("1867-07-01")Ord a ⇒ [a] → a dice que la máxima toma una colección de elementos de algún tipo, pero ese tipo debe adherirse a Ord.
En JS, no hay forma de garantizar que el usuario no nos pasará [1, 2, ‘a’, false, undefined, null]. Así que toda nuestra anotación de tipo es descriptiva y aspiracional en lugar de ser impuesta por el compilador, como sería en, digamos, Haskell.
Firmas múltiples
A veces, en lugar de tratar de encontrar la versión más genérica de una firma, es más sencillo enumerar varias firmas relacionadas por separado. Podríamos hacer eso como abajo:
getIndex :: a -> [a] -> Number
:: String -> String -> Number
const getIndex = curry((needle, haystack) => haystack.indexOf(needle));getIndex("ba", "foobar"); //=> 3getIndex(42, [7, 14, 21, 28, 35, 42, 49]); //=> 5Funciones variadicas (específicas para Ramda)
En Haskell, todas las funciones tienen una aridad fija. Pero Javsacript tiene que lidiar con funciones variadicas.
flip :: (a -> b -> ... -> z) -> (b -> a -> ... -> z)
const flip = (fn) => function (b, a) { return fn.apply(this, [a, b].concat([].slice.call(arguments, 2))); };flip((x, y, z) => x + y + z)("a", "b", "c"); //=> 'bac'Objetos simples
Cuando un objeto se utiliza como un diccionario de valores de tipo similar (a diferencia de su otro papel como un Registro), entonces los tipos de las claves y los valores pueden volverse relevantes.
Entonces podríamos representarlos así:
keys :: {k: v} -> [k]
values :: {k: v} -> [v]
keys({ a: 86, b: 75, c: 309 }); //=> ['a', 'b', 'c']values({ a: 86, b: 75, c: 309 }); //=> [86, 75, 309]Ejemplo complejo
Lens s a -> (a -> a) -> s -> s
Lens s a = Functor f => (a -> f a) -> s -> f s
Comenzamos con el alias de tipo, Lens s a = Functor f ⇒ (a → f a) → s → f s. Esto nos dice que el tipo Lens está parametrizado por dos variables genéricas, s, y a. Sabemos que hay una restricción en el tipo de la variable f utilizada en una Lens: debe ser un Functor. Con eso en mente, vemos que una Lens es una función acurruada de dos parámetros, el primero siendo una función de un valor del tipo genérico a a uno del tipo parametrizado f a, y el segundo siendo un valor del tipo genérico s.
El resultado es un valor del tipo parametrizado f・s