Reputation: 1822
John Hughes in his "Generalising Monads to Arrows" writes (chapter 8):
We formalise the property that
first f
depends only on first components of pairs as follows:first f >>> arr fst = arr fst >>> f
I understand that the law filters out implementations of such sort:
newtype KleisliMaybe a b = KMb { runKMb :: a -> Maybe b }
instance Category KleisliMaybe where
...
instance Arrow KleisliMaybe where
first f = KMb $ const Nothing
...
But the wording seems a bit odd for this case (I would have chosen "first
has no side-effects" or something like that for such an instance).
So, what are other reasons to keep it around?
Moreover, there is another law: first f >>> second (arr g) = second (arr g) >>> first f
. I did not find any implementations it filters out (I did - see the edit). How does this law help us?
Edit: more thoughts on the latter law.
Take a look at the following snippet:
newtype KleisliWriter = KW { runKW :: a -> (String, b) }
instance Category KleisliWriter where
...
instance Arrow KleisliWriter where
arr f = KW $ \ x -> ("", f x)
first (KW f) = KW $ \ ~(a, d) -> f a >>= (\ x -> ("A", (x, d)))
second (KW f) = KW $ \ ~(d, b) -> f b >>= (\ x -> ("B", (d, x)))
Such an instance behaves this way:
GHCi> c = KW $ \ x -> ("C", x)
GHCi> fst . runKW (first c >>> second (arr id)) $ (1, 2)
"CAB"
GHCi> fst . runKW (second (arr id) >>> first c) $ (1, 2)
"BCA"
As far as I get it, we have got no law for second f = swap >>> first f >>> swap
. Therefore, we can forbid both second
and first
to have any side-effects with this law. Yet, the original wording still does not seem to hint at that again:
...we formalise the intuition that the second component of the pair is unaffected by
first f
as a law...
Are those laws just pure formalisations for solid proofs?
Upvotes: 2
Views: 186
Reputation: 51119
Short answer: There's a different pair of laws that cover "first
and second
have no side-effects":
first (arr f) = arr (first f)
second (arr f) = arr (second f)
After thinking about it, I THINK that both laws you've identified:
first f >>> arr fst = arr fst >>> f -- LAW-A
first f >>> second (arr g) = second (arr g) >>> first f -- LAW-B
are, in fact, redundant because they follow from those no-side-effects laws, the other laws, and a couple of "free theorems".
Your counterexamples violate the no-side-effects laws, so that's why they also violate LAW-A and/or LAW-B. If someone has a true counterexample that obeys the no-side-effects laws but violates LAW-A or LAW-B, I'd be very interested in seeing it.
Long answer:
The property "first
has no side effects (of its own, at least)" is better formalized by the law stated earlier in Section 8 of that article:
first (arr f) = arr (first f)
Recall that Hughes says an arrow is "pure" (equivalently, "has no side-effects") if it can be written arr expr
. So, this law states that, given any computation that is already pure and so can be written arr f
, applying first
to that computation also results in a pure computation (because it is of the form arr expr
with expr = first f
). Therefore, first
introduces no impurities / no effects of its own.
The other two laws:
first f >>> arr fst = arr fst >>> f -- LAW-A
first f >>> second (arr g) = second (arr g) >>> first f -- LAW-B
are intended to capture the idea that for a particular instance Arrow Foo
and a particular arrow action f :: Foo B C
, the action:
first f :: forall d. Foo (B,d) (C,d)
acts on the first components of its input/output pairs as if the second components weren't there. The laws correspond to the properties:
C
and any side effects depend only on input B
, not input d
(i.e., no dependence on d
)d
passes through unchanged, unaffected by the input B
or any side effects (i.e., no effect on d
)With respect to LAW-A, if we consider the action first f :: Foo (B,d) (C,d)
and focus on the C
component of its output using a pure function to extract it:
first f >>> arr fst :: Foo (B,d) C
then the result is the same as if we first forcibly remove the second component using a pure action:
arr fst :: Foo (B,d) B
and allow the original action f
to act only on B
:
arr fst >>> f :: Foo (B,d) C
Here, the structure of the first f >>> arr fst
action leaves open the possibility that first f
can depend on the d
component of the input in formulating its side effects and constructing the C
component of its output; but, the structure of the arr fst >>> f
action eliminates this possibility by removing the d
component via a pure action before allowing any non-trivial computation by f
. The fact that these two actions are equal (the law) makes it clear that first f
produces a C
output (and side effects, through f
, since first
has no additional effects of its own) from the B
input in a manner that can't also depend on the d
input.
LAW-B is is harder. The most obvious way of formalizing this property would be the pseudolaw:
first f >>> arr snd = arr snd
which directly states that first f
doesn't change the extracted (arr snd
) second component. However, Hughes points out that this is too restrictive, because it doesn't allow first f
to have side effects (or at least any that can survive the pure action arr snd
). Instead, he provides the more complicated law:
first f >>> second (arr g) = second (arr g) >>> first f
The idea here is that, if first f
ever modified the d
value, then there would be some case where the following two actions would be different:
-- `first f` changes `inval` to something else
second (arr (const inval)) >>> first f
-- if we change it back, we change the action
second (arr (const inval)) >>> first f >>> second (arr (const inval))
But, because of LAW-B, we have:
second (arr (const inval)) >>> first f >>> second (arr (const inval))
-- associativity
= second (arr (const inval)) >>> (first f >>> second (arr (const inval)))
-- LAW-B
= second (arr (const inval)) >>> (second (arr (const inval)) >>> first f)
-- associativity
= (second (arr (const inval)) >>> (second (arr (const inval))) >>> first f
-- second and arr preserve composition
= second (arr (const inval >>> const inval)) >>> first f
-- properties of const function
= second (arr (const inval)) >>> first f
and so the actions are the same, contrary to our assumption.
HOWEVER, I conjecture that LAW-A and LAW-B are both redundant, because I believe (see my hesitation below) they follow from the other laws plus a "free theorem" for the signature:
first f :: forall d. Foo (B,d) (C,d)
Assuming first
and second
satisfy the no-side-effects laws:
first (arr f) = arr (first f)
second (arr f) = arr (second f)
then LAW-B can be rewritten as:
first f >>> second (arr g) = second (arr g) >>> first f
-- no side effects for "second"
first f >>> arr (second g) = arr (second g) >>> first f
-- definition of "second" for functions
first f >>> arr (\(x,y) -> (x, g y)) = arr (\(x,y) -> (x, g y)) >>> first f
and this last statement (call it LAW-B-prime) is just the free theorem for first f
. (Intuitively, since first f
is polymorphic in the type of d
, any pure action on d
is necessarily "invisible" to first f
, so first f
and any such action will commute.) Similarly, there's a free theorem for:
first f >>> arr fst :: forall d. Foo (B,d) C
that captures the idea that, since this signature is polymorphic in d
, no pure pre-action on d
can affect the action:
arr (\(x,y) -> (x, g y)) >>> (first f >>> arr fst) = first f >>> arr fst
But the left-hand side can be rewritten:
-- by associativity
(arr (\(x,y) -> (x, g y)) >>> first f) >>> arr fst
-- by LAW-B-prime
(first f >>> arr (\(x,y) -> (x, g y))) >>> arr fst
-- by associativity
first f >>> (arr (\(x,y) -> (x, g y)) >>> arr fst)
-- `arr` preserves composition
first f >>> arr ((\(x,y) -> (x, g y)) >>> fst)
-- properties of fst
first f >>> arr fst
giving the right-hand side.
I only hesitate here because I'm not used to thinking about "free theorems" for possibly effectful arrows instead of functions, and so I'm not 100% sure that it goes through.
I'd be very interested to see if someone can come up with true counterexamples for these laws that violate LAW-A or LAW-B but satisfy the no-side-effects laws. The reason your counterexamples violate LAW-A and LAW-B is that they violate the no-side-effects laws. For your first example:
> runKMb (first (arr (2*))) (2,3)
Nothing
> runKMb (arr (first (2*))) (2,3)
Just (4,3)
and for your second:
> runKW (first (arr (2*))) (1,2)
("A",(2,2))
> runKW (arr (first (2*))) (1,2)
("",(2,2))
Upvotes: 2