1

Let us consider a dwarf wandering in a tunnel. I will define a type that represents this situation thusly:

data X a = X { xs :: [a], i :: Int }

display :: X Bool -> IO ()
display X{..} = putStrLn (concatMap f xs) where { f True = "*" ; f False = "-" }

Here you see a dwarf in a section of a tunnel:

λ display x
-*---

It is discovered that a pointed container is an instance of Comonad. I can use this instance here to define a function that simulates my dwarf moving right:

shiftRight :: X Bool -> Bool
shiftRight x@X{..} | let i' = i - 1 in i' `isInRange` x && xs !! i' = True
                   | otherwise = False

See:

λ traverse_ display $ scanl (&) x (replicate 4 (extend shiftRight))
-*---
--*--
---*-
----*
-----

Spectacularly, this same operation works with any number of dwarves, in any pointed container, and so can be extended to a whole dwarf fortress if desired. I can similarly define a function that moves a dwarf leftwards, or in any other deterministic fashion.

But now what if I want my dwarf to wander around aimlessly? Now my "shift randomly" must only place a dwarf to the right if the same dwarf is not being placed to the left (for that would make two dwarves out of one), and also it must never place two dwarves in the same place (which would make one dwarf out of two). In other words, "shift randomly" must be linear (as in "linear logic") when applied over a comonadic fortress.

One approach I have in mind is to assign some sort of state to dwarves that tracks the available moves for a dwarf, removing moves from every relevant dwarf when we decide that the location is taken by one of them. This way, the remaining dwarves will not be able to take that move. Or we may track availability of locations. I am thinking that some sort of a "monadic" extendM might be useful. (It would compare to the usual extend as traverse compares to fmap.) But I am not aware of any prior art.

Ignat Insarov
  • 4,660
  • 18
  • 37
  • 1
    Perhaps you are looking for a `RandT` (random monad transformer): https://hackage.haskell.org/package/MonadRandom-0.1.3/docs/Control-Monad-Random.html – Willem Van Onsem Aug 29 '19 at 13:41
  • Slightly off topic: you can simplify your code to ``shiftRight x@X{..} = let i' = i - 1 in i' `isInRange` x && xs !! i'``. No need for guards here. – chi Aug 29 '19 at 15:05
  • I suspect it will be tricky if you want to select uniformly at random from among the possible ending configurations. Certainly you can't just pick an ordering on the dwarves and then just pick a direction uniformly at random from among the legal moves for that dwarf; for example, the configuration `*-*` can evolve to three valid next configurations, and for each dwarf there are two configurations where they step away from the other and only one where they step towards. – Daniel Wagner Aug 29 '19 at 20:06
  • @DanielWagner Yes, I know that. If we pick a random total ordering on dwarves, we may let the first dwarf choose freely and then restrict the choices of the second dwarf according to whether the first dwarf decided to occupy the contested middle location. Then, if we shuffle the ordering often enough, it will be fair overall. – Ignat Insarov Aug 29 '19 at 20:28
  • 1
    Take the 1D case for a moment. Consider just the even positions. For each run of n consecutive dwarves, there are exactly n+1 outcomes: the leftmost 0 <= i <= n step left, and the remaining n-i step right. Each maximal consecutive run may choose independently. How the dwarves on odd positions move may also be chosen independently, by the same mechanism. This should get you exactly the right distribution for 1D dwarves; but it isn't clear to me at all how to extend this to two dimensions. – Daniel Wagner Aug 29 '19 at 20:49
  • @DanielWagner [Turns out there is a straightforward general solution through matchings on a bipartite graph between the current and the next state.](https://math.stackexchange.com/a/3348444/278244) – Ignat Insarov Sep 08 '19 at 15:41
  • @IgnatInsarov Hah, your idea of straightforward and mine don't agree! Solving #P-complete problems... in one sense I suppose it's straightforward in that "there's nothing really clever you can do, so just brute-force it", but in another very real sense it means it's not reasonably solvable exactly. – Daniel Wagner Sep 08 '19 at 17:15

1 Answers1

2

The easiest way to solve this is by using the MonadRandom library, which introduces a new monad for random computations. So let’s set up a computation using random numbers:

-- normal comonadic computation
type CoKleisli w a b = w a -> b

-- randomised comonadic computation
type RCoKleisli w a b = w a -> Rand b

Now, how to apply this thing? It’s easy enough to extend it:

halfApply :: Comonad w => (w a -> Rand b) -> (w a -> w (Rand b))
halfApply = extend

But this doesn’t quite work: it gives us a container of randomised values, whereas we want a randomised container of values. In other words, we need to find something which can do w (Rand b) -> Rand (w b). And in fact there does exist such a function: sequenceA! As the documentation states, if we apply sequenceA to a w (Rand b), it will run each Rand computation, then accumulate the results to get a Rand (w b) — which is exactly what we want! So:

fullApply :: (Comonad w, Traversible w, Applicative f)
          => (w a -> f b) -> (w a -> f (w b))
fullApply c = sequenceA . extend c

As you can see from the type signature above, this actually works for any Applicative (because all we require is that each applicative computation can be run in turn), but requires w to be Traversible (so we can traverse over each value in w).

(For more on this sort of thing, I recommend this blog post, plus its second part. If you want to see the above technique in action, I recommend my own probabilistic cellular automata library, back when it still used comonads instead of my own typeclass.)

So that answers one half of your question; that is, how to get probabilistic behaviour using comonads. The second half is:

… and also it must never place two dwarves in the same place …

This I’m not too sure about, but one solution could be to split your comonadic computation into three stages:

  1. Convert every dwarf probabilistically to a diff stating whether that dwarf will move left, right, or stay. Type for this operation: mkDiffs :: X Dwarf -> Rand (X DwarfDiff)
  2. Execute each diff, but keeping the original dwarf positions. Type for this operation: execDiffs :: X DwarfDiff -> X (DwarfDiff, [DwarfDiffed]).
  3. Resolve situations where dwarfs have collided. Type for this operation: resolve :: X (Dwarf, [DwarfDiffed]) -> Rand (X Dwarf).

Types used above:

data Dwarf = Dwarf | NoDwarf
data DwarfDiff = MoveLeft | MoveRight | DontMove | NoDiff
data DwarfDiffed = MovedFromLeft | MovedFromRight | NothingMoved

Example of what I’m talking about:

myDwarfs = X [NoDwarf                ,Dwarf                     ,NoDwarf                                ,Dwarf                    ,Dwarf                     ,Dwarf      ] 0
mkDiffs myDwarfs
         = X [NoDiff                 ,MoveRight                 ,NoDiff                                 ,MoveLeft                 ,MoveRight                 ,DontMove   ] 0
execDiffs (mkDiffs myDwarfs)
         = X [(NoDiff,[NothingMoved]),(MoveRight,[NothingMoved]),(NoDiff,[MovedFromRight,MovedFromLeft]),(MoveLeft,[NothingMoved]),(MoveRight,[NothingMoved]),(DontMove,[MovedFromLeft])] 0
resolve (execDiffs (mkDiffs myDwarfs))
         = X [NoDwarf                ,NoDwarf                   ,Dwarf                                  ,Dwarf                    ,Dwarf                     , Dwarf     ] 0

As you can see, the above solution is pretty complicated. I have an alternate recommendation: don’t use comonads for this problem! Comonads are great for when you need to update one value based on its context, but are awful at updating multiple values simultaneously. The issue is that comonads such as your X are zippers, which store a data structure as a single ‘focused’ value plus a surrounding ‘context’. As I said, this is great for updating a focused value based on its context, but if you need to update multiple values, you have to shoehorn your computation into this value+context mould… which, as we saw above, can be pretty tricky. So possibly comonads aren’t the best choice for this application.

bradrn
  • 8,337
  • 2
  • 22
  • 51