2016-05-14 00:27:41 +03:00
|
|
|
|
{-# LANGUAGE RankNTypes, ScopedTypeVariables #-}
|
2016-03-11 20:29:17 +03:00
|
|
|
|
module Alignment
|
2016-03-14 21:13:06 +03:00
|
|
|
|
( hasChanges
|
2016-03-11 20:29:17 +03:00
|
|
|
|
, numberedRows
|
2016-03-22 15:20:56 +03:00
|
|
|
|
, AlignedDiff
|
|
|
|
|
, alignDiff
|
2016-04-28 00:44:15 +03:00
|
|
|
|
, alignChildrenInRanges
|
2016-04-25 21:42:38 +03:00
|
|
|
|
, applyThese
|
|
|
|
|
, modifyJoin
|
2016-03-11 20:29:17 +03:00
|
|
|
|
) where
|
2016-02-28 22:01:56 +03:00
|
|
|
|
|
2016-04-15 19:44:04 +03:00
|
|
|
|
import Control.Applicative
|
2016-04-15 04:47:18 +03:00
|
|
|
|
import Control.Arrow ((&&&), (***))
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Control.Comonad.Cofree
|
2016-03-05 04:18:49 +03:00
|
|
|
|
import Control.Monad
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Control.Monad.Free
|
2016-03-10 23:06:03 +03:00
|
|
|
|
import Data.Align
|
2016-04-14 23:34:49 +03:00
|
|
|
|
import Data.Biapplicative
|
2016-03-18 21:30:22 +03:00
|
|
|
|
import Data.Bifunctor.Join
|
2016-03-03 07:11:24 +03:00
|
|
|
|
import Data.Copointed
|
2016-03-12 01:59:58 +03:00
|
|
|
|
import Data.Foldable
|
2016-04-25 18:25:32 +03:00
|
|
|
|
import Data.Function
|
2016-04-04 23:25:16 +03:00
|
|
|
|
import Data.Functor.Both as Both
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Data.Functor.Identity
|
2016-03-03 15:53:23 +03:00
|
|
|
|
import Data.Maybe
|
2016-04-15 18:57:26 +03:00
|
|
|
|
import Data.Monoid
|
2016-04-22 19:18:40 +03:00
|
|
|
|
import qualified Data.OrderedMap as Map
|
2016-04-15 05:00:00 +03:00
|
|
|
|
import Data.These
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Diff
|
2016-03-31 00:26:52 +03:00
|
|
|
|
import Info
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Patch
|
2016-03-01 03:39:04 +03:00
|
|
|
|
import Prelude hiding (fst, snd)
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import Range
|
2016-03-29 00:55:01 +03:00
|
|
|
|
import Source hiding (fromList, uncons, (++))
|
2016-02-28 22:01:56 +03:00
|
|
|
|
import SplitDiff
|
|
|
|
|
import Syntax
|
|
|
|
|
import Term
|
|
|
|
|
|
2016-03-02 00:29:42 +03:00
|
|
|
|
-- | Assign line numbers to the lines on each side of a list of rows.
|
2016-04-15 04:16:02 +03:00
|
|
|
|
numberedRows :: [Join These a] -> [Join These (Int, a)]
|
2016-04-27 00:36:00 +03:00
|
|
|
|
numberedRows = countUp (both 1 1)
|
|
|
|
|
where countUp from (row : rows) = fromJust ((,) <$> modifyJoin (uncurry These) from `applyThese` row) : countUp (modifyJoin (fromThese id id) (succ <$ row) <*> from) rows
|
2016-03-08 01:13:51 +03:00
|
|
|
|
countUp _ [] = []
|
2016-03-02 00:29:42 +03:00
|
|
|
|
|
2016-03-02 16:05:18 +03:00
|
|
|
|
-- | Determine whether a line contains any patches.
|
2016-04-15 04:16:02 +03:00
|
|
|
|
hasChanges :: SplitDiff leaf Info -> Bool
|
|
|
|
|
hasChanges = or . (True <$)
|
2016-03-09 09:36:40 +03:00
|
|
|
|
|
2016-03-22 15:20:56 +03:00
|
|
|
|
type AlignedDiff leaf = [Join These (SplitDiff leaf Info)]
|
2016-03-18 19:02:07 +03:00
|
|
|
|
|
2016-04-15 00:03:53 +03:00
|
|
|
|
alignDiff :: Both (Source Char) -> Diff leaf Info -> AlignedDiff leaf
|
2016-04-15 03:27:24 +03:00
|
|
|
|
alignDiff sources diff = iter (uncurry (alignSyntax (runBothWith ((Join .) . These)) ((Free .) . Annotated) getRange sources) . (annotation &&& syntax)) (alignPatch sources <$> diff)
|
2016-04-15 00:03:53 +03:00
|
|
|
|
|
2016-03-18 21:15:49 +03:00
|
|
|
|
alignPatch :: Both (Source Char) -> Patch (Term leaf Info) -> AlignedDiff leaf
|
2016-04-15 03:27:24 +03:00
|
|
|
|
alignPatch sources patch = case patch of
|
2016-04-15 16:37:01 +03:00
|
|
|
|
Delete term -> fmap (Pure . SplitDelete) <$> hylo (alignSyntax this (:<) getRange (Identity (fst sources))) unCofree (Identity <$> term)
|
|
|
|
|
Insert term -> fmap (Pure . SplitInsert) <$> hylo (alignSyntax that (:<) getRange (Identity (snd sources))) unCofree (Identity <$> term)
|
2016-04-15 03:27:24 +03:00
|
|
|
|
Replace term1 term2 -> fmap (Pure . SplitReplace) <$> alignWith (fmap (these id id const . runJoin) . Join)
|
|
|
|
|
(hylo (alignSyntax this (:<) getRange (Identity (fst sources))) unCofree (Identity <$> term1))
|
|
|
|
|
(hylo (alignSyntax that (:<) getRange (Identity (snd sources))) unCofree (Identity <$> term2))
|
|
|
|
|
where getRange = characterRange . copoint
|
|
|
|
|
this = Join . This . runIdentity
|
|
|
|
|
that = Join . That . runIdentity
|
|
|
|
|
|
2016-05-13 23:39:48 +03:00
|
|
|
|
-- | The Applicative instance f is either Identity or Both. Identity is for Terms in Patches, Both is for Diffs in unchanged portions of the diff.
|
2016-04-15 03:27:24 +03:00
|
|
|
|
alignSyntax :: Applicative f => (forall a. f a -> Join These a) -> (Info -> Syntax leaf term -> term) -> (term -> Range) -> f (Source Char) -> f Info -> Syntax leaf [Join These term] -> [Join These term]
|
|
|
|
|
alignSyntax toJoinThese toNode getRange sources infos syntax = case syntax of
|
2016-04-05 00:18:29 +03:00
|
|
|
|
Leaf s -> catMaybes $ wrapInBranch (const (Leaf s)) . fmap (flip (,) []) <$> sequenceL lineRanges
|
2016-04-28 00:44:15 +03:00
|
|
|
|
Indexed children -> catMaybes $ wrapInBranch (Indexed . fmap runIdentity) <$> alignChildrenInRanges getRange lineRanges (Identity <$> children)
|
|
|
|
|
Fixed children -> catMaybes $ wrapInBranch (Fixed . fmap runIdentity) <$> alignChildrenInRanges getRange lineRanges (Identity <$> children)
|
|
|
|
|
Keyed children -> catMaybes $ wrapInBranch (Keyed . Map.fromList) <$> alignChildrenInRanges getRange lineRanges (Map.toList children)
|
2016-04-05 00:18:29 +03:00
|
|
|
|
where lineRanges = toJoinThese $ actualLineRanges <$> (characterRange <$> infos) <*> sources
|
2016-04-15 03:27:24 +03:00
|
|
|
|
wrapInBranch constructor = applyThese $ toJoinThese ((\ info (range, children) -> toNode (info { characterRange = range }) (constructor children)) <$> infos)
|
2016-03-22 15:20:56 +03:00
|
|
|
|
|
2016-04-28 00:44:15 +03:00
|
|
|
|
alignChildrenInRanges :: (Copointed c, Functor c, Foldable f) => (term -> Range) -> Join These [Range] -> f (c [Join These term]) -> [Join These (Range, [c term])]
|
2016-04-15 03:27:24 +03:00
|
|
|
|
alignChildrenInRanges getRange ranges children
|
2016-04-14 20:19:18 +03:00
|
|
|
|
| Just headRanges <- sequenceL $ listToMaybe <$> ranges
|
2016-04-22 18:57:38 +03:00
|
|
|
|
, (thisLine, nextLines, nonintersecting) <- spanAndSplitFirstLines (intersects getRange headRanges) children
|
2016-04-25 19:06:30 +03:00
|
|
|
|
, thisRanges <- fromMaybe headRanges $ const <$> headRanges `applyThese` unionThese (distribute <$> (thisLine ++ (nextLines >>= distribute)))
|
2016-04-25 18:26:20 +03:00
|
|
|
|
, merged <- pairRangesWithLine thisRanges (modifyJoin (uncurry These . fromThese [] []) (unionThese (distribute <$> thisLine)))
|
2016-04-22 19:18:40 +03:00
|
|
|
|
, advance <- fromThese id id . runJoin . (drop 1 <$) $ unionThese (nextLines >>= copoint)
|
2016-04-28 00:44:15 +03:00
|
|
|
|
, moreLines <- alignChildrenInRanges getRange (modifyJoin (uncurry bimap advance) ranges) (nextLines ++ nonintersecting)
|
|
|
|
|
= merged : moreLines
|
|
|
|
|
| otherwise = fmap (flip (,) []) <$> sequenceL ranges
|
2016-03-31 21:45:19 +03:00
|
|
|
|
|
2016-05-13 23:40:10 +03:00
|
|
|
|
{-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
We align asymmetrically since the first child is asymmetrical, and then continue aligning symmetrically afterwards:
|
|
|
|
|
[ | [
|
|
|
|
|
a |
|
|
|
|
|
, b | b
|
|
|
|
|
] | ]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[ [ Join This (Range 4 5, [ pure (Delete (Info (Range 4 5) mempty 0 :< Leaf "a")) ]) ]
|
|
|
|
|
, [ Join These (Range 4 5, [ liftF (Info (Range 4 5) mempty 0 :< Leaf "b") ])
|
|
|
|
|
(Range 4 5, [ liftF (Info (Range 4 5) mempty 0 :< Leaf "b") ])
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The first child is asymmetrical but there is also a symmetrical child on the same line, so we align symmetrically, producing:
|
|
|
|
|
[ a, b ] | [ b ]
|
|
|
|
|
|
|
|
|
|
and not:
|
|
|
|
|
[ a, b ] |
|
|
|
|
|
| [ b ]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
We align the child symmetrically, and thus have to take the first line range on the right asymmetrically so as not to break the child’s alignment.
|
|
|
|
|
| [
|
|
|
|
|
[ b ] | b
|
|
|
|
|
| ]
|
|
|
|
|
(Eventually, we’ll align the left hand side of this up a line, but that constraint is undecidable for now.)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If a is replaced with b in a Replace patch, we would like to align them side by side (that’s what makes it a replacement—they correlate), but a catamorphism which loses the Replace relationship (by splitting it into two SplitReplaces) can’t know that they’re related:
|
|
|
|
|
[ a ] | [ b ]
|
|
|
|
|
|
|
|
|
|
If a is deleted and b is coincidentally inserted, we want to separate them, because they’re semantically unrelated:
|
|
|
|
|
[ a ] |
|
|
|
|
|
| [ b ]
|
|
|
|
|
|
|
|
|
|
The presence of a symmetrical child forces it to be symmetrical again:
|
|
|
|
|
[ a, c ] | [ c, b ]
|
|
|
|
|
|
|
|
|
|
We might split up children so `This` and `That` aren’t 1:1 with `Delete` and `Insert`. This is because earlier symmetrical children take precedence over later ones:
|
|
|
|
|
[ a, b ] | [ a
|
|
|
|
|
| , b
|
|
|
|
|
| ]
|
|
|
|
|
|
|
|
|
|
Lines without children on them are aligned irrespective of their textual content:
|
|
|
|
|
[\n | [\n
|
|
|
|
|
a\n | a, b\n
|
|
|
|
|
,\n | \n
|
|
|
|
|
b\n | \n
|
|
|
|
|
] | ]
|
|
|
|
|
|
|
|
|
|
[ [ Join That (Range 4 5, [ liftF (Info (Range 4 5) mempty 0 :< Leaf "b") ]) ] ]
|
|
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
2016-05-13 23:40:46 +03:00
|
|
|
|
-- | Given a function to get the range, a list of already-aligned children, and the lists of ranges spanned by a branch, return the aligned lines.
|
2016-05-14 00:27:41 +03:00
|
|
|
|
alignBranch :: forall term. (term -> Range) -> [[Join These term]] -> Both [Range] -> [Join These (Range, [term])]
|
2016-05-13 23:40:46 +03:00
|
|
|
|
-- There are no more ranges, so we’re done.
|
|
|
|
|
alignBranch _ _ (Join ([], [])) = []
|
|
|
|
|
-- There are no more children, so we can just zip the remaining ranges together.
|
|
|
|
|
alignBranch _ [] ranges = runBothWith (alignWith Join) (fmap (flip (,) []) <$> ranges)
|
|
|
|
|
-- There are both children and ranges, so we need to proceed line by line
|
2016-05-13 23:46:53 +03:00
|
|
|
|
alignBranch getRange children ranges = case intersectingChildren of
|
|
|
|
|
-- | No child intersects the current ranges on either side, so advance.
|
|
|
|
|
[] -> (flip (,) [] <$> headRanges) : alignBranch getRange children (drop 1 <$> ranges)
|
2016-05-13 23:51:50 +03:00
|
|
|
|
-- | At least one child intersects on at least one side.
|
2016-05-14 00:29:37 +03:00
|
|
|
|
_ -> if any (and . intersects getRange headRanges . head) children
|
2016-05-13 23:51:50 +03:00
|
|
|
|
-- | No child intersects on both sides, so align asymmetrically.
|
|
|
|
|
then []
|
|
|
|
|
-- | At least one child intersects on both sides, so align symmetrically.
|
2016-05-14 00:16:11 +03:00
|
|
|
|
else let (them, remainingLinesOfIntersectingChildren) = alignChildren intersectingChildren in
|
|
|
|
|
fromJust ((,) <$> headRanges `applyThese` Join (runBothWith These them))
|
|
|
|
|
: alignBranch getRange (remainingLinesOfIntersectingChildren ++ nonIntersectingChildren) (drop 1 <$> ranges)
|
2016-05-14 00:29:37 +03:00
|
|
|
|
where (intersectingChildren, nonIntersectingChildren) = span (or . intersects getRange headRanges . head) children
|
2016-05-13 23:40:46 +03:00
|
|
|
|
Just headRanges = sequenceL $ listToMaybe <$> Join (runBothWith These ranges)
|
2016-05-14 00:15:45 +03:00
|
|
|
|
-- | Given a list of aligned children, produce lists of their intersecting first lines, and a list of the remaining lines/nonintersecting first lines.
|
2016-05-14 00:27:41 +03:00
|
|
|
|
alignChildren :: [[Join These term]] -> (Both [term], [[Join These term]])
|
2016-05-14 00:15:45 +03:00
|
|
|
|
alignChildren [] = (both [] [], [])
|
|
|
|
|
alignChildren ([]:rest) = alignChildren rest
|
2016-05-14 00:31:21 +03:00
|
|
|
|
alignChildren ((firstLine:restOfLines):rest) = case fromThese False False . runJoin $ intersects getRange headRanges firstLine of
|
2016-05-14 00:15:45 +03:00
|
|
|
|
-- | It intersects on both sides, so we can just take the first line whole.
|
2016-05-14 00:31:21 +03:00
|
|
|
|
(True, True) -> let (firstRemaining, restRemaining) = alignChildren rest in
|
2016-05-14 00:27:41 +03:00
|
|
|
|
((++) <$> modifyJoin (fromThese [] []) (pure <$> firstLine) <*> firstRemaining, restOfLines : restRemaining)
|
2016-05-14 00:15:45 +03:00
|
|
|
|
-- | It only intersects on one side, so we have to split it up.
|
2016-05-14 00:31:21 +03:00
|
|
|
|
_ -> (both [] [], [])
|
2016-05-13 23:40:46 +03:00
|
|
|
|
|
2016-05-13 23:40:18 +03:00
|
|
|
|
{-
|
|
|
|
|
|
|
|
|
|
Properties of well-formed alignments:
|
|
|
|
|
|
|
|
|
|
- nodes are aligned in order, with priority given to earlier siblings over later ones (i.e. later nodes can be split up if necessary to allow earlier ones to be aligned)
|
|
|
|
|
- insertions alone on a line should not break the alignment of later siblings
|
|
|
|
|
- ✔︎ nodes should be aligned starting at the same line
|
|
|
|
|
- ✔︎ nodes should have the same height (# of lines spanned) on both sides, padding with blank lines as necessary
|
|
|
|
|
|
|
|
|
|
-}
|
|
|
|
|
|
2016-04-22 19:18:40 +03:00
|
|
|
|
spanAndSplitFirstLines :: (Copointed c, Functor c, Foldable f) => (Join These a -> Join These Bool) -> f (c [Join These a]) -> ([c (Join These a)], [c [Join These a]], [c [Join These a]])
|
|
|
|
|
spanAndSplitFirstLines pred = foldr (go pred) ([], [], [])
|
|
|
|
|
where go pred child (this, next, nonintersecting)
|
|
|
|
|
| (first : rest) <- copoint child
|
2016-04-22 17:39:03 +03:00
|
|
|
|
, ~(l, r) <- splitThese first
|
|
|
|
|
= case fromThese False False . runJoin $ pred first of
|
2016-04-22 19:18:40 +03:00
|
|
|
|
(True, True) -> ((first <$ child) : this, (rest <$ child) : next, nonintersecting)
|
|
|
|
|
(True, False) -> ((fromJust l <$ child) : this, (maybe rest (: rest) r <$ child) : next, nonintersecting)
|
|
|
|
|
(False, True) -> ((fromJust r <$ child) : this, (maybe rest (: rest) l <$ child) : next, nonintersecting)
|
2016-04-22 18:57:38 +03:00
|
|
|
|
_ -> (this, next, child : nonintersecting)
|
|
|
|
|
| otherwise = (this, next, nonintersecting)
|
2016-04-13 18:08:01 +03:00
|
|
|
|
|
2016-04-15 19:45:33 +03:00
|
|
|
|
unionThese :: (Alternative f, Foldable f, Monoid (f a)) => f (Join These a) -> Join These (f a)
|
|
|
|
|
unionThese as = fromMaybe (Join (These empty empty)) . getUnion . fold $ Union . Just . fmap pure <$> as
|
2016-04-14 16:59:46 +03:00
|
|
|
|
|
2016-04-01 17:56:44 +03:00
|
|
|
|
pairRangesWithLine :: Monoid b => Join These a -> Join These b -> Join These (a, b)
|
|
|
|
|
pairRangesWithLine headRanges childLine = fromMaybe (flip (,) mempty <$> headRanges) $ (,) <$> headRanges `applyThese` childLine
|
2016-04-01 17:55:25 +03:00
|
|
|
|
|
2016-04-15 16:35:41 +03:00
|
|
|
|
-- | Test ranges and terms for intersection on either or both sides.
|
2016-04-15 03:27:24 +03:00
|
|
|
|
intersects :: (term -> Range) -> Join These Range -> Join These term -> Join These Bool
|
|
|
|
|
intersects getRange ranges line = fromMaybe (False <$ line) $ intersectsRange <$> ranges `applyThese` (getRange <$> line)
|
2016-03-29 01:27:01 +03:00
|
|
|
|
|
2016-04-15 16:31:28 +03:00
|
|
|
|
-- | Split a These value up into independent These values representing the left and right sides, if any.
|
2016-04-15 16:33:30 +03:00
|
|
|
|
splitThese :: Join These a -> (Maybe (Join These a), Maybe (Join These a))
|
|
|
|
|
splitThese these = fromThese Nothing Nothing $ bimap (Just . Join . This) (Just . Join . That) (runJoin these)
|
2016-03-29 01:46:22 +03:00
|
|
|
|
|
|
|
|
|
infixl 4 `applyThese`
|
|
|
|
|
|
2016-04-15 16:29:23 +03:00
|
|
|
|
-- | Like `<*>`, but it returns its result in `Maybe` since the result is the intersection of the shapes of the inputs.
|
2016-03-29 01:46:22 +03:00
|
|
|
|
applyThese :: Join These (a -> b) -> Join These a -> Maybe (Join These b)
|
2016-04-15 16:29:23 +03:00
|
|
|
|
applyThese (Join fg) (Join ab) = fmap Join . uncurry maybeThese $ uncurry (***) (bimap (<*>) (<*>) (unpack fg)) (unpack ab)
|
|
|
|
|
where unpack = fromThese Nothing Nothing . bimap Just Just
|
2016-03-29 16:58:31 +03:00
|
|
|
|
|
2016-04-15 16:30:20 +03:00
|
|
|
|
-- Map over the bifunctor inside a Join, producing another Join.
|
2016-03-24 21:18:53 +03:00
|
|
|
|
modifyJoin :: (p a a -> q b b) -> Join p a -> Join q b
|
2016-03-24 21:26:25 +03:00
|
|
|
|
modifyJoin f = Join . f . runJoin
|
2016-04-15 04:47:18 +03:00
|
|
|
|
|
|
|
|
|
-- | Given a pair of Maybes, produce a These containing Just their values, or Nothing if they haven’t any.
|
|
|
|
|
maybeThese :: Maybe a -> Maybe b -> Maybe (These a b)
|
|
|
|
|
maybeThese (Just a) (Just b) = Just (These a b)
|
|
|
|
|
maybeThese (Just a) _ = Just (This a)
|
|
|
|
|
maybeThese _ (Just b) = Just (That b)
|
|
|
|
|
maybeThese _ _ = Nothing
|
|
|
|
|
|
2016-04-25 18:38:35 +03:00
|
|
|
|
-- Distributes a copointed functor through another functor.
|
2016-04-25 18:33:13 +03:00
|
|
|
|
--
|
2016-04-25 18:38:35 +03:00
|
|
|
|
-- This allows us to preserve any associated state while embedding the contents of the other functor into it.
|
|
|
|
|
distribute :: (Copointed c, Functor c, Functor f) => c (f a) -> f (c a)
|
2016-04-25 19:00:33 +03:00
|
|
|
|
distribute c = (<$ c) <$> copoint c
|
2016-04-25 18:19:51 +03:00
|
|
|
|
|
2016-04-15 19:15:47 +03:00
|
|
|
|
-- | A Monoid wrapping Join These, for which mappend is the smallest shape covering both arguments.
|
|
|
|
|
newtype Union a = Union { getUnion :: Maybe (Join These a) }
|
2016-04-22 17:39:15 +03:00
|
|
|
|
deriving (Eq, Functor, Show)
|
2016-04-15 16:23:41 +03:00
|
|
|
|
|
|
|
|
|
|
2016-04-15 16:26:16 +03:00
|
|
|
|
-- | Instances
|
|
|
|
|
|
2016-04-15 19:15:47 +03:00
|
|
|
|
instance Monoid a => Monoid (Union a) where
|
2016-04-15 04:47:18 +03:00
|
|
|
|
mempty = Union Nothing
|
2016-04-15 19:15:47 +03:00
|
|
|
|
Union (Just a) `mappend` Union (Just b) = Union $ Join <$> uncurry maybeThese (uncurry (***) (bimap mappend mappend (unpack a)) (unpack b))
|
|
|
|
|
where unpack = fromThese Nothing Nothing . runJoin . fmap Just
|
2016-04-15 04:47:18 +03:00
|
|
|
|
Union (Just a) `mappend` _ = Union $ Just a
|
|
|
|
|
Union _ `mappend` Union (Just b) = Union $ Just b
|
|
|
|
|
_ `mappend` _ = Union Nothing
|
2016-04-15 16:26:16 +03:00
|
|
|
|
|
|
|
|
|
instance Bicrosswalk t => Crosswalk (Join t) where
|
|
|
|
|
crosswalk f = fmap Join . bicrosswalk f f . runJoin
|