From ef7bde3c69682ee367a1b3069ce18c36f414d4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 28 Sep 2024 17:05:25 +1200 Subject: [PATCH 1/5] Add robot-name exercise and roc-random package --- config.json | 8 + config/generator_macros.j2 | 2 + .../practice/robot-name/.docs/instructions.md | 14 ++ .../practice/robot-name/.meta/Example.roc | 63 +++++++ .../practice/robot-name/.meta/config.json | 18 ++ exercises/practice/robot-name/RobotName.roc | 33 ++++ .../practice/robot-name/robot-name-test.roc | 162 ++++++++++++++++++ 7 files changed, 300 insertions(+) create mode 100644 exercises/practice/robot-name/.docs/instructions.md create mode 100644 exercises/practice/robot-name/.meta/Example.roc create mode 100644 exercises/practice/robot-name/.meta/config.json create mode 100644 exercises/practice/robot-name/RobotName.roc create mode 100644 exercises/practice/robot-name/robot-name-test.roc diff --git a/config.json b/config.json index 85a03e13..76d6deeb 100644 --- a/config.json +++ b/config.json @@ -495,6 +495,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "robot-name", + "name": "Robot Name", + "uuid": "88eac1aa-a4ae-405a-88ab-0dedd5ac7ae2", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "acronym", "name": "Acronym", diff --git a/config/generator_macros.j2 b/config/generator_macros.j2 index 8571de86..5fa228b9 100644 --- a/config/generator_macros.j2 +++ b/config/generator_macros.j2 @@ -27,6 +27,8 @@ app [main] { isodate: "https://github.com/imclerran/roc-isodate/releases/download/v0.5.0/ptg0ElRLlIqsxMDZTTvQHgUSkNrUSymQaGwTfv0UEmk.tar.br" {%- elif name == "json" -%} json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.10.2/FH4N0Sw-JSFXJfG3j54VEDPtXOoN-6I9v_IA8S18IGk.tar.br" + {%- elif name == "rand" -%} + rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.1/mJSD8-uN-biRqa6CiqdN4-VJsKXxY8b1eFf6mFTe93A.tar.br", {%- endif -%} {%- endfor -%} {%- endif %} diff --git a/exercises/practice/robot-name/.docs/instructions.md b/exercises/practice/robot-name/.docs/instructions.md new file mode 100644 index 00000000..fca3a41a --- /dev/null +++ b/exercises/practice/robot-name/.docs/instructions.md @@ -0,0 +1,14 @@ +# Instructions + +Manage robot factory settings. + +When a robot comes off the factory floor, it has no name. + +The first time you turn on a robot, a random name is generated in the format of two uppercase letters followed by three digits, such as RX837 or BC811. + +Every once in a while we need to reset a robot to its factory settings, which means that its name gets wiped. +The next time you ask, that robot will respond with a new random name. + +The names must be random: they should not follow a predictable sequence. +Using random names means a risk of collisions. +Your solution must ensure that every existing robot has a unique name. diff --git a/exercises/practice/robot-name/.meta/Example.roc b/exercises/practice/robot-name/.meta/Example.roc new file mode 100644 index 00000000..e39550f5 --- /dev/null +++ b/exercises/practice/robot-name/.meta/Example.roc @@ -0,0 +1,63 @@ +module [createFactory, createRobot, name] + +import rand.Random + +## 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 with a name composed of two letters followed by three digits +Robot := { + name : Str, +} + +createFactory : { seed : U32 } -> Factory +createFactory = \{ seed } -> + existingNames = Set.empty {} + @Factory { state: Random.seed seed, existingNames } + +createRobot : Factory -> { robot : Robot, updatedFactory : Factory } +createRobot = \@Factory { state, existingNames } -> + { 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 + @Factory { existingNames, state: updatedState2 } |> createRobot + else + updatedFactory = @Factory { + existingNames: existingNames |> Set.insert possibleName, + state: updatedState2, + } + robot = @Robot { name: possibleName } + { robot, updatedFactory } + +name : Robot -> Str +name = \@Robot { name: uniqueName } -> + uniqueName + +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" diff --git a/exercises/practice/robot-name/.meta/config.json b/exercises/practice/robot-name/.meta/config.json new file mode 100644 index 00000000..2e9fddc8 --- /dev/null +++ b/exercises/practice/robot-name/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": [ + "ageron" + ], + "files": { + "solution": [ + "RobotName.roc" + ], + "test": [ + "robot-name-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Manage robot factory settings.", + "source": "A debugging session with Paul Blackwell at gSchool." +} diff --git a/exercises/practice/robot-name/RobotName.roc b/exercises/practice/robot-name/RobotName.roc new file mode 100644 index 00000000..19b490b2 --- /dev/null +++ b/exercises/practice/robot-name/RobotName.roc @@ -0,0 +1,33 @@ +module [createFactory, createRobot, name] + +import rand.Random + +## A factory is used to create robots, and hold state such as the +## existing robot names and the current random state +Factory := { + # TODO: change this opaque type however you need + todo1 : U64, + todo2 : U64, + todo3 : U64, + # etc. +} + +Robot := { + # TODO: change this opaque type however you need + todo4 : U64, + todo5 : U64, + todo6 : U64, + # etc. +} + +createFactory : { seed : U32 } -> Factory +createFactory = \{ seed } -> + crash "Please implement the 'createFactory' function" + +createRobot : Factory -> { robot : Robot, updatedFactory : Factory } +createRobot = \factory -> + crash "Please implement the 'createRobot' function" + +name : Robot -> Str +name = \robot -> + crash "Please implement the 'name' function" diff --git a/exercises/practice/robot-name/robot-name-test.roc b/exercises/practice/robot-name/robot-name-test.roc new file mode 100644 index 00000000..bf5c6498 --- /dev/null +++ b/exercises/practice/robot-name/robot-name-test.roc @@ -0,0 +1,162 @@ +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.1/mJSD8-uN-biRqa6CiqdN4-VJsKXxY8b1eFf6mFTe93A.tar.br", +} + +main = + Task.ok {} + +import RobotName exposing [createFactory, createRobot, name] + +## 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. +robotNames : { seed : U32, quantity ? U64 } -> List (List U8) +robotNames = \{ seed, quantity ? 1000 } -> + factory = createFactory { seed } + List.range { start: At 0, end: Before quantity } + |> List.walk { robots: [], updatedFactory: factory } \state, _ -> + { robot, updatedFactory } = state.updatedFactory |> createRobot + { robots: state.robots |> List.append robot, updatedFactory } + |> .robots + |> List.map \robot -> robot |> name |> Str.toUtf8 + +## many random robot names based on seed 0 +names0 : List (List U8) +names0 = robotNames { seed: 0 } + +## many random robot names based on seed 1 +names1 : List (List U8) +names1 = robotNames { seed: 1 } + +## The set of letters from 'A' to 'Z' +capitalLetters : Set U8 +capitalLetters = List.range { start: At 'A', end: At 'Z' } |> Set.fromList + +## The set of digits from '0' to '9' +digits : Set U8 +digits = List.range { start: At '0', end: At '9' } |> Set.fromList + +## 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) + +# A robot's name must always be 5 characters long +expect + result = names0 |> List.map List.len |> Set.fromList + result == Set.single 5 + +# The first characters must range from 'A' to 'Z' +expect + result = names0 |> List.mapTry \names -> names |> List.get 0 + when result is + Ok chars -> Set.fromList chars == capitalLetters + Err OutOfBounds -> Bool.false + +# The second characters must also range from 'A' to 'Z' +expect + result = names0 |> List.mapTry \names -> names |> List.get 1 + when result is + Ok chars -> Set.fromList chars == capitalLetters + Err OutOfBounds -> Bool.false + +# The third characters must range from '0' to '9' +expect + result = names0 |> List.mapTry \names -> names |> List.get 2 + when result is + Ok chars -> Set.fromList chars == digits + Err OutOfBounds -> Bool.false + +# The fourth characters must range from '0' to '9' +expect + result = names0 |> List.mapTry \names -> names |> List.get 3 + when result is + Ok chars -> Set.fromList chars == digits + Err OutOfBounds -> Bool.false + +# The fifth characters must range from '0' to '9' +expect + result = names0 |> List.mapTry \names -> names |> List.get 4 + when result is + Ok chars -> Set.fromList chars == digits + Err OutOfBounds -> Bool.false + +# The same seed must generate the same robot names +expect + newNames0 = robotNames { seed: 0 } + names0 == newNames0 + +# Different seeds must generate different robot names (to be precise, it's +# technically possible for the two lists to be identical, but the probability +# is negligible when the lists are long enough). +expect names0 != names1 + +# All robot names coming from the same factory must be unique +expect + uniqueNames = names0 |> Set.fromList + numberOfNames = names0 |> List.len + numberOfUniqueNames = uniqueNames |> Set.len + numberOfNames == numberOfUniqueNames + +# 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 + +# Characters within a name should not be correlated +expect + truncatedNames0 = names0 |> List.takeFirst correlationSampleSize + [0, 1, 2, 3, 4] + |> List.joinMap \index1 -> [0, 1, 2, 3, 4] |> List.map \index2 -> (index1, index2) + |> List.dropIf \(index1, index2) -> index1 == index2 + |> List.all \(index1, index2) -> + maybeChars = truncatedNames0 |> List.dropLast 1 |> List.mapTry \chars -> chars |> List.get index1 + maybeCharsNext = truncatedNames0 |> List.dropFirst 1 |> List.mapTry \chars -> chars |> List.get index2 + when (maybeChars, maybeCharsNext) is + (Ok chars, Ok charsNext) -> + r2 = r2Coeff (chars |> toFloats) (charsNext |> toFloats) + r2 < r2Threshold + + _ -> Bool.false + +# Characters in consecutive names should not be correlated +expect + # we truncate the list to speed up the tests + truncatedNames0 = names0 |> List.takeFirst correlationSampleSize + truncatedNames1 = names0 |> 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 + when (maybeChars, maybeCharsNext) is + (Ok chars, Ok charsNext) -> + r2 = r2Coeff (chars |> toFloats) (charsNext |> toFloats) + r2 < r2Threshold + + _ -> Bool.false From 7f296d8afb3601d86cbba5afd2b06bffbfaef6c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sat, 28 Sep 2024 19:09:50 +1200 Subject: [PATCH 2/5] Update the API to better match the instructions --- .../practice/robot-name/.meta/Example.roc | 82 ++++--- exercises/practice/robot-name/RobotName.roc | 28 ++- .../practice/robot-name/robot-name-test.roc | 219 ++++++++++++------ 3 files changed, 217 insertions(+), 112 deletions(-) diff --git a/exercises/practice/robot-name/.meta/Example.roc b/exercises/practice/robot-name/.meta/Example.roc index e39550f5..946cbc9f 100644 --- a/exercises/practice/robot-name/.meta/Example.roc +++ b/exercises/practice/robot-name/.meta/Example.roc @@ -1,53 +1,79 @@ -module [createFactory, createRobot, name] +module [createFactory, createRobot, boot, reset, getName, getFactory] import rand.Random -## A factory is used to create robots, and hold state such as the -## existing robot names and the current random state +## 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 with a name composed of two letters followed by three digits +## A robot must either have no name or a name composed of two letters followed +## by three digits Robot := { - name : Str, + maybeName : Result Str [NoName], + factory : Factory, } createFactory : { seed : U32 } -> Factory createFactory = \{ seed } -> - existingNames = Set.empty {} - @Factory { state: Random.seed seed, existingNames } - -createRobot : Factory -> { robot : Robot, updatedFactory : Factory } -createRobot = \@Factory { state, existingNames } -> - { 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, - } + @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 + +reset : Robot -> Robot +reset = \robot -> + resetRobot = + when robot |> getName is + Err NoName -> robot + Ok nameToRemove -> + factory = robot |> getFactory |> removeName nameToRemove + @Robot { maybeName: Err NoName, factory } + + resetRobot |> boot + +getName : Robot -> Result Str _ +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 - @Factory { existingNames, state: updatedState2 } |> createRobot + 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 = @Robot { name: possibleName } - { robot, updatedFactory } + @Robot { maybeName: Ok possibleName, factory: updatedFactory } -name : Robot -> Str -name = \@Robot { name: uniqueName } -> - uniqueName +removeName : Factory, Str -> Factory +removeName = \@Factory { state, existingNames }, robotName -> + @Factory { state, existingNames: existingNames |> Set.remove robotName } randomString : { state : Random.State U32, generator : Random.Generator U32 U32, length : U64 } -> { updatedState : Random.State U32, string : Str } randomString = \{ state, generator, length } -> diff --git a/exercises/practice/robot-name/RobotName.roc b/exercises/practice/robot-name/RobotName.roc index 19b490b2..e64f8510 100644 --- a/exercises/practice/robot-name/RobotName.roc +++ b/exercises/practice/robot-name/RobotName.roc @@ -1,9 +1,9 @@ -module [createFactory, createRobot, name] +module [createFactory, createRobot, boot, reset, getName, getFactory] import rand.Random -## A factory is used to create robots, and hold state such as the -## existing robot names and the current random state +## A factory is used to create robots, and hold state such as the existing robot +## names and the current random state Factory := { # TODO: change this opaque type however you need todo1 : U64, @@ -12,6 +12,8 @@ Factory := { # etc. } +## A robot must either have no name or a name composed of two letters followed +## by three digits Robot := { # TODO: change this opaque type however you need todo4 : U64, @@ -24,10 +26,22 @@ createFactory : { seed : U32 } -> Factory createFactory = \{ seed } -> crash "Please implement the 'createFactory' function" -createRobot : Factory -> { robot : Robot, updatedFactory : Factory } +createRobot : Factory -> Robot createRobot = \factory -> crash "Please implement the 'createRobot' function" -name : Robot -> Str -name = \robot -> - crash "Please implement the 'name' function" +boot : Robot -> Robot +boot = \robot -> + crash "Please implement the 'boot' function" + +reset : Robot -> Robot +reset = \robot -> + crash "Please implement the 'reset' function" + +getName : Robot -> Result Str _ +getName = \robot -> + crash "Please implement the 'getName' function" + +getFactory : Robot -> Factory +getFactory = \robot -> + crash "Please implement the 'getFactory' function" diff --git a/exercises/practice/robot-name/robot-name-test.roc b/exercises/practice/robot-name/robot-name-test.roc index bf5c6498..03bdc1cf 100644 --- a/exercises/practice/robot-name/robot-name-test.roc +++ b/exercises/practice/robot-name/robot-name-test.roc @@ -6,157 +6,222 @@ app [main] { main = Task.ok {} -import RobotName exposing [createFactory, createRobot, name] +import RobotName exposing [createFactory, createRobot, boot, reset, getName, getFactory] + +### Let's start by testing the basic robot workflow + +# A new robot must not have a name +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot + result = robot |> getName + result |> Result.isErr + +# After the first boot, a robot must have a name +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> boot + result = robot |> getName + result |> Result.isOk + +# Rebooting a robot should leave its name unchanged +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> boot + name1 = robot |> getName + name2 = robot |> boot |> getName + name1 == name2 + +# After it is factory reset (which also reboots), a robot must have a name +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> boot |> reset + result = robot |> getName + result |> Result.isOk + +# After it is factory reset, a robot must have a new name. If by chance it +# is the same (you should buy a lottery ticket today), we can try again to get +# a new name. If it's the same again we can be pretty confident that there's a +# problem. +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> boot + name1 = robot |> getName + name2 = robot |> reset |> getName + name3 = robot |> reset |> reset |> getName + name1 != name2 || name1 != name3 + +# If you factory reset a new robot, since this includes a boot, the robot +# should have a name +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> reset + result = robot |> getName + result |> Result.isOk + +# Once created, a robot's name must be 5 characters long +expect + factory = createFactory { seed: 0 } + robot = factory |> createRobot |> boot + result = robot |> getName |> Result.try \n -> n |> Str.toUtf8 |> List.len |> Ok + result == Ok 5 + +### 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. -robotNames : { seed : U32, quantity ? U64 } -> List (List U8) -robotNames = \{ seed, quantity ? 1000 } -> +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 { robots: [], updatedFactory: factory } \state, _ -> - { robot, updatedFactory } = state.updatedFactory |> createRobot - { robots: state.robots |> List.append robot, updatedFactory } - |> .robots - |> List.map \robot -> robot |> name |> Str.toUtf8 + |> 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 -names0 : List (List U8) -names0 = robotNames { seed: 0 } +manyNames0 : List (List U8) +manyNames0 = generateRobotNames { seed: 0 } ## many random robot names based on seed 1 -names1 : List (List U8) -names1 = robotNames { seed: 1 } +manyNames1 : List (List U8) +manyNames1 = generateRobotNames { seed: 1 } ## The set of letters from 'A' to 'Z' capitalLetters : Set U8 capitalLetters = List.range { start: At 'A', end: At 'Z' } |> Set.fromList -## The set of digits from '0' to '9' -digits : Set U8 -digits = List.range { start: At '0', end: At '9' } |> Set.fromList - -## 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) - -# A robot's name must always be 5 characters long -expect - result = names0 |> List.map List.len |> Set.fromList - result == Set.single 5 - -# The first characters must range from 'A' to 'Z' +# The first character of a robot's name must range from 'A' to 'Z' expect - result = names0 |> List.mapTry \names -> names |> List.get 0 + result = manyNames0 |> List.mapTry \names -> names |> List.get 0 when result is Ok chars -> Set.fromList chars == capitalLetters Err OutOfBounds -> Bool.false -# The second characters must also range from 'A' to 'Z' +# The second character must also range from 'A' to 'Z' expect - result = names0 |> List.mapTry \names -> names |> List.get 1 + result = manyNames0 |> List.mapTry \names -> names |> List.get 1 when result is Ok chars -> Set.fromList chars == capitalLetters Err OutOfBounds -> Bool.false -# The third characters must range from '0' to '9' +## The set of digits from '0' to '9' +digits : Set U8 +digits = List.range { start: At '0', end: At '9' } |> Set.fromList + +# The third character must range from '0' to '9' expect - result = names0 |> List.mapTry \names -> names |> List.get 2 + result = manyNames0 |> List.mapTry \names -> names |> List.get 2 when result is Ok chars -> Set.fromList chars == digits Err OutOfBounds -> Bool.false -# The fourth characters must range from '0' to '9' +# The fourth character must range from '0' to '9' expect - result = names0 |> List.mapTry \names -> names |> List.get 3 + result = manyNames0 |> List.mapTry \names -> names |> List.get 3 when result is Ok chars -> Set.fromList chars == digits Err OutOfBounds -> Bool.false -# The fifth characters must range from '0' to '9' +# The fifth character must range from '0' to '9' expect - result = names0 |> List.mapTry \names -> names |> List.get 4 + result = manyNames0 |> List.mapTry \names -> names |> List.get 4 when result is Ok chars -> Set.fromList chars == digits Err OutOfBounds -> Bool.false # The same seed must generate the same robot names expect - newNames0 = robotNames { seed: 0 } - names0 == newNames0 + newNames0 = generateRobotNames { seed: 0 } + manyNames0 == newNames0 # Different seeds must generate different robot names (to be precise, it's # technically possible for the two lists to be identical, but the probability # is negligible when the lists are long enough). -expect names0 != names1 +expect manyNames0 != manyNames1 # All robot names coming from the same factory must be unique expect - uniqueNames = names0 |> Set.fromList - numberOfNames = names0 |> List.len + uniqueNames = manyNames0 |> Set.fromList + numberOfNames = manyNames0 |> List.len numberOfUniqueNames = uniqueNames |> Set.len numberOfNames == numberOfUniqueNames -# to speed up the correlation tests, we truncate the list of names +### 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, +# 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 + # Characters within a name should not be correlated expect - truncatedNames0 = names0 |> List.takeFirst correlationSampleSize + truncatedNames0 = manyNames0 |> List.takeFirst correlationSampleSize [0, 1, 2, 3, 4] |> List.joinMap \index1 -> [0, 1, 2, 3, 4] |> List.map \index2 -> (index1, index2) |> List.dropIf \(index1, index2) -> index1 == index2 |> List.all \(index1, index2) -> maybeChars = truncatedNames0 |> List.dropLast 1 |> List.mapTry \chars -> chars |> List.get index1 maybeCharsNext = truncatedNames0 |> List.dropFirst 1 |> List.mapTry \chars -> chars |> List.get index2 - when (maybeChars, maybeCharsNext) is - (Ok chars, Ok charsNext) -> - r2 = r2Coeff (chars |> toFloats) (charsNext |> toFloats) - r2 < r2Threshold - - _ -> Bool.false + maybeChars |> seemsIndependentEnoughFrom maybeCharsNext # Characters in consecutive names should not be correlated expect # we truncate the list to speed up the tests - truncatedNames0 = names0 |> List.takeFirst correlationSampleSize - truncatedNames1 = names0 |> List.dropFirst 1 |> List.takeFirst correlationSampleSize + 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 - when (maybeChars, maybeCharsNext) is - (Ok chars, Ok charsNext) -> - r2 = r2Coeff (chars |> toFloats) (charsNext |> toFloats) - r2 < r2Threshold - - _ -> Bool.false + maybeChars |> seemsIndependentEnoughFrom maybeCharsNext From 3615e9a4c5ee4f5ac5b73c3d7f2149c18b10bd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Sun, 29 Sep 2024 22:55:09 +1300 Subject: [PATCH 3/5] Add [NoName] annotation to getName --- exercises/practice/robot-name/.meta/Example.roc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/robot-name/.meta/Example.roc b/exercises/practice/robot-name/.meta/Example.roc index 946cbc9f..fa77b47a 100644 --- a/exercises/practice/robot-name/.meta/Example.roc +++ b/exercises/practice/robot-name/.meta/Example.roc @@ -41,7 +41,7 @@ reset = \robot -> resetRobot |> boot -getName : Robot -> Result Str _ +getName : Robot -> Result Str [NoName] getName = \@Robot { maybeName } -> maybeName From bdd0156722bae5077b5ba2d49af57241939bd590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 2 Oct 2024 23:39:53 +1300 Subject: [PATCH 4/5] Bump up roc-random to 0.2.2 --- config/generator_macros.j2 | 2 +- exercises/practice/robot-name/robot-name-test.roc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/generator_macros.j2 b/config/generator_macros.j2 index 5fa228b9..e03399f8 100644 --- a/config/generator_macros.j2 +++ b/config/generator_macros.j2 @@ -28,7 +28,7 @@ app [main] { {%- elif name == "json" -%} json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.10.2/FH4N0Sw-JSFXJfG3j54VEDPtXOoN-6I9v_IA8S18IGk.tar.br" {%- elif name == "rand" -%} - rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.1/mJSD8-uN-biRqa6CiqdN4-VJsKXxY8b1eFf6mFTe93A.tar.br", + rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.2/cfMw9d_uxoqozMTg7Rvk-By3k1RscEDoR1sZIPVBRKQ.tar.br", {%- endif -%} {%- endfor -%} {%- endif %} diff --git a/exercises/practice/robot-name/robot-name-test.roc b/exercises/practice/robot-name/robot-name-test.roc index 03bdc1cf..5a2b4caf 100644 --- a/exercises/practice/robot-name/robot-name-test.roc +++ b/exercises/practice/robot-name/robot-name-test.roc @@ -1,6 +1,6 @@ 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.1/mJSD8-uN-biRqa6CiqdN4-VJsKXxY8b1eFf6mFTe93A.tar.br", + rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.2/cfMw9d_uxoqozMTg7Rvk-By3k1RscEDoR1sZIPVBRKQ.tar.br", } main = From 73ab4ffca66156ca26206abfa5b4773208d3c332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Fri, 18 Oct 2024 17:07:28 +1300 Subject: [PATCH 5/5] Upgrade to roc-random 0.3.0 --- config/generator_macros.j2 | 2 +- exercises/practice/robot-name/.meta/Example.roc | 8 ++++---- exercises/practice/robot-name/robot-name-test.roc | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/generator_macros.j2 b/config/generator_macros.j2 index e03399f8..58db8ac6 100644 --- a/config/generator_macros.j2 +++ b/config/generator_macros.j2 @@ -28,7 +28,7 @@ app [main] { {%- elif name == "json" -%} json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.10.2/FH4N0Sw-JSFXJfG3j54VEDPtXOoN-6I9v_IA8S18IGk.tar.br" {%- elif name == "rand" -%} - rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.2.2/cfMw9d_uxoqozMTg7Rvk-By3k1RscEDoR1sZIPVBRKQ.tar.br", + rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.3.0/hPlOciYUhWMU7BefqNzL89g84-30fTE6l2_6Y3cxIcE.tar.br", {%- endif -%} {%- endfor -%} {%- endif %} diff --git a/exercises/practice/robot-name/.meta/Example.roc b/exercises/practice/robot-name/.meta/Example.roc index fa77b47a..473cf6af 100644 --- a/exercises/practice/robot-name/.meta/Example.roc +++ b/exercises/practice/robot-name/.meta/Example.roc @@ -6,7 +6,7 @@ import rand.Random ## names and the current random state Factory := { existingNames : Set Str, - state : Random.State U32, + state : Random.State, } ## A robot must either have no name or a name composed of two letters followed @@ -52,8 +52,8 @@ getFactory = \@Robot { 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 } + { updatedState, string: twoLetters } = randomString { state, generator: Random.boundedU32 'A' 'Z', length: 2 } + { updatedState: updatedState2, string: threeDigits } = randomString { state: updatedState, generator: Random.boundedU32 '0' '9', length: 3 } possibleName = "$(twoLetters)$(threeDigits)" if existingNames |> Set.contains possibleName then @@ -75,7 +75,7 @@ removeName : Factory, Str -> Factory removeName = \@Factory { state, existingNames }, robotName -> @Factory { state, existingNames: existingNames |> Set.remove robotName } -randomString : { state : Random.State U32, generator : Random.Generator U32 U32, length : U64 } -> { updatedState : Random.State U32, string : Str } +randomString : { state : Random.State, generator : Random.Generator U32, length : U64 } -> { updatedState : Random.State, string : Str } randomString = \{ state, generator, length } -> List.range { start: At 0, end: Before length } |> List.walk { state, characters: [] } \walk, _ -> diff --git a/exercises/practice/robot-name/robot-name-test.roc b/exercises/practice/robot-name/robot-name-test.roc index 5a2b4caf..82f12357 100644 --- a/exercises/practice/robot-name/robot-name-test.roc +++ b/exercises/practice/robot-name/robot-name-test.roc @@ -1,6 +1,6 @@ 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", + rand: "https://github.com/lukewilliamboswell/roc-random/releases/download/0.3.0/hPlOciYUhWMU7BefqNzL89g84-30fTE6l2_6Y3cxIcE.tar.br", } main =