Commutative by construction
2 Aug 2013 09:11 pmRecently gelisam posted an article on a combinatorial approach to provably commutative binary operators, which has a decent discussion on reddit. Here, I'm going to generalize that idea.
Edit (2013-08-03T05:06:58Z): Okay, I should be done tweaking things now.
Edit (2013-08-03T06:02:40Z): See also the followup post.
Preliminary Definitions
Let A
and B
be arbitrary types, and let κ be any non-zero cardinal number (we only really care about finite cardinals, or maybe at most aleph-naught). Let the notation A^{κ}
denote a multiset of elements drawn from A
with cardinality exactly κ. And let the notation A^{<κ}
denote a non-empty multiset of elements drawn from A
with cardinality not greater than κ.
Commutative operators
A κ-commutative operator from A
to B
can be thought of as a function of type A^{κ} → B
. For example:
- The 1-commutative functions are all the functions of type
A → B
. - The 2-commutative functions are commutative binary functions, thus a subtype of
A → A → B
. - The 3-commutative functions are ternary functions which return the same result for all permutation orders of their arguments, thus a subtype of
A → A → A → B
.
A (<κ)-commutative operator from A
to B
can be thought of as a function of type A^{<κ} → B
, i.e., a polymorphic function of type ∀λ. 0<λ<κ ⇒ A^{λ} → B
. Thus, the (<κ)-commutative functions can be thought of as a family of λ-commutative functions, for all non-zero λ not greater than κ, satisfying suitable coherence conditions. However, for finite κ, a simpler representation is to think of them in terms of folds over non-empty multisets; that is, as 4-tuples:
(C, h:A→C, f:A→C→C, g:C→B)
such that
∀a,a'∈A, c∈C. f a (f a' c) == f a' (f a c)
∀a,a'∈A. f a (h a') == f a' (h a)
Alternatively, if you are willing to include the case where λ=0
, then use 4-tuples:
(C, c0:C, f:A→C→C, g:C→B)
such that
∀a,a'∈A, c∈C. f a (f a' c) == f a' (f a c)
Quasi-unordering pairs
Gelisam's solution to the higher-order problem is to generalize unordered pairs to quasi-unordered pairs: which are, either an unordered pair or else some summary information about a pair which can't be unordered. The way we do this is to have an observation function we apply to each argument. If that observation function can distinguish the arguments, then we can pass the arguments to the function in a canonical order such that we do not betray the original order of the inputs. However, if we cannot distinguish the two arguments, then the best we can do is to return the observation we made on both of them— since, if we returned the actual arguments themselves then we'd betray their input order. Rather than hard-coding the choice of Bool
as gelisam did, we can generalize to any totally ordered set of observations:
distinguish :: Ord r ⇒ (a → r) → Commutative a (Either r (a,a))
distinguish f = Commutative $ \ x y ↦
let fx = f x
fy = f y
in
case compare fx fy of
LT ↦ Right (x,y)
EQ ↦ Left fx
GT ↦ Right (y,x)
Quasi-unordering multisets
Now, we want to generalize this to multisets with any non-zero cardinality. Note, because the observations r
are totally ordered, the observation function induces an almost-linear partial order on the original domain a
. Assume we have some suitable abstract implementation of multisets: Confusion a
, which implements a^{<κ}
for some particular κ. Because the observation function induces an order on our original domain, there exists a function:
observe_0 :: Ord r ⇒ (a → r) → Confusion a → [Confusion a]
such that
∀ r f xs0. isOrdered (observe_0 f xs0)
where
isOrdered (xs:xss) =
(∀x,x'∈xs. f x == f x')
⋀ case xss of
[] ↦ True
ys:_ ↦
(∀ x∈xs, y∈ys. f x < f y)
⋀ isOrdered xss
That is, every element of the result is a multiset of elements which are equivalent under the observation function, and the elements of previous multisets precede the elements of subsequent multisets. That is, we can take the input multiset which is inherently unordered and return a nearly-linear DAG.
As written observe_0
doesn't get us that far, since the implementation of multisets is abstract, and all we've done is break one multiset up into a bunch of multisets. Now, assume our multisets support the function isSingleton :: Confusion a → Maybe a
which tells us whether a multiset is a singleton or not, and gives us the element if it is. Using this, we can strengthen our observing function to:
observe_1 :: Ord r ⇒ (a → r) → Confusion a → [Either (Confusion a) a]
observe_1 f
= map (\xs ↦ maybe (Left xs) Right (isSingleton xs))
. observe_0 f
That gets us pretty much what we want. However, in order to gain the full functionality we had in the binary case, we'd like to have some way to eliminate the remaining multisets. Probably the most general way is to define the following function which also returns the shared observation for classes of inputs which are still confused:
observe_2 :: Ord r ⇒ (a → r) → Confusion a → [Either (r, Confusion a) a]
-- or, even better
observe_2' :: Ord r ⇒ (a → r) → Confusion a → [(r, Either (Confusion a) a)]
We could implement this in terms of observe_0
, however, that means we'd be calling the observation function redundantly since observe_0
throws away the results. Thus, it's more efficient to just implement this version directly, in lieu of implementing If we're returning the result of observing those confusion sets, then why bother returning the confusion set too? The reason is that this way we can apply our observing function again in order to further distinguish those confusion sets. Therefore, this version is complete for composition of observation functions whereas the version that discards the confusion set is not.
And, as a final note, we'd also want to ensure that our multisets support the operation size :: Confusion a → Cardinal
; otherwise, we need to return a triple of the multiset, its size, and its observation. The binary case could avoid this detail since there all confusion sets must be of cardinality 2.