问题
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.
回答1:
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:
- 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)
- Execute each diff, but keeping the original dwarf positions. Type for this operation:
execDiffs :: X DwarfDiff -> X (DwarfDiff, [DwarfDiffed])
. - 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.
来源:https://stackoverflow.com/questions/57711476/random-walk-on-a-pointed-container