Where can I find an explanation / summary of characters used to explain functional programming, in particular Ramda.js?

The Ramda.js JavaScript Functional Programming API documentation contains symbolic abbreviations but does not provide a legend for understanding them. Is there a place (website, article, cheat sheet, etc.) that I can go to decrypt?

Some examples from the Ramda.js API documentation:

Number -> Number -> Number Apply f => f (a -> b) -> fa -> fb Number -> [a] -> [[a]] (*... -> a) -> [*] -> a {k: ((a, b, ..., m) -> v)} -> ((a, b, ..., m) -> {k: v}) Filterable f => (a -> Boolean) -> fa -> fa Lens sa = Functor f => (a -> fa) -> s -> fs (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y]) (Applicative f, Traversable t) => (a -> fa) -> t (fa) -> f (ta) 

Currently, I can understand a lot of what Ramda.js is trying to do, and I can often give an educated guess about what statements like the one above mean. However, I am sure that I would understand more easily if I understood these symbols / statements better. I would like to understand what the individual components mean (for example, specific letters, keywords, different types of arrows, punctuation marks, etc.). I would also like to know how to β€œread” these lines.

I have not had success googling this or searching StackExchange. I used various combinations of "Ramda", "functional programming", "characters", "abbreviations", "shorthand", etc. I'm also not quite sure if I am looking for (A) universally usable abbreviations in a wider area of ​​functional programming (or perhaps even just programming in general), or (B) the specialized syntax that Ramda authors use (or perhaps co-opt in another place, but change further) only for your library.

+6
source share
3 answers

From the Ramda Wiki :

(Part 1/2 is too long for one SO answer!)


Type Labels

(or "What are these funny arrows?")

Looking at the documentation for the Ramda over function, the first thing we see is two lines that look like this:

 Lens sa -> (a -> a) -> s -> s Lens sa = Functor f => (a -> fa) -> s -> fs 

For people coming to Ramda from other FP languages, they probably look familiar, but they can be pure gobbledy-gook for Javascript developers. Here we describe how to read them in the Ramda documentation and how to use them for your own code.

And in the end, as soon as we understand how these works, we will explore why people would like them.

Named types

Many ML languages, including Haskell , use the standard method for describing the signatures of their functions. As functional programming becomes more common in Javascript, this signature style is gradually becoming almost standard. We occupy the Haskell Version for Ramda.

We will not try to create a formal description, but simply the essence of these signatures with examples.

 // length :: String -> Number const length = word => word.length; length('abcde'); //=> 5 

Here we have a simple length function that takes a word of type String and returns the number of characters in the string, which is Number . The comment above the function is the signature line. This starts with the name of the function, then the separator " :: ", and then the actual description of the functions. It should be clear enough that the syntax of this description. Function input is given, then arrow, then output. Usually you will see an arrow written as indicated above, " -> " in the source code and as " β†’ " in the output documentation. They mean the same thing.

What we put before and after the arrow are the types of parameters, not their names. At this level of description, we all really said that this is a function that takes a string and returns a Number.

 // charAt :: (Number, String) -> String const charAt = (pos, word) => word.charAt(pos); charAt(9, 'mississippi'); //=> 'p' 

In this case, the function takes two parameters, the position - which is a Number - and the word is String - and it returns a one-character String or an empty String .

In Javascript, unlike Haskell, functions can take more than one parameter. To show a function that requires two parameters, we separate the two input parameters with a comma and wrap the group in parentheses: (Number, String) . As in many languages, the Javascript function parameters are positional, so the order matters. (String, Number) has a completely different meaning.

Of course, for a function that takes three parameters, we simply expand the list, separated by commas inside parentheses:

 // foundAtPos :: (Number, String, String) -> Boolean const foundAtPos = (pos, char, word) => word.charAt(pos) === char; foundAtPos(6, 's', 'mississippi'); //=> true 

And also for any larger finite list of parameters.

It would be instructive to note the parallel between the ES6 style arrow defining a function and declaration of this type. Function defined by

 (pos, word) => word.charAt(pos); 

Replacing the argument names with their types, the body with the type of the value that it returns, and the bold arrow " => ", with the skinny one, " -> ", we get the signature:

 // (Number, String) -> String 

Lists of Values

Very often we work with lists of values, all of the same type. If we wanted the function to add all the numbers to the list, we could use:

 // addAll :: [Number] -> Number const addAll = nbrs => nbrs.reduce((acc, val) => acc + val, 0); addAll([8, 6, 7, 5, 3, 0, 9]); //=> 38 

The input to this function is a list of Number s. There is a separate discussion of exactly what we mean by lists , but for now we can think of it as if they were arrays. To describe the list of this type, we wrap this type name in square brackets " [ ] ". The list from String will be [String] , the Boolean list will be [Boolean] , the list of Number lists will be [[Number]] .

Such lists can also be return values ​​from a function:

 // findWords :: String -> [String] const findWords = sentence => sentence.split(/\s+/); findWords('She sells seashells by the seashore'); //=> ["She", "sells", "seashells", "by", "the", "seashore"] 

And we should not be surprised that we can combine them:

 // addToAll :: (Number, [Number]) -> [Number] const addToAll = (val, nbrs) => nbrs.map(nbr => nbr + val); addToAll(10, [2, 3, 5, 7]); //=> [12, 13, 15, 17] 

This function takes Number , val and a list of Number s, nbrs and returns a new list of Number s.

It’s important to understand that this is all that the signature tells us. It is impossible to distinguish this function, only signatures, from any other function that accepts to accept Number and the Number list and return the Number s list. [^ theorems]

[^ theorems]: Well, there is other information that we can get, in the form of free theorems implies a signature.

Functions

There is another very important type that we have not discussed. Functional programming is all functions; we pass functions as parameters and receive functions as the return value from another function. We must also introduce them.

In fact, we have already seen how we represent functions. Each signature line documented a specific function. We again use the small technique described above for higher-order functions used in our signatures.

 // applyCalculation :: ((Number -> Number), [Number]) -> [Number] const applyCalculation = (calc, nbrs) => nbrs.map(nbr => calc(nbr)); applyCalculation(n => 3 * n + 1, [1, 2, 3, 4]); //=> [4, 7, 10, 13] 

Here the calc function is described (Number β†’ Number) This is exactly the same as our top-level function signatures, just wrapped in brackets to correctly group it as a separate unit. We can do the same with the function returned from another function:

 // makeTaxCalculator :: Number -> (Number -> Number) const makeTaxCalculator = rate => base => Math.round(100 * base + base * rate) / 100; const afterSalesTax = makeTaxCalculator(6.35); // tax rate: 6.35% afterSalesTax(152.83); //=> 162.53 

makeTaxCalculator accepts the tax rate expressed as a percentage (type Number and returns a new function, which itself takes Number and returns a Number . Again, we describe the function returned (Number β†’ Number) , which makes the signature of the entire function Number β†’ (Number β†’ Number)

Carring

Using Ramda, we probably won't write a makeTaxCalculator exactly like this. Currying is central to Ramda, and we will probably take advantage of it here [^ curry-desc]

Instead, in Ramda, most likely write a calculateTax curry function that can be used just like makeTaxCalculator , if that's what you wanted, but could also be used in a single pass:

 // calculateTax :: Number -> Number -> Number const calculateTax = R.curry((rate, base) => Math.round(100 * base + base * rate) / 100); const afterSalesTax = calculateTax(6.35); // tax rate: 6.35% afterSalesTax(152.83); //=> 162.53 // OR calculateTax(8.875, 49.95); //=> 54.38 

This curry function can be used either by feeding both parameters up to the front and returning a value, or by supplying only one and getting a function that is looking for the second to return. To do this, we use Number β†’ Number β†’ Number . In Haskell, ambiguity is resolved quite simply: the arrows snap to the right, and all functions take one parameter, although there is syntactic sleight of hand for which you can call them several parameters.

In Ramda, ambiguity is not permitted until we call a function. when we call calculateTax(6.35) , since we decided not to provide the second parameter, we return the final part of the Number β†’ Number of the signature part. When we call calculateTax(8.875, 49.95) , we set the first two parameters to Number , and so we get only the final Number .

Signatures of curry functions always look like this: sequence Types separated by " β†’ ". Since some of these types may themselves be functions, there may be parenthesized substructures that themselves have arrows. That would be perfectly acceptable:

 // someFunc :: ((Boolean, Number) -> String) -> (Object -> Boolean) -> // (Object -> Number) -> Object -> String 

It is made up. I do not have a real function to point this out. But we can find out a fair bit about such a function from our type signature. This takes three functions and an Object and returns a String . the first function that it takes itself takes Boolean and a Number and returns a String . Note that this is not described here as curry (or it would be written as (Boolean β†’ Number β†’ String) .) The second parameter of the function takes Object and returns a Boolean , and the third takes Object and returns a Number .

This is a bit more complicated than realistic in Ramda's features. We do not often have functions of four parameters, and we, of course, do not have those that take three parameters of the function. Therefore, if this is clear, we are well on the path to understanding that Ramda should abandon us.

[^ curry-desc]: for people coming from other languages, Ramda currying may be slightly different from what you used: If f :: (A, B, C) β†’ D and g = curry(f) , then g(a)(b)(c) == g(a)(b, c) == g(a, b)(c) == g(a, b, c) == f(a, b, c) .

Type variables

If you worked with map , you will know that it is quite flexible:

 map(word => word.toUpperCase(), ['foo', 'bar', 'baz']); //=> ["FOO", "BAR", "BAZ"] map(word => word.length, ['Four', 'score', 'and', 'seven']); //=> [4, 5, 3, 5] map(n => n * n, [1, 2, 3, 4, 5]); //=> [1, 4, 9, 16, 25] map(n => n % 2 === 0, [8, 6, 7, 5, 3, 0, 9]); //=> [true, true, false, false, false, true, false] 

From this, we would like to apply all of the following type signatures to the map:

 // map :: (String -> String) -> [String] -> [String] // map :: (String -> Number) -> [String] -> [Number] // map :: (Number -> Number) -> [Number] -> [Number] // map :: (Number -> Boolean) -> [Number] -> [Boolean] 

But it is clear that there are still many possibilities. We cannot just list a mall. To handle this, type signatures deal not only with specific classes such as Number , String and Object , but also with the representation of generic classes.

How would we describe map ? It is pretty simple. The first parameter is a function that takes an element of one type and returns an element of the second type. (The two types do not have to be different). The second parameter is a list of elements of the input type of this function. It returns a list of elements of the output type of this function.

Here is how we could describe it:

 // map :: (a -> b) -> [a] -> [b] 

Instead of specific types, we use common placeholders, single letters of the lower character to indicate arbitrary types.

It is easy to distinguish them from specific types. These are complete words and are capitalized by agreement. Common type variables are just a , b , c , etc. Sometimes, if there is a good reason, we could use the letter later in the alphabet if it helps the sense of which types can represent common ones (think k and v for key and value or n for number), but basically we just use these from beginning of the alphabet.

Note that after a generic type variable is used in the signature, it represents a value that is fixed for all uses of the same variable. We cannot use b in one part of the signature, and then reuse it in another place if both of them must be of the same type in the whole signature. Moreover, if the two types in the signature should be the same, then we have to use the same variable for them.

But there is nothing to say that two different variables sometimes cannot point to the same types. map(n => n * n, [1, 2, 3]); //=> [1, 4, 9] map(n => n * n, [1, 2, 3]); //=> [1, 4, 9] (Number β†’ Number) β†’ [Number] β†’ [Number] , therefore, if we must match (a β†’ b) β†’ [a] β†’ [b] , then both a and and b point to Number . It's not a problem. We still have two type variables, as there will be times when they do not match.

Parameterized Types

Some types are more complicated. We can easily imagine a type representing a collection of similar items, let's call it Box . But not a single instance of an arbitrary Box ; each can contain only one item. When we discuss Box we always need to indicate Box of something.

 // makeBox :: Number -> Number -> Number -> [a] -> Box a const makeBox = curry((height, width, depth, items) => /* ... */); // addItem :: a -> Box a -> Box a const addItem = curry((item, box) => /* ... */); 

So we specify a Box , parameterized by an unknown type a : Box a . This can be used wherever we need a type, as a parameter, or as a function return. Of course, we could parameterize the type using a more specific type, Box Candy or Box Rock . (Although this is legal, we are not actually doing this in Ramda at the moment. Maybe we just don't want to be blamed for being as stupid as a crate of rocks.)

There cannot be only one type parameter. We can have a Dictionary , which is parameterized both by the type of keys and the type of values ​​used. This can be written Dictionary k v . It also demonstrates the place where we could use single letters that are not original from the alphabet.

There are not many declarations in Ramda, but we can often use such things in user code. Their greatest use is type support, so we need to describe those.

Type Aliases

Sometimes our types get out of hand, and it becomes difficult to work with them because of their intrinsic complexity or because they are too generic. Haskell allows you to use type aliases to simplify the understanding of these. Ramda also borrows this concept, although he used it sparingly.

The idea is simple. If we had a parameterized User String type, where the String should represent the name, and we wanted to be more specific to the String type that was presented when creating the URL, we could create an alias of the type:

 // 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' 

The aliases Name and Url displayed to the left of " = ". Their equivalent values ​​are displayed on the right.

As already noted, this can also be used to create simple aliases for a more complex type. A number of functions in Ramda work with Lens es, and the types for them are simplified using a type alias:

 // Lens sa = Functor f => (a -> fa) -> s -> fs 

We will try to break this complex value a little later, but for now, it should be clear enough that no matter what Lens sa represents, underneath it is just an alias for the complex expression, Functor f β‡’ (a β†’ fa) β†’ s β†’ fs .

(Part 2 in a separate answer .)

+5
source

From the Ramda Wiki :

(Part 2/2 is too long for one SO answer!)


Type restrictions

Sometimes we want to limit the general types that we can use in a signature anyway. We may need a maximum function that can work on Numbers , on Strings , on Dates , but not on arbitrary Objects . We want to describe ordered types for which a < b will always return a meaningful result. We will discuss the details of the type Ord in the Types section ; for our purposes, it’s enough to say that it is intended to capture those types that have some sorting ordering operation that works with < .

 // maximum :: Ord a => [a] -> a const maximum = vals => reduce((curr, next) => next > curr ? next : curr, head(vals), tail(vals)) maximum([3, 1, 4, 1]); //=> 4 maximum(['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") 

This description [^ maximum-note] adds a restriction section to the beginning, separated from the rest by the right double arrow (" => " in the code, sometimes " β‡’ " in other documentation.) Ord a β‡’ [a] β†’ a says that the maximum accepts a set of elements of some type, but this type must adhere to Ord .

In dynamically typed Javascript, there is no easy way to enforce this type restriction without adding type checking to each parameter, and even every value of each list. [^ strong-types] But this is true for ours in general. When we require [a] in the signature, there is no way to guarantee that the user will not give us [1, 2, 'a', false, undefined, [42, 43], {foo: bar}, new Date, null] . So, our entire type of annotation is descriptive and desirable, and not a compiler, as it would, say, in Haskell.

The most common type restrictions for Ramda functions are those specified by the Javascript FantasyLand specification.

When we discussed the map function earlier, we only talked about mapping a function over a list of values. But the idea of ​​mapping is more general than that. It can be used to describe the application of a function to any data structure containing a certain number of values ​​of a certain type if it returns another structure of the same form with a new value in it. We could map through Tree , a Dictionary , a plain Wrapper that contains only one value or many other types.

The concept of what can be matched is captured by the algebraic type that other languages ​​and FantasyLand borrow from the abstract math known as Functor . A Functor is just a type that contains a map method subject to some simple laws. The ramda map function will call the map method for our type, assuming that if we did not pass a list (or another type known to Ramde), but passed something with map on it, we expect it to act like a Functor .

To describe this in a signature, we add a restriction section to the signature block:

 // map :: Functor f => (a -> b) -> fa -> fb 

Note that a restriction block should not have only one restriction on it. We can have several restrictions, separated by commas and wrapped in parentheses. So this could be the signature for some odd Function:

 // weirdFunc :: (Functor f, Monoid b, Ord b) => (a -> b) -> fa -> fb 

Without dwelling on what he does or how he uses Monoid or Ord , we can at least see what types must be set for this function to work properly.

[^ maximum note]: there is a problem with this maximum function; This will crash on an empty list. Trying to fix this problem will take us too far.

[^ strong-types]: There are very good tools that lack Javascript, including language methods such as Ramda, Sanctuary , Javascript extensions for more strongly typed ones, for example flow and TypeScript and more strongly typed languages ​​that are compiled in Javascript e.g. ClojureScript , Elm, and PureScript .

Several signatures

, , . JSDoc, . :

 // getIndex :: a -> [a] -> Number // :: String -> String -> Number const getIndex = curry((needle, haystack) => haystack.indexOf(needle)); getIndex('ba', 'foobar'); //=> 3 getIndex(42, [7, 14, 21, 28, 35, 42, 49]); //=> 5 

, , , . , . , , , , . , , , . , . , , , .

Ramda Miscellany

, Haskell Javascript. - .

Haskell arity. Javsacript . Ramda flip . : , .

 // 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' 

[^ -] , , : ( " ... " , "`" ), , , . Ramda , , , .

[^ flip-example]: , .

Any / *

, Ramda ( * ) Any . , . , , , , , . , , [Any] . , , , a b . .

Javascript . , Object , , , - . ( Record ), . " {k: v} " .

 // 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] 

, , :

 // makeObj :: [k,v]] -> {k: v} const makeObj = reduce((obj, pair) => assoc(pair[0], pair[1], obj), {}); makeObj([['x', 10], ['y', 20]]); //=> {"x": 10, "y": 20} makeObj([['a', true], ['b', true], ['c', false]]); //=> {a: true, b: true, c: false} 

Reports

, , , , , Javascript, , , . , {k: v} , , {k: Number} {k: Rectangle} , , {String: Number} .. , :

 // display :: {name: String, age: Number} -> (String -> Number -> String) -> String const display = curry((person, formatter) => formatter(person.name, person.age)); const formatter = (name, age) => name + ', who is ' + age + ' years old.'; display({name: 'Fred', age: 25, occupation: 'crane operator'}, formatter); //=> "Fred, who is 25 years old." 

, Object, . . ( , "", , .

: over

, , over :

 Lens sa -> (a -> a) -> s -> s Lens sa = Functor f => (a -> fa) -> s -> fs 

Lens sa = Functor f β‡’ (a β†’ fa) β†’ s β†’ fs . , Lens , s a . , f , Lens : Functor . , , a Lens , a fa , - s . f s . ? . . , , . , - map fa , , Functor , , map . , , a Lens as , over .

over , a Lens as , , a s . s .

, , over , . , . . , .

?

, , . , ?

. , , , . , , , - , , . , " maximum " " makeObj ". , , " max " " fromPairs "? . , , . lingua franca , , , . ; , , .

, , . :

 foo :: Object -> Number 

map , ,

 map :: (a -> b) -> [a] -> [b] 

map(foo) , Object a Number b , map :

 map(foo) :: [Object] -> [Number] 

" Tab A A ". , . - . .

additional literature

+4
source

, ( Haskell) .

, . , Haskell ; 1 . , . , -> , . " ", 1 1 . , Haskell.

For instance:

 Number -> [a] -> [[a]] 

, a s, a s. , Haskell , Number , , a s, a s. . , 2 .

a . , , -, . , ( ), , , (, <T> Java, T ).

 Apply f => f (a -> b) -> fa -> fb 

, a , a b . -, map . Apply , , a , b .

" " . Apply f , f , Apply ( ). , Apply , , fa a ( ), , . , Apply , , ( (a -> b) ) f .

:

 (a -> b) 

Represents a function that accepts aand turns it into b; but in any case we don’t care what type aor b. Since there are parentheses around it, it represents a single passed function. Each time you see a signature with something like (a -> b), this means that it is the signature for a higher order function.

Recommended reading:

Understanding Haskell Signatures

+1
source

Source: https://habr.com/ru/post/1011912/


All Articles