-
-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add robot name exercise #123
base: main
Are you sure you want to change the base?
Conversation
It looks like there's a Roc bug causing crashes on Ubuntu (but not on my Macbook), so the tests fail. If I replace the # Random.roc
module [State, u32, seed, Generator]
State a := { state : a }
u32 = \min, max ->
a = 1664525 # multiplier
c = 1013904223 # increment
\@State { state } ->
newState = (((state |> Num.toU64) * a + c) % 0x100000000) |> Num.toU32
{
value: newState % (max - min + 1) + min,
state: @State { state: newState },
}
seed : U32 -> State U32
seed = \startSeed -> @State { state: startSeed }
Generator a b : State a -> { value : b, state : State a } So it looks like the issue is triggered by the roc-random package somehow. I'll file a Roc issue. |
I've reproduced the issue using the official Roc image based on Ubuntu 22.04. I updated the package to roc-random 0.2.2 and sadly I still get the issue. As explained above, I've tried to replace Num.addChecked
Num.addWrap
Num.bitwiseXor
Num.mulWrap
Num.shiftLeftBy
Num.shiftRightZfBy
Num.intCast
Num.add
Num.sub
Num.rem
Num.toI16
Num.toI32
Num.toI64
Num.toI8
Num.toU16
Num.toU32
Num.toU8
Num.maxI16
Num.maxI32
Num.maxI8
Num.minI16
Num.minI32
Num.minI8 If there's a bug in any of them, I would bet it's one of the first ones because they're not used that often. The other possibility is that there's an overflow somewhere that's mishandled. The code seems to deal with values close to the integer bounds. Otherwise, the only vaguely "exotic" thing I see is this: Generator uint value : State uint -> Generation uint value
## A pseudorandom value, paired with its [Generator]'s output state (for chaining)
Generation uint value : { value : value, state : State uint }
## Internal state for [Generator]s
State uint := { s : uint, c : AlgorithmConstants uint } The Any help resolving this issue would be much appreciated. 🙏 |
Quick minimization: app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.2/cfMw9d_uxoqozMTg7Rvk-By3k1RscEDoR1sZIPVBRKQ.tar.br",
}
import rand.Random
import pf.Stdout
main =
# Characters in consecutive names should not be correlated
# we truncate the list to speed up the tests
truncatedNames0 = manyNames0 |> List.takeFirst correlationSampleSize
truncatedNames1 = manyNames0 |> List.dropFirst 1 |> List.takeFirst correlationSampleSize
[0, 1, 2, 3, 4]
|> List.joinMap \index1 -> [0, 1, 2, 3, 4] |> List.map \index2 -> (index1, index2)
|> List.all \(index1, index2) ->
maybeChars = truncatedNames0 |> List.mapTry \chars -> chars |> List.get index1
maybeCharsNext = truncatedNames1 |> List.mapTry \chars -> chars |> List.get index2
maybeChars |> seemsIndependentEnoughFrom maybeCharsNext
|> Inspect.toStr
|> Stdout.line
## A factory is used to create robots, and hold state such as the existing robot
## names and the current random state
Factory := {
existingNames : Set Str,
state : Random.State U32,
}
## A robot must either have no name or a name composed of two letters followed
## by three digits
Robot := {
maybeName : Result Str [NoName],
factory : Factory,
}
createFactory : { seed : U32 } -> Factory
createFactory = \{ seed } ->
@Factory { state: Random.seed seed, existingNames: Set.empty {} }
createRobot : Factory -> Robot
createRobot = \factory ->
@Robot { maybeName: Err NoName, factory }
boot : Robot -> Robot
boot = \robot ->
when robot |> getName is
Ok _ -> robot
Err NoName -> robot |> generateRandomName
getName : Robot -> Result Str [NoName]
getName = \@Robot { maybeName } ->
maybeName
getFactory : Robot -> Factory
getFactory = \@Robot { factory } ->
factory
generateRandomName : Robot -> Robot
generateRandomName = \@Robot { maybeName, factory } ->
(@Factory { state, existingNames }) = factory
{ updatedState, string: twoLetters } = randomString { state, generator: Random.u32 'A' 'Z', length: 2 }
{ updatedState: updatedState2, string: threeDigits } = randomString { state: updatedState, generator: Random.u32 '0' '9', length: 3 }
possibleName = "$(twoLetters)$(threeDigits)"
if existingNames |> Set.contains possibleName then
numberOfPossibleNames = 26 * 26 * 10 * 10 * 10
if existingNames |> Set.len == numberOfPossibleNames then
# better crash than run into an infinite loop
crash "Too many robots, we have run out of possible names!"
else
updatedFactory = @Factory { existingNames, state: updatedState2 }
generateRandomName (@Robot { maybeName, factory: updatedFactory })
else
updatedFactory = @Factory {
existingNames: existingNames |> Set.insert possibleName,
state: updatedState2,
}
@Robot { maybeName: Ok possibleName, factory: updatedFactory }
randomString : { state : Random.State U32, generator : Random.Generator U32 U32, length : U64 } -> { updatedState : Random.State U32, string : Str }
randomString = \{ state, generator, length } ->
List.range { start: At 0, end: Before length }
|> List.walk { state, characters: [] } \walk, _ ->
random = generator walk.state
updatedState = random.state
characters = walk.characters |> List.append (random.value |> Num.toU8)
{ state: updatedState, characters }
|> \{ state: updatedState, characters } ->
when characters |> Str.fromUtf8 is
Ok string -> { updatedState, string }
Err (BadUtf8 _ _) -> crash "Unreachable: characters are all ASCII"
### Next we will try to ensure that the random names are sufficiently diverse.
### For this, we will first create many robot names.
## Create many robots using a given random seed, and return their names
## encoded using Str.toUtf8.
## The default quantity is 1,000, which is enough to offer strong statistical
## guarantees in the tests below, for example the probability that any letter
## or digit is absent from all names is negligible.
generateRobotNames : { seed : U32, quantity ? U64 } -> List (List U8)
generateRobotNames = \{ seed, quantity ? 1000 } ->
factory = createFactory { seed }
List.range { start: At 0, end: Before quantity }
|> List.walk { names: [], factory } \state, _ ->
robot = state.factory |> createRobot |> boot
nameUtf8 =
when robot |> getName is
Ok name -> name |> Str.toUtf8
Err NoName -> crash "A robot must have a name after the first boot"
{
names: state.names |> List.append nameUtf8,
factory: robot |> getFactory,
}
|> .names
## many random robot names based on seed 0
manyNames0 : List (List U8)
manyNames0 = generateRobotNames { seed: 0 }
### Finally, we will try to ensure that the characters are not linearly
### correlated within each name or across consecutive names. This does not
### guarantee that the names are truly random, but at least it should rule out
### many types of non-random sequences (e.g., such as simply incrementing a
### counter).
## Convert a list of integers to F64s
toFloats : List (Num *) -> List F64
toFloats = \numbers ->
numbers |> List.map Num.toF64
## The R² correlation coefficient, also known as the coefficient of determination,
## measures the degree of linear correlation between two lists of numbers.
## It ranges from -∞ to +1.0.
## When both lists are strongly linearly correlated, R² approaches +1.0.
## When both lists are long and independently drawn from the same random
## distribution, R² approaches -1.0.
r2Coeff : List F64, List F64 -> F64
r2Coeff = \numbers1, numbers2 ->
length = numbers1 |> List.len |> Num.toF64
mean = numbers1 |> List.sum |> Num.div length
subtractMean = \val -> val - mean
square = \val -> val * val
# Total sum of squares (TSS)
tss = numbers1 |> List.map subtractMean |> List.map square |> List.sum
# Residual sum of squares (RSS)
rss = numbers1 |> List.map2 numbers2 Num.sub |> List.map square |> List.sum
epsilon = 1e-10 # to avoid division by zero
1.0 - rss / (tss + epsilon)
# To speed up the correlation tests, we truncate the list of names
correlationSampleSize = 200
# It's not impossible for the random characters to be correlated by chance,
# but given 200 letters or digits, the probability that the correlation
# coefficient ends up greater than this threshold is negligible
r2Threshold = -0.25
seemsIndependentEnoughFrom = \maybeChars1, maybeChars2 ->
when (maybeChars1, maybeChars2) is
(Ok chars1, Ok chars2) ->
r2Coeff (chars1 |> toFloats) (chars2 |> toFloats) < r2Threshold
_ -> Bool.false # unreachable if names are 5 chars long
Next I did
|
I've got some things to take care of before I can get to this, but I recommend:
|
That's great, thanks @Anton-4 ! Especially useful to see how you proceed in such cases, I've never used Valgrind. I'll follow your suggestions. 👍 |
This exercise did not have test case specifications, so I didn't create
template.j2
this time, I just wrote the test file manually. These tests include the core workflow (creating a factory, using it to create a robot, booting the robot, resetting it to factory defaults, etc.). It also includes some tests to ensure that the generated names have the right format, and that they look sufficiently random. For this I included some limited statistical tests. Hopefully this will reject very basic "random" number generators such as just incrementing a counter.I pushed exercism/roc-test-runner#12 to add the roc-random package to the roc-test-runner. The tests for this exercise will only work once that PR has been merged.