Unfortunately, while you may not have appreciated the tone of the Haskell interaction, they are correct in their assessment from a factual perspective. This explanation propagates a number of misunderstandings of the topics well known to be endemic to beginners.
In particular, I observed the common belief that functors apply to "containers", when in fact they apply to things that are not containers as well, most notably functions themselves, and it also contains the common belief that a monad has "a" value, rather than any number of values. For instance, the "list monad" will confuse someone operating on this description because when the monad "takes the value out of the list", it actually does it once per value in the list. This is the common "monad as burrito" metaphor, basically, which isn't just bad, but is actually wrong.
I'm not limiting it to these errors either, these are just the ones that leap out at me.
I agree. The "container" intuition for Monads leaves you stuck when you try to contemplate IO (or even Promises, these days), because the "bind" operator looks like it does something impossible: extract "the" `a` from the `IO a`, when you have no idea what it is. (Trust me, I spent a long time stuck at this point.) Better to think of Monad as "Applicative + join" (you need Applicative to get `pure`).
If you think of Monads in terms of `fmap` + `join :: Monad m => m (m a) -> m a`, then you don't need to imagine an "extraction" step and your intuition is correct across more instances. Understanding `join` gives you an intuition that works for all the monads I can think of, whereas the container intuition only works for `Maybe` or `Either e` (not even `[]`, even though it _is_ a container). You can define each of `>>=`/`join`/`>=>` in terms of `pure` + any of the other two, and it is an illuminating exercise to do so. (That `class Monad` defines `>>=` as its method is mostly due to technical GHC reasons rather than anything mechanical.)
I prefer the "join" approach for beginners too, but >>= has become so pervasive that I feel bad trying to explain it that way. Turning people loose on monad-heavy code with that approach still leaves them needing to convert their understanding into >>= anyhow.
One does wonder about the alternate world where that was the primary way people interacted with it.
I think you don't have to teach people to program with `join`, but just that `m >>= f = join (fmap f) m`. It explains away the "get a value out" question but teaches the most common function from the interface.
Bartosz Milewski argues that we can think of functions etc. as containers as well, if you check out his YouTube lectures on Category Theory for Programmers. Lists and functions "contain" a type.
A term's utility comes from its ability to separate things into different categories. A definition of "container" that includes everything is therefore useless, because if everything a container, there is no information in the statement that something is a container.
In Bartosz's case he's probably making the precise point that we can abstract out to that point and that at a super, super high level of category theory, there isn't anything that isn't a container. However, that's a didactic point, not a general truth. In general we programmers generally do mean something by the word "container", and functors can indeed include things that are therefore not containers.
Moreover, I would say it's not what the author was thinking. The author is not operating on that super high level of category theory.
A list [b] is a container for bs indexed by integers. A function a->b is a container for bs indexed by as.
[b] is more like a blueprint for a container, and a->b is more like an assembly line of containers.
> A function a->b is a container for bs
Anecdotally, this is one of those things that's trivially true to some people, but really hard for other people to internalize. I think it's why the "container" can lead people astray- if you haven't internalized the idea of functions as being indexed by their argument, it's a really mind twisting thing to try to make that leap.
One of the fun things about Clojure that reinforces this "trivially true" perspective is that maps and sets are functions:
;; "maps" the keys to the values
(map {1 "a" 2 "b"} (take 5 (cycle 1 2))) ;;=> '("a" "b" "a" "b" "a")
;; acts as a predicate that tests for membership
(filter #{"a" "b" "c"} ["a" "b" "c" "d" "e" "f"]) ;;=> '("a" "b" "c")
Once you get used to this idiom you naturally start thing of other functions (or applicative functors) the same way. The syntax sugar makes for some very concise and expressive code too. If I give you a function "f(x) := 3 * x", is it really that useful to talk about it as a container of the natural numbers?
The reverse though is useful, a container looks like a function that takes one or more indices and returns a value or element.
I think that understanding the (moral) equivalence is useful in both directions. In particular, I think helping people understand the "function-as-container" analogy is a useful way for people to understand pure functions- another thing that's conceptually simple but a lot of people struggle to really wrap their mind around it.
Personally I would say it muddies the water for me, as "container" has strong connotations in other directions.
But then I've never thought the concept of a pure function to be particularly difficult, despite growing up on procedural languages.
It's other bits that I struggle with when it comes to functional programming.
I can recommend learnung some scala, where HashMap extends PartialFunction
I've never looked at scala, but that's really interesting. Do you find that's useful in practice?
> really hard [...] leap
Two stepping stones might be array getters (function that's array-ish), and arrays with an indexed default value function (array that's function-ish)?
I've recently started writing a series of blog posts (https://rebeccaskinner.net/posts/2024-10-18-dictionaries-are...) trying to explain the idea and my approach has been to explain the idea using comprehensions. I haven't had a lot of people review the post yet, and I still have at least one if not two more follow-ups before it's done, so I'm not yet sure how well the idea will land.
Nice introduction. Still not entirely sold that dictionaries are pure functions, though.
Will you be covering common dictionary operations like adding/removing elements and iterating over the dictionary keys?
I have some ideas on how one might frame it in a pure function setting but they all seem quite contorted in a similar way to your incrementDict, ie you'd never actually do that, so curious if there are better ways. Then maybe you'll sell me on the premise.
I'm really focusing less on the idea that Dict the data type with it's associated methods is like a function, and more on the idea that a dictionary in the general sense is a mapping of input values to output values, and you can think of functions that way.
That said, there are some pretty reasonable analogies to be made between common dictionary operations and functions.
For example, adding and removing items can be done with function composition so long as you are okay with partial lookups. Here's a really short example I put together:
module Example where
import Control.Applicative
type Dict a b = Eq a => a -> Maybe b
emptyDict :: Dict a b
emptyDict = const Nothing
singleton :: a -> b -> Dict a b
singleton k v target
| k == target = Just v
| otherwise = Nothing
unionDict :: Dict a b -> Dict a b -> Dict a b
unionDict dict1 dict2 k = dict1 k <|> dict2 k
insertDict :: a -> b -> Dict a b -> Dict a b
insertDict k v dict = singleton k v `unionDict` dict
removeDict :: a -> Dict a b -> Dict a b
removeDict k dict target
| k == target = Nothing
| otherwise = dict k
This particular representation of dictionaries isn't necessarily something you'd really want to do, but the general approach can be quite useful when you start working with something like GADTs and you end up with things like: data Smaller a where
SmallerInt :: Smaller Int
SmallerBool :: Smaller Bool
data Larger a where
LargerInt :: Larger Int
LargerBool :: Larger Bool
LargerString :: Larger String
someLarger :: Larger x -> x
someLarger l =
case l of
LargerInt -> 5
LargerBool -> True
LargerString -> "foo"
embedLarger ::
(forall x. Larger x -> Smaller x) ->
(forall smallerI. Smaller smallerI -> r) ->
(forall largerI. Larger largerI) -> r
embedLarger mapping fromSmaller larger = fromSmaller (mapping larger)
(I'm actually co-authoring a talk for zurihac this year on this pattern, so I have quite a bit more to say on it, but probably not ideal to cram all of that into this comment). > and more on the idea that a dictionary in the general sense is a mapping of input values to output values, and you can think of functions that way.
So what's the difference between a map and a dictionary then?
> Here's a really short example I put together
Much appreciated. I don't really know Haskell (nor any other functional language), but I'm pretty sure I understood it.
> This particular representation of dictionaries isn't necessarily something you'd really want to do
Yeah that's pretty much what I had in mind, and yes it's possible but it feels forced. For one you're not actually removing an element, you just make it impossible to retrieve. A distinction that might seem moot until you try to use it, depending on the compiler magic available.
> I'm actually co-authoring a talk for zurihac this year on this pattern
Sounds interesting, will check it out when it's published.
> So what's the difference between a map and a dictionary then?
You're asking good questions and catching me being imprecise with my language. Let me try to explain what I'm thinking about more precisely without (hopefully) getting too formal.
When I say "a function is a mapping of values" I'm really trying to convey the idea of mathematical functions in the "value goes in, value comes out" sense. In a pure function, the same input always returns the same output. If you have a finite number of inputs, you could simply replace your function with a lookup table and it would behave the same way.
When I talk about dictionaries, I'm speaking a little loosely and sometimes I'm taking about particular values (or instances of a python Dict), and other times I'm being more abstract. In any case though, I'm generally trying to get across the idea that you have a similar relationship where for any key (input) you get a particular output.
(aside: Literally right now as I'm typing this comment, I also realize I've been implicitly assuming that I'm talking about an _immutable_ value, and I've been remiss in not mentioning that. I just want to say that I really appreciate this thread because, if nothing else, I'm going to edit my blog post to make that more clear.)
The main point is that dictionaries are made up of discrete keys and have, in Python at least, a finite number of keys. Neither of those constraints necessarily apply to functions, so we end up in an "all dictionaries are functions, but not all functions are dictionaries" situation.
> Yeah that's pretty much what I had in mind, and yes it's possible but it feels forced. For one you're not actually removing an element, you just make it impossible to retrieve. A distinction that might seem moot until you try to use it, depending on the compiler magic available.
This is a great example of the kind of thinking I'm trying to address in the article. You're completely right in a very mechanical "this is what memory is doing in the computer" sort of sense, but from the standpoint of reasoning about the problem space deleting an element and being unable to access the element are the same thing.
Of course in the real world we can't completely handwave away how much memory our program uses, or the fact that a function encoding of a dictionary turns a constant time lookup into a linear time lookup. Those are real concerns that you have to deal with for non-trival applications, even in a pure functional language.
The benefit you get, and I apologize because this is hard to explain- let alone prove, is that you can often end up with a much _better_ solution to problems when you start by handwaving away those details. It opens up the solution space to you. Transformations to your architecture and the way you think about your program can be applied regardless of the specific representation, and it's a really powerful way to think about programming in general.
Thanks for the detailed responses, highly appreciated.
I taught myself programming as a kid using QBasic, and quickly moved on to Turbo Pascal and assembly, so clearly my programming career was doomed from the start[1].
For one I do like to keep in mind how it will actually be executed. The times I've not done that it has usually come back to bite me. But that perhaps hampers me a bit when reading very abstract work.
That said I like going outside my comfortable box, as I often find useful things even though they might not be directly applicable to what I normally do. Like you say, often changing the point of view can help alot, something that can often be done in a general way.
Anyway, looking forward to the rest of the article series and the talk.
[1]: https://en.wikiquote.org/wiki/Edsger_W._Dijkstra#How_do_we_t...
Interesting. I liked how "Dictionaries are Pure Functions" set up currying as JSON nested dictionaries.
Curiously, I've a backburnered esolang idea of gathering up the rich variety of dict-associated tooling one never gets to have all in one place, and then making everything dict-like. Permitting say xpath sets across function compositions.
One can start with a partial explanation and expand it cover all the cases as learning progresses. This is how most learning takes place. I expect your primary school teachers introduced numbers with the natural numbers, instead of, say, transfinite numbers. Students learn Newtonian physics before relativity. It's completely fine to build an understanding of monads as operating on containers, and then expand that understanding as one encounters more cases.
An intuition of monads built on "flattening" nested layers of `m` is easier to teach and works for more monads.
In general, in abstract mathematics no analogy or "intuitive concept" of something will ever replace the rigorous definition. That doesn't mean that imperfect analogies can't be useful, though. You just have to use them as a starting point instead of stopping there.
I think the container analogy can be useful up to a point. There is (potentially) something of value wrapped in another type (e.g. an integer "wrapped in" IO) and we usually cannot access it directly (because of various reasons: because IO is special, because a list may be empty, etc.), but we can string together some operations that manipulate the contents implicitly.
Thinking too concretely about monads as boxes might make the behavior of the ListT monad transformer seem a bit surprising... unless you were already imagining your box as containing Schrodinger's cat.
I can definitely understand the author taking offense to the interaction, but now that a lot more programmers have had some experience with types like Result<T> and Promise<T> in whatever their other favorite typed language with generics is, the box/container metaphors are probably less helpful for those people than just relating the typeclasses to interfaces, and pointing out that algebraic laws are useful for limiting the leakiness of abstractions.
Functions are just containers of calculations (the whole “code is data”).
I don’t know why lists as values in a container would be confusing. Lots of very popular languages literally have box types which may not be exactly the same, but show that expecting containers to potentially commission complex data isn’t unusual.
One source for confusion around lists is that the list monad is often used to model non-determinism, rather than just "many things". If you're thinking about non-determinism, a list is akin to a container of one item when you don't precisely know which item it is, but do know it's one of zero or more candidates.
The most widely recognised example, IMO, would be monadic parser combinators. "A parser for a thing, is a function from a string, to a list of pairs of strings and things."
> I don’t know why lists as values in a container would be confusing.
The GP makes it pretty clear - the misunderstanding is that there is one value in a container. A list has many.
That's like saying a dev would be confused that an Object can contain a list. I can't see that tripping up anyone but the most junior of developers.