Reputation: 2293
I'm working on some code for a card game:
splitCards_ (x:xs) (a,b,c) | isGold x = splitCards xs (a:x,b,c)
| isAction x = splitCards xs (a,b:x,c)
| isVP x = splitCards xs (a,b,c:x)
splitCards_ [] (a,b,c) = (a,b,c)
splitCards xs = splitCards_ xs ([],[],[])
Essentially, taking a list of cards, and splitting it into three different lists depending on the type of the card. splitCards_
represents state updates by recursively updating it's parameters, then splitCards
(the actual function) is used to always start the computation with the three lists of specific types of cards empty.
I believe this is called state-passing style, and I'm pretty sure that's perfectly idiomatic, but I'm more concerned with the fact that I have to define a helper function splitCards_
in order to get this to work the way I want. Is creating helper functions like this idiomatic Haskell? Is there a cleaner way to write this? Are there naming conventions preferable to just putting an underscore on the end of the name of the helper function?
Upvotes: 5
Views: 380
Reputation: 116139
In the general case, code as yours is idiomatic, especially if cleaned up a bit as @Tikhon suggested. In the specific case, your recursion scheme appears to be a left fold, so it is usually made that explicit in the code. Building upon @Tikhon's code:
splitCards xs = foldl' separate ([],[],[]) xs
where separate (a, b, c) x | isGold x = (a:x,b,c)
| isAction x = (a,b:x,c)
| isVP x = (a,b,c:x)
The first line can even be eta-contracted (not much needed in this case, IMHO, but ...)
splitCards = foldl' separate ([],[],[])
where ...
Upvotes: 1
Reputation: 52039
I have nothing much to add to @Tikhon's answer except that in this specific case you could use the partition
function from Data.List
:
splitCards xs =
let (as,bs) = partition isGold xs
(cs,ds) = partition isAction bs
in (as,cs,ds) -- (golds, actions, others)
Due to lazy evaluation it should have essentially the same performance as your hand-crafted version.
Upvotes: 3
Reputation: 68152
Yes, this is a perfectly idiomatic style. It's a classic way to make a function tail recursive. (Although that's less important and slightly more nuanced in Haskell).
Moreover, making helper functions is definitely good and a key part of many common patterns. If you feel that something is more natural factored out, go for it! Unless it's taken to an extreme, it helps readability.
The main suggestion I have is putting the helper function into a where
clause. This way, it will only be visible in the scope of the main function which makes it clear that it's just a helper. The name you give the helper function is less important; splitCards_
is fine, but splitCards'
(pronounced "splitCards prime") and go
(a generic name for helper functions) would be more common. So I would rewrite your code something like this:
splitCards xs = go xs ([],[],[])
where go (x:xs) (a, b, c) | isGold x = go xs (a:x,b,c)
| isAction x = go xs (a,b:x,c)
| isVP x = go xs (a,b,c:x)
go [] (a, b, c) = (a, b, c)
Note that these are just cosmetic changes—what you were doing is fundamentally sound.
The two naming options (go
vs splitCards'
) is just a matter of preference.
I personally like go
because, once you're used to the convention, it clearly signals that something is just a helper function. In a sense, go
is almost more like syntax than a function of its own; it's meaning is purely subordinate to the splitCards
function.
Others do not like go
because it is a little cryptic and somewhat arbitrary. It might also make the recursive structure of your code less clear because you're recursing on go
and not the function itself.
Go for whatever you think looks best.
Upvotes: 9