0

This works :

genAnimal :: Gen String
genAnimal = do
  animals <- shuffle ["tiger","rabbit","dragon","snake","rat","ox","pig","sheep","horse","monkey","dog"]
  return (head animals)

genWinner :: Gen String
genWinner = do 
  animal <- genAnimal
  prize <- choose (10::Int,1000::Int)
  return (unwords (replicate 3 animal) ++ " " ++ show prize)

genTicket :: Gen String
genTicket = do 
  animals <- replicateM 3 genAnimal
  prize <- choose (10::Int,1000::Int)
  return (unwords animals ++ " " ++ show prize)

genTickets :: Gen [String]
genTickets = do
  tickets <- replicateM 6 (oneof [genWinner, genTicket])
  return tickets

But it looks just ungainly, is there a more sensible way to combine these generators? It basically picks three random animals and then a random prize and then makes six tickets.

Cliff Stamp
  • 531
  • 5
  • 11
  • 2
    [`elements :: [a] -> Gen a`](https://www.haskell.org/hoogle/?hoogle=%5Ba%5D+-%3E+Gen+a). – AJF Aug 26 '18 at 07:41
  • I find it rather strange that a ticket is a `String`. The idea of having a (static) type system is that it guarantees certain contracts. By thus introducing a `Ticket` data type, you make a contract that the result is a `Ticket`, not just a *random* string. – Willem Van Onsem Aug 26 '18 at 08:11
  • It's not clear to me what those generators are supposed to do. AFAICT, `genWinner` and `genTicket` have the same behaviour. Can you elaborate on the goal? – Mark Seemann Aug 26 '18 at 15:36
  • genWinner generates three of the same animal, genTicket picks three random ones which could be the same or not. – Cliff Stamp Aug 26 '18 at 18:50

1 Answers1

1

As @AJFarmar noted, you can use the elements combinator to pick a random element from a list, instead of taking the head of a shuffled version:

genAnimal :: Gen String
genAnimal = elements ["tiger","rabbit","dragon","snake","rat","ox",
                      "pig","sheep","horse","monkey","dog"]

Also, an applicative style can generally be more succinct, particularly if you introduce helper functions for the pure computations that combine the results:

genPrize :: Gen Int
genPrize = choose (10,1000)

genTicket :: Gen String
genTicket = ticket <$> replicateM 3 genAnimal <*> genPrize
  where ticket animals prize = unwords animals ++ " " ++ show prize

A few other miscellaneous improvements might come from:

  • shifting your perspective on your generators to generate words that you combine at the end, instead of strings that have to be unworded and glued together with blanks;
  • or better yet, introducing some datatypes to better model your problem and providing a utility function to print it in the appropriate String form only when needed;
  • getting rid of the gen prefixes that clutter everything up
  • using the vectorOf alias for replicateM which is a little more readable in this context.

This might lead to something like:

{-# OPTIONS_GHC -Wall #-}

module Lottery where

import Test.QuickCheck

type Animal = String
data Ticket = Ticket [Animal] Int

pticket :: Ticket -> String
pticket (Ticket ts prz) = unwords ts ++ ' ':show prz

tickets :: Gen [Ticket]
tickets = vectorOf 6 (oneof [winner, loser])
  where winner = Ticket <$> (replicate 3 <$> animal) <*> prize
        loser  = Ticket <$> (vectorOf 3 animal)      <*> prize
        animal = elements ["tiger","rabbit","dragon","snake","rat","ox",
                           "pig","sheep","horse","monkey","dog"]
        prize  = choose (10,1000)

main :: IO ()
main = print . map pticket =<< generate tickets

which I think looks pretty good.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71