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.
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.
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.
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.