Curious about this. Usually I try to get an Apt source (I use Ubuntu). Why would I want to use brew on Linux? I stopped using Macs and brew around 2018 when Apple started closing down it's macOS for a few years and got sick of it. Thanks!
Perhaps you can also explain what a box is, and what a flatMap is please? Fortunately I know what map is already. Also, what does it mean to combine boxes in order? Thanks!
Sure! "Box" here is used to just abstractly describe a value that contains other values. Let's take a list as an example:
[1, 2, 3] :: [Int]
Here, the "box" is a list, and inside of it are the values 1, 2, and 3.
As you know, `map` is an operation that converts the values inside of the box into other values; for example, adding 1 to every element:
[1, 2, 3] :: [Int]
| | | (+ 1)
v v v
[2, 3, 4] :: [Int]
But the operation you perform with `map` doesn't need to keep the values of the same type:
[ 1, 2, 3 ] :: [Int]
| | | (show)
v v v
["1", "2", "3"] :: [String]
The operation can also produce new boxes! Since `String` is actually itself a list (`[Char]`), the result above is the same as
[ 1, 2, 3 ] :: [Int]
| | | (show)
v v v
[['1'], ['2'], ['3']] :: [[Char]]
In some cases, you might want to "flatten" this box-of-boxes together. In some languages this operation is called "flatten"; for lists in Haskell, it's called `concat`
[['1'], ['2'], ['3']] :: [[Char]]
| | | (concat)
v v v
[ '1', '2', '3' ] :: [Char]
This example isn't terribly motivating, but you can see when you have deeper lists-of-lists how this might be handy:
[[1,2,3], [4,5,6], [7,8,9]] :: [[Int]]
| | | (concat)
v v v
[1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Int]
Here, we took a collection of boxes (`[[Int]]`) and combined them in order (sequentially) to produce a new box (`[Int]`).
What other languages call `flatMap` is just a `map` operation followed by a `flatten` operation. Very roughly, `Functor` gives you "map" (`map`), `Applicative` gives you "flatten" (`concat`), and `Monad` gives you "flatMap" (`concatMap`).
The power of these comes from considering different types of "boxes". `Maybe`, for example, works almost like a list that can contain up to 1 element, and its operations behave pretty much identically. Other types are interesting because how you define their "box-ness" can lead to interesting/useful results. It can be tough to envision how, e.g., a function could look like a "box", but it turns out that you can define rules for it that make it useful. (What does "map" look like for a function? Well, it turns out that mapping a function over another function is already just... function composition!)
You can go a lot deeper into these definitions, and it helps to look at some implementations to grok them better, but the core concepts themselves are not very complicated. The "magic" is in how you define the "boxes".
To expand a bit, too, on how these definitions make side effects easier to represent in Haskell:
One way to represent side effects in a purely functional language is to model them as if they aren't side effects, by representing them as state changes in the "outside world". You don't need to grok the specifics of this, but the definition of the `IO` monad is:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
i.e., it's a "pure" transformation of the "real world".
This allows you to define a "box" called `IO` that represents a computation that can perform a side-effect (by affecting the "real world"), then returning a value.
The real trick to this is that the "box" is entirely opaque to you: unlike a list or a `Maybe` where you know how to reach in and pull values _out_ (e.g., `head`, `last`, `fromJust`, etc.), `IO` doesn't allow you to do this*. Once you have something inside of an `IO` box, it's stuck there.
This means that you can separate the "impure" world from the "pure" world: you can't perform side effects arbitrarily — you're can only do so in an `IO` context that's intentionally "viral".
The functor/applicative/monad rules just make `IO` easier to use and consume:
1. `Functor` allows you to "map" over the results of a computation
2. `Applicative` allows you to chain computations together in order so side effects happen in sequence
3. `Monad` makes it easier to repeatedly chain computations within a single `IO` context (so if you need to perform repeated side effects, you can "stay" in the outer context — `IO a` instead of `IO (IO (IO (IO (... (IO a)))))`)
This is just one way to represent side effects, and the monad rules are only really needed to make this representation ergonomic to actually use.
(*There is technically a way to "escape" the `IO` monad called `unsafePerformIO`, but you basically never need to use this. If you find yourself reaching for it, don't.)
reply