# Map and Filter functions

There's the last guy called `reduce` but it's not here because it's like the odd guy out and requires a dedicated article to it

## Let us `map`
Before we map, let us take a look at a beautiful for-loop in Python.
```python
squares = []

for i in range (2, 10):
    squares.append(i ** 2)

print(squares) # [4, 9, 16, 25, 36, 49, 64, 89]
```

The point of the above is to take the numbers 2 up to 10 (exclusive) and get the squares of them. With a `map` function, we can easily achieve that with grace and mercy.

The map function, included in the global namespace by default is an elegant way to perform operations on an iterable, basically replacing an expressive for-loop, like so:
```python
# returns an iterator of our results
squares = map(lambda x: x ** 2, range(2, 10))

# we can then get a list with
squares = list(squares)
```

The `map` function accepts two arguments: a function and an iterable; and applies the function to each member of the iterable and spits the result as a generator. Generally, [lambda functions](https://blog.lordsarcastic.dev/lambda-functions-in-python/) are preferred as the function to be used due to its conciseness. We can with this define the internal implementation of a `map` function crudely to be:
```python
from typing import (
  Any,
  Callable,
  Iterable,
  Generator
)


def map(func: Callable, iterable: Iterable) -> Generator[Any, Any, Any]: 
  # the first `Any` is the type of the value being returned
  # we can replace the `Generator` with an `Iterator`
  # but we're not here to learn about types
  for member in iterable:
    yield func(member)

# so we call it like so:
map(func, iterable)
```
With this information, `map` can (not every time) essentially replace your for-loops with a more concise definition that looks linear to the eye especially when you are performing simple operations. We can see more examples of `map` like so:
```python
# This is only for the purpose of demonstration
names = list(map(
  lambda x: (input("Enter a name: ")).strip(),
  range(10)
))

# displays names
print(names)
```

### Performance with the usual `list` wrapper
You may be concerned as to the performance implications of using a map and then wrapping the results in a list. Relieving your fears is the fact that `map` returns a generator-like (an iterator) result which ensures results are yielded only on demand. So wrapping the result in a list would have little or no performance overhead.

## And then `filter` out
Filter on the other hand is a function with a similar structure with `map` except that it executes the function against the iterable and only returns value that evaluates to `True`. In this sense, it:
> Filters out falsities and untrue things, revealing their true nature in an elegant display of absence.

A good way to use `filter` is a to filter out odd numbers from a list:
```python
even_numbers = list(filter(
  lambda x: not (x % 2),
  range(100)
))

print(even_numbers) # -> 0, 2, 4, 6, 8, 10, ..., 98
```
The inner working of `filter` can be described crudely as:
```
from typing import (
  Any,
  Callable,
  Iterable,
  Generator,
  Optional
)

def filter(func: Optional[Callable], iterable: Iterable) -> Generator[Any, Any, Any]:
  for member in iterable:
    result = func(member)
    if result:
      yield result

# so we call it like so:
filter(func, iterable)
```
You'll notice that the `func` attribute is marked as optional. This is because you can pass `None` to `filter`, except that you'll get only values in the iterable that evaluate as `True`.

## And so therefore
Go out there and populate codebases like JS spawns till you get the kind of comment I got on a PR:
> A list comprehension is better than all these `lambda` and `map` you're putting everywhere.

If you're fascinated by lambda functions, those `lambda x: x ** 2` you see in the `filter` and `map` functions, you can read [my article on lambda functions](https://blog.lordsarcastic.dev/lambda-functions-in-python/).
