I've decided that I should get to learn more about Haskell, because (from what
totherme,
michiexile and Duncan say) it sounds like it's got much cooler since I last touched it. So today I sat down to write a program to generate valid Siteswaps for juggling patterns. A siteswap is a list of throw heights: "5" means that you throw a ball sufficiently high that the next time it will be thrown is in five beats' time, and so on. So a Three-Ball Cascade is 3, Right Middle Left is 423, and so on. Wikipedia has more information. A candidate pattern is juggleable if you don't have two balls trying to land at the same time.
It took me about three hours and 53 lines, and a lot of that time was spent wrestling with the type system. Debug.Trace was invaluable, though a bit of a pain to use. Anyway, here's the code: do any of you clever Haskell types out there have any comments for how I could have done things better?
Now, a quick
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
It took me about three hours and 53 lines, and a lot of that time was spent wrestling with the type system. Debug.Trace was invaluable, though a bit of a pain to use. Anyway, here's the code: do any of you clever Haskell types out there have any comments for how I could have done things better?
-- newtype SiteSwap = SS [Int] deriving (Show, Read, Eq, Ord) -- this just turned out to be a hassle collision :: [Int] -> Bool collision xs = (length (filter (==0) xs)) >= 2 --isJuggleable :: SiteSwap -> Bool isJuggleable [] = False isJuggleable ss = isJuggleable' [] 0 (length ss) (cycle ss) isJuggleable' :: [Int]->Int->Int->[Int]->Bool isJuggleable' falling fallen maxfallen (s:ss) | collision $ map (\x->x-1) falling = False -- a pattern is not juggleable if two balls collide | fallen > maxfallen = True -- a pattern is juggleable if all the initial balls have landed safely | otherwise = isJuggleable' newfalling newfallen maxfallen ss where (newfalling, newfallen) = advance s falling fallen advance :: Int->[Int]->Int->([Int], Int) advance height falling fallen = (advanceFalling height falling, advanceFallen falling fallen) advanceFalling :: Int->[Int]->[Int] advanceFalling height falling = height:(filter (>0) (map (subtract 1) falling)) -- Has to be (>0) because of possibility of throwing 0s (ie Gaps) -- This way, a throw of 0 gets turned to -1 by (subtract 1) and doesn't trigger `collision`. advanceFallen falling fallen = fallen + (length (filter (==0) (map (subtract 1) falling))) isRepetitive word = or $ map ((flip isRepeated) word) (tail $ reverse $ prefixes word) -- Lop off the final prefix, which will be the whole word. Else test becomes degenerate prefixes :: [a]->[[a]] prefixes [] = [] prefixes (w:ws) = [w]:(map (w:) (prefixes ws)) -- is word a repetition of candidate? isRepeated candidate word = isRepeated' (cycle candidate) word isRepeated' _ [] = True isRepeated' (c:cs) (w:ws) | c == w = isRepeated cs ws | otherwise = False finiteLists xs = concat $ map (finiteLists' xs) [0..] finiteLists' xs 0 = [[]] finiteLists' xs n = (concat $ outer (:) xs (finiteLists' xs (n-1))) outer f [] _ = [] outer f (x:xs) ys = (map (f x) ys):(outer f xs ys) juggleablePatterns = map (concatMap show) $ filter (\x -> ((isJuggleable x) && (not $ isRepetitive x))) (finiteLists [1..9])
Now, a quick
take 10000 juggleablePatterns
should keep me busy for the next several lifetimes...Tags:
no subject
That's because, of the 3 ways in haskell to declare new data types, you've picked the wrong one :)
The three ways are: ["data", "type", "newtype"]
data actually creates a new data structure, using the sort of syntax you were aiming for there. Like "data Tree x = Leaf x | Branch (Tree x, Tree x)" etc...
This is great if you're doing something genuinely new, but there, you weren't. You actually just wanted a list of Ints, but you wanted to refer to them as something else. For that, we have type synonyms. To make your programme do what you want, add the line:
type SiteSwap = [Int]
And do a s/[Int]/SiteSwap/ over the rest of the code. I just tried it in my copy - it works. (there's no need for all the deriving stuff there - the new type has all the properties of the old one, including the ones you were trying to derive)
As for newtype - I believe it does something useful, but I've not yet sat down and read what ;)
no subject
http://www.cs.auckland.ac.nz/references/haskell/haskell-intro-html/moretypes.html
So, I feel justified in not bothering about them for the most part ;)
no subject
To clarify, I'm not (yet) criticizing Haskell's type system, just my own understanding of it. Things are improving: I'm starting to remember some of the common pitfalls and avoiding them.
no subject
Good, good ;)
Just thinking about that though - it might be worth, when you get what appears to be a spurious type error, taking a copy of the code that produced the error, hiding it, and getting on with your debugging. Then, when you have a working programme, see if the code actually meant what you were thinking at the time - or if real runtime bugs were caught.
Of course, that would have been fairly pointless in this case, since a bug in constructing a type is genuinely one you can avoid by not having types ;)
no subject
no subject
So, that's why :) For more context about the problem being solved, follow the lisppaste link to "Context in IRC logs".
no subject
There's only one style point I think I noticed while I was playing, and that's the advance function. Do you really need it? It seems to me that all it does is aggregate one common argument of two distinct 2-arg functions. You could probably do:
...instead. I dunno - doing without advance saves about 1 line in this particular file - if you were using it somewhere else too, the saving would go the other way. Meh :)
As for the rest, it's just knowing what libraries are out there :)
Take a look at Data.List - it contains the function inits, which allows you to refactor prefixes into prefixes = drop 1 . inits - which is pretty cool. It also contains findIndices, which allows us to render prefixes and isRepeated obsolete by redefining:
One cool way to find out about this sort of stuff is with hoogle. There's also a lambdabot plugin that does the same job - which you can also get as part of GHCi On Acid.
Finally:
I thought I had a really cool way of refactoring finiteLists - but it turns out not quite :)
I thought, looking at the output of that function, that what it was doing was actually just counting. So finiteLists xs would count in base (length xs), using the members of xs as symbols. That being the case, I figured it should be possible to define:
...for some appropriate definition of toBase.
I got the definition of toBase that I wanted - and thus discovered that my original idea was flawed:
(it reverses the order of the output of course:
(reverse $ toBase "0123456789" 65537) == "65537"
...but that's just for ease of list manipulation)
The problem is of course, that finiteLists "abcd" will never produce the string "aa". There's no bug in the code - it's just that when we count: 0 == 00 == 000 == ...
Damn, huh? I thought I was being so cunning there, too :)
(and the end result is more than twice as long as the original 3 line function - lol)
no subject
(from memory)
Unfortunately, this wasn't as serious as the other bug I found - most of the patterns generated aren't actually juggleable! I think my test needs to be made stricter, but I'm not quite sure how :-(
no subject
Ideally, with an explaination as to why they're not juggleable ;)
no subject
I'm guessing that the pattern 4100 isn't juggleable, since:
So, in the pattern 4100, starting with the left, the left hand would always be throwing to itself, and the right hand would always be throwing to the left. So where do the balls come from? And since twice as many balls land in the left as are thrown by it, where do they go?
In my copy of the programme, 4100 passes the isJuggleable test - because it only checks for colisions, not resourcing.
I suppose that might be fixable by crossing your arms at appropriate points to even out the resourcing, but at that stage, my grip on the rules of our model get shakey ;)
no subject
The problem seems most obvious in patterns involving 0 (ie Gaps), but you're right, it may well have to do with resourcing. So you'd have to check that at each throw (unless it has weight 0) there's a ball waiting to be thrown (ie, an entry in the falling queue with height 0). Up to off-by-one errors. The question is, when do you stop checking?
no subject
Now, in this case, we've got new balls being introduced, and nowhere for them to go, so there's an inevitable collision, which the code can pick up. This problem can slip by the current code in patterns with 0s in them, I think.
btw - if you present 531 as 153, you have to pick up an extra ball on the second cycle, to make up for only having two in the first - but then the pattern levels out. 135 actually continues demanding more resourses, and obliterating them in colisions ;)
Even without 0s, I suspect that a difficult-to-detect "getting too many balls in/aimed-at one hand, so there's nothing to throw from the other" problem is still possible...
Thinking about your averaging the weights thing (and it's far too late for me to really appriciate that) - I have a vague intuition that in the likes of 135, the 1 spoils the formula by persistantly providing second-hand the ball that 3 would like to mint afresh... So that in some sense, the 1 gets cancelled out, and the true measure of the thing is (3+5)/3 rather than (1+3+5)/3...
Hm - I wonder how blatently wrong that'll look in the morning.... Ah well :)
no subject
no subject
Ah, yeah, good point. The real reason it's like that is because I originally wrote it as one function (there's code duplicated between advanceFalling and advanceFallen), and had to split it into two to make sense of the compilation errors I was getting. But now it's fixed, I should either refactor them back into one function properly or eliminate advance.
no subject
no subject
no subject
no subject