16

I've run in to a few situations where I need the list:

[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)] -- no (0,0)

Note that there is no (0,0) in the list. I use the (dx,dy) tuples to search up, down, left, right and diagonally from a coordinate.

Every time I write it I feel like there should be a more concise, and/or easier to read way to define it. I'm relatively new to Haskell and I figure somewhere in the bag of Applicative/Functor/Monad tricks there should be a neat way to do this.

I've tried:

[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]

Sometimes it's better just to write it out, but I don't think this is one of those times. It's not obvious with a glance that (0,0) isn't included, and you have to read it a bit to notice the pattern.

map (\[a,b] -> (a,b)) $ delete [0,0] $ sequence $ replicate 2 [-1,0,1]

I like the one above because I get to put the "2" in there which is a nice explicit way to say "We're doing the same sort of thing twice", but I can't accept the map in front with the big unsafe lambda and 2 names.

[(dx,dy) | let a = [-1,0,1], dx <- a, dy <- a, (dx,dy) /= (0, 0)]

This one has too many names in it, but uses the list comprehension exactly as it is designed. It might be easier to read for somebody who really likes list comprehensions, but I don't like all the names.

let x = [-1,0,1] in delete (0,0) $ (,) <$> x <*> x

That one looks prettier imo, but I don't have that "2" in there, and I have a name. This is my favorite so far, but it doesn't feel perfect.

I think if I understood how to write this better I might have a deeper understanding of Functors/Monads or the like. I've read about them quite a bit, and I've heard a bunch of words like fmap/mconcat/etc, but I don't know which one to grab for in this situation.

Jake Brownson
  • 469
  • 2
  • 9
  • 2
    You might be overanalyzing such a small snippet. I personally like `delete (0, 0) $ liftA2 (,) [-1..1] [-1..1]`. – Dietrich Epp Jan 23 '14 at 23:25
  • 2
    Deitrich's solution is concise but I find the traditional comprehension more readable, `[ (a,b) | a <- [-1..1] , b <- [-1..1], not (a == 0 && b == 0)]`. – Thomas M. DuBuisson Jan 23 '14 at 23:27
  • Yeah, I recognize the over-analysis, but as I mention at the end there's more to the Q, like I just learned that liftA2 is appropriate here and might recognize another need for it. – Jake Brownson Jan 23 '14 at 23:27
  • oh and shoot, I was doing [-1,0,1] because I was thinking I would have to do [-1,0..1], but you have to do that when you count down, not when you start below 0 [smack] – Jake Brownson Jan 23 '14 at 23:29

7 Answers7

25

Actually, I think it's indeed best to write it out explicitly in this case. Just align it reasonably, and no questions stay open whatsoever:

neighbours = [ (-1,-1), (-1,0), (-1,1)
             , ( 0,-1),         ( 0,1)
             , ( 1,-1), ( 1,0), ( 1,1) ]

There's no way any alternative could be more self-explanatory than this.

Of course, there are more concise alternatives. Being a physics guy, I'd tend to

   [ (round $ - sin φ, round $ - cos φ) | φ <- [pi/4, pi/2 .. 2*pi] ]

which is of course more expensive to compute, but that doesn't matter if you only define this list at one place and re-use it from all over you program. The order is different here, not sure if that matters.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • That's a great point about changing the formatting to make it more obvious! also, very cute w/ #2 :). – Jake Brownson Jan 23 '14 at 23:34
  • 6
    BTW, the angle solution is a nice example for why Haskell actually got it right with the [controversial definition of enum ranges for floats](http://stackoverflow.com/questions/7290438/haskell-ranges-and-floats/7296160#7296160): if it was required that all elements are `<=` the terminating one, then we couldn't be sure that something close to `2 π` actually turns up, because its float representation might be smaller than the accumulated series. Thus `(0,-1)` might stay missing in the result. – leftaroundabout Jan 23 '14 at 23:42
  • Wow, did some reading and learned about a corner of Haskell I totally didn't expect when I asked this Q. Thanks! – Jake Brownson Jan 23 '14 at 23:49
  • @leftaroundabout, I have no complaint about the details of the `Enum` instance-my complaint is that the instance shouldn't exist at all (and neither should the class as currently conceived). – dfeuer Jul 03 '15 at 02:16
24

Why not just use a list comprehension? They can have boolean guards in them, so excluding (0,0) is pretty simple:

[(i,j) | i <- [-1..1], j <- [-1..1], (i,j) /= (0,0)]

Note that as a Haskell newb myself, there is likely a more compact/efficient way to write that guard expression. Nonetheless, this gets the job done.

ase
  • 13,231
  • 4
  • 34
  • 46
Ben
  • 6,023
  • 1
  • 25
  • 40
  • 1
    This is very similar to one of the answers I listed, as I mentioned I don't like introducing that many names so much, but it is pretty readable other than that. – Jake Brownson Jan 23 '14 at 23:33
  • 1
    It's true that Haskell encourages point-free functions; personally, I find this optimally compact and readable, but then I'm from a Java background - we do love our names :) – Ben Jan 23 '14 at 23:40
  • 6
    The names are not visible to the outside of the list comprehension. – Mihai Maruseac Jan 24 '14 at 00:09
  • Guard expressions aren't going to be more efficient. Everything should compile down to reasonably similar stuff. – hugomg Jan 24 '14 at 14:37
15
Prelude Data.Ix Data.List> delete (0,0) (range ((-1,-1),(1,1)))
[(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
6

Here's a concise version using stuff from Control.Applicative:

delete (0, 0) $ (,) <$> [-1..1] <*> [-1..1]

Personally, I think this looks much better using &, which is just $ flipped.

(,) <$> [-1..1] <*> [-1..1] & delete (0, 0)

You could also use liftA2 instead of <$> and <*>:

liftA2 (,) [-1..1] [-1..1] & delete (0, 0)

It's defined in the lens library, among other place, but you can define it yourself:

infixl 1 &
x & f = f x

All that said, I still prefer the version with the list comprehension or even just the literal list with good indentation.

Tikhon Jelvis
  • 67,485
  • 18
  • 177
  • 214
2
liftA2 zip (!!0) (!!1) . transpose . tail . replicateM 2 $ [0,1,-1]

(Not that I recommend this.)

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • 1
    Very interesting answer. I agree it's not a good readable solution, but it definitely taught me a couple of interesting tools that are related to this! – Jake Brownson Jan 23 '14 at 23:55
  • 1
    It's interesting that if you change "replicateM 2"->"replicateM 3" you get every tuple duplicated, so the answer is specific to 2D. It happens because the transposed result gets 2x the number of entries in the first two lists and the third list is ignored since the 2 is hardcoded in the liftA2. I'm thinking that the answer I'm really looking for won't exist and would require a liftAN of sorts? This answer also says "2" twice, even though one is in a name its a pseudo-parameter of sorts I'd say. – Jake Brownson Jan 24 '14 at 00:02
  • There are actually 3 mentions of '2' in the answer. One is in the 'zip', but it's omitted by convention. Here's the 3D version of this: `liftA3 zip3 (!!0) (!!1) (!!2) . transpose . tail . replicateM 3 $ [0,1,-1]` – Jake Brownson Jan 24 '14 at 22:34
2
Prelude> let sqrt' n | n == 0 = [0] | otherwise = [sqrt n, negate (sqrt n)]

Prelude> [(x,y) | x <- [-1..1]
                , y <- sqrt' (1 - x^2) ++ (if x==0 then [] else sqrt' (2 - x^2))]
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • 1
    @NewAlexandria Quite the opposite. Answers "without an explanation" often stimulate thinking, learning and imagination. –  Jan 25 '14 at 00:42
  • Then take it up on meta.SO. Until then, policy is that answers without discussion are low quality and subject to deletion. FYI. – New Alexandria Jan 26 '14 at 06:22
  • @NewAlexandria Thank you for informing me about a policy. I'm still curious why you chose to note a "lack of explanation" only on this answer when there are two other answers on this page that seem to fit your criteria (not to mention hundreds if not thousands of some of the most informative, interesting, and fun answers on Stackoverflow as a whole). – גלעד ברקן Jan 26 '14 at 14:03
  • I did not audit this Question, and flag many answers. I reviewed a flag that was issued.. I agree that others are bad-by-standards, too. Including those with lots of upvotes – New Alexandria Jan 27 '14 at 22:48
  • May I suggest `nub` to get rid of the repeated root? – dfeuer Jul 03 '15 at 02:20
  • @dfeuer what repeated root? I don't understand. – גלעד ברקן Jul 03 '15 at 16:24
1

For easier to read you could try:

left = (-1,0)
right = (1,0)
up  = (0,1)
down = (0,-1)
--etc
directions = [left,right,up,down]

These are really vectors, so you might want to consider using a vector library if it makes sense in your app, or create your own custom vector operations:

import Data.Vect
vecUp = Vec2 0,1
vecDown = Vec2 0,(-1)
--etc.
goUp10 = 10 *& vecUp -- '*&' is the scalar multiply operator
brent
  • 11
  • 1