All suggestions are good so far. Here is another, which may seem a little strange at first, but it turns out to be very convenient in many other situations.
Some type formation operators, such as [] , which is an operator that displays the type of elements, for example. Int to the list type of these elements [Int] , have the property of being Applicative . For lists, this means that there is some way, indicated by the operator, <*> , pronounced "apply" to list function lists and argument lists in result lists.
(<*>) :: [s -> t] -> [s] -> [t] -- one instance of the general type of <*>
not your regular application given by empty space, or $
($) :: (s -> t) -> s -> t
The result is that we can do normal functional programming with lists of things instead of things: we sometimes call it "programming in a list idiom." The only other component is that to cope with a situation where some of our components are separate things, we need an additional gadget
pure :: x -> [x] -- again, one instance of the general scheme
which wraps the thing as a list compatible with <*> . This pure moves the usual meaning into the applicative idiom.
For lists, pure simply makes a singleton list, and <*> gives the result of each pairwise application of one of the functions to one of the arguments. In particular,
pure f <*> [1..10] :: [Int -> Int -> Int -> Int -> Int]
is a list of functions (like map f [1..10] ), which can be used with <*> again. The rest of the arguments to f are not lists, so you need pure them.
pure f <*> [1..10] <*> pure 1 <*> pure 2 <*> pure 3 <*> pure 4
For lists, this gives
[f] <*> [1..10] <*> [1] <*> [2] <*> [3] <*> [4]
i.e. a list of ways to make an application from f, one of [1..10], 1, 2, 3, and 4.
The discovery of pure f <*> s so common, it is abbreviated f <$> s , therefore
f <$> [1..10] <*> [1] <*> [2] <*> [3] <*> [4]
- this is what is usually written. If you can filter out the noises <$> , pure and <*> , this is similar to the application you had in mind. Additional punctuation is necessary only because Haskell cannot determine the difference between counting a list of functions or arguments and non-listy computing what is intended as a single value, but it turns out to be a list. However, at least the components are in the order you started, so you can more easily see what happens.
Esoterics. (1) in my (not so) private Haskell dialect , above would be
(|f [1..10] (|1|) (|2|) (|3|) (|4|)|)
where each bracket of the idiom, (|f a1 a2 ... an|) represents the application of a pure function to zero or more arguments that live in the idiom. This is just a way to write
pure f <*> a1 <*> a2 ... <*> an
Idris has idiomatic brackets, but Haskell did not add them. Nonetheless.
(2) In languages ββwith algebraic effects, the idiom of non-deterministic computing is not the same thing (typechecker type) as the data type of lists, although you can easily convert between them. Program becomes
f (range 1 10) 2 3 4
where the range non-deterministically selects a value between the given lower and upper bounds. Thus, uncertainty is seen as a local side effect, rather than a data structure that allows operations to be made for failure and selection. You can wrap non-deterministic calculations in a handler that gives values ββto these operations, and one of these handlers can generate a list of all solutions. This means that additional notations to explain what is happening are pushed to the border, and do not penetrate the entire interior, like those <*> and pure .
Managing the boundaries of things, not their interiors, is one of the few good ideas that our species has managed to have. But at least we can repeat it over and over. This is why we are instead of a farm instead of hunting. This is why we prefer static type checking to dynamic tag checking. And so on...