You've correctly identified the problem: what you're generating is functions with the property ∃ a≠b. f a≠f b
(which is readily true for most random functions anyway), whereas what you want is ∀ a≠b. f a≠f b
. That is a much more difficult property to ensure, because you need to know about all the other function values for generating each individual one.
I don't think this is possible to ensure for general input types, however for word specifically what you can do is “fake” a function by precomputing all the output values sequentially, making sure that you don't repeat one that has already been done, and then just reading off from that predetermined chart. It requires a bit of laziness fu to actually get this working:
import qualified Data.Set as Set
newtype Injective = Injective ([Char] {- simply a list without duplicates -})
deriving Show
instance Arbitrary Injective where
arbitrary = Injective . lazyNub <$> arbitrary
lazyNub :: Ord a => [a] -> [a]
lazyNub = go Set.empty
where go _ [] = []
go forbidden (x:xs)
| x `Set.member` forbidden = go forbidden xs
| otherwise = x : go (Set.insert x forbidden) xs
This is not very efficient, and may well not be ok for your application, but it's probably the best you can do.
In practice, to actually use Injective
as a function, you'll want to wrap the values in a suitable structure that has only O (log n) lookup time. Unfortunately, Data.Map.Lazy
is not lazy enough, you may need to hand-bake something like a list of exponentially-growing maps.
There's also the concern that for some insufficiently big result types, it is just not possible to generate injective functions because there aren't enough values available. In fact as Joseph remarked, this is the case here. The lazyNub
function will go into an infinite loop in this case. I'd say for a QuickCheck this is probably ok though.
Fun Word Char
can't be injective, since there's moreWord
s than there areChar
s. – WavemeterMonadFix Gen
to definef
recursively, so thatf n
is a random number distinct fromf 0 .. f (n-1)
. I'm not sure, however, if this will actually work (it looks unfeasible right now?). Another possibility is to usearbitrary
once to first generate a seed of a RNG, and then definef
recursively without messing with the monadGen
. It will have the same (or worse) statistical issues thatsplit
has on RNG seeds, but since we are only using it for QuickCheck it could be good enough. – Afterburner